diff --git a/.editorconfig b/.editorconfig index 3620896f34..a144ec8843 100644 --- a/.editorconfig +++ b/.editorconfig @@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +# private static fields should have s_ prefix +dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.private_static_prefix_style.required_prefix = s_ +dotnet_naming_style.private_static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion @@ -117,7 +118,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false @@ -137,15 +138,22 @@ space_within_single_line_array_initializer_braces = true #Net Analyzer dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed. +# CS0649: Field 'field' is never assigned to, and will always have its default value 'value' +dotnet_diagnostic.CS0649.severity = error + # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = suggestion # CS0162: Remove unreachable code dotnet_diagnostic.CS0162.severity = error +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = error # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length @@ -204,5 +212,5 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf -[*.{cmd, bat}] +[*.{cmd,bat}] end_of_line = crlf diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..4a7a329fc6 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -5,6 +5,7 @@ "packages\\Avalonia\\Avalonia.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", + "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", @@ -14,6 +15,7 @@ "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", "src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj", + "src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj", "src\\Avalonia.Controls\\Avalonia.Controls.csproj", "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj", "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", @@ -40,9 +42,11 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", + "tests\\Avalonia.Controls.ItemsRepeater.UnitTests\\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj", "tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj", "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj", @@ -58,4 +62,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..56847bae31 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +555,21 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.Build.0 = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +634,9 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/Base.props b/build/Base.props index 26f19e3abc..2d50a7eae0 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,8 @@  - + + + diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 620ec58ff3..75d317be1a 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/ImageSharp.props b/build/ImageSharp.props index 178c274ac9..66e6580070 100644 --- a/build/ImageSharp.props +++ b/build/ImageSharp.props @@ -1,5 +1,5 @@ - + diff --git a/build/Moq.props b/build/Moq.props index 9e2fd1db5d..357f0c9a5f 100644 --- a/build/Moq.props +++ b/build/Moq.props @@ -1,5 +1,5 @@  - + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index eca3ba37b0..2849262591 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,6 +3,7 @@ Avalonia 11.0.999 + Avalonia Team Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ diff --git a/build/SharpDX.props b/build/SharpDX.props index 69aa817a01..ff521977fd 100644 --- a/build/SharpDX.props +++ b/build/SharpDX.props @@ -1,9 +1,14 @@  + + 4.0.1 + - - - - - + + + + + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 31619399f9..f45addaa2a 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/System.Memory.props b/build/System.Memory.props index a413e18927..35a87a3a2f 100644 --- a/build/System.Memory.props +++ b/build/System.Memory.props @@ -1,5 +1,6 @@ - + + diff --git a/build/XUnit.props b/build/XUnit.props index 17ead91aa3..3c89c8b52b 100644 --- a/build/XUnit.props +++ b/build/XUnit.props @@ -1,13 +1,12 @@  - - - - - - - - + + + + + + + diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 41d1534f8d..c49290314d 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; + 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; @@ -95,6 +96,7 @@ 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; + 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -140,6 +142,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, BC11A5BC2608D58F0017BAD0 /* automation.h */, BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, @@ -288,6 +291,7 @@ 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, + 855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index d3b7b4ede6..23abf1d53f 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -223,6 +223,19 @@ } } +// From chromium: +// +// > The delegate or the window class should implement this method so that +// > -[NSWindow isZoomed] can be then determined by whether or not the current +// > window frame is equal to the zoomed frame. +// +// If we don't implement this, then isZoomed always returns true for a non- +// resizable window ¯\_(ツ)_/¯ +- (NSRect)windowWillUseStandardFrame:(NSWindow*)window + defaultFrame:(NSRect)newFrame { + return newFrame; +} + -(BOOL)canBecomeKeyWindow { if(_canBecomeKeyWindow) @@ -261,10 +274,6 @@ -(void) setEnabled:(bool)enable { _isEnabled = enable; - - [[self standardWindowButton:NSWindowCloseButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable]; - [[self standardWindowButton:NSWindowZoomButton] setEnabled:enable]; } -(void)becomeKeyWindow diff --git a/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm b/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm new file mode 100644 index 0000000000..db054d82ef --- /dev/null +++ b/native/Avalonia.Native/src/OSX/PlatformBehaviorInhibition.mm @@ -0,0 +1,39 @@ +#include "common.h" + +namespace +{ + id s_inhibitAppSleepHandle{}; +} + +class PlatformBehaviorInhibition : public ComSingleObject +{ +public: + FORWARD_IUNKNOWN() + + virtual void SetInhibitAppSleep(bool inhibitAppSleep, char* reason) override + { + START_COM_CALL; + + @autoreleasepool + { + if (inhibitAppSleep && s_inhibitAppSleepHandle == nullptr) + { + NSActivityOptions options = NSActivityUserInitiatedAllowingIdleSystemSleep; + s_inhibitAppSleepHandle = [[NSProcessInfo processInfo] beginActivityWithOptions:options reason:[NSString stringWithUTF8String: reason]]; + } + + if (!inhibitAppSleep) + { + s_inhibitAppSleepHandle = nullptr; + } + } + } +}; + +extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition() +{ + @autoreleasepool + { + return new PlatformBehaviorInhibition(); + } +} diff --git a/native/Avalonia.Native/src/OSX/PopupImpl.mm b/native/Avalonia.Native/src/OSX/PopupImpl.mm index 9820a9f052..972d03d08c 100644 --- a/native/Avalonia.Native/src/OSX/PopupImpl.mm +++ b/native/Avalonia.Native/src/OSX/PopupImpl.mm @@ -29,7 +29,7 @@ private: [Window setLevel:NSPopUpMenuWindowLevel]; } protected: - virtual NSWindowStyleMask GetStyle() override + virtual NSWindowStyleMask CalculateStyleMask() override { return NSWindowStyleMaskBorderless; } diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..93decef136 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -105,9 +105,8 @@ BEGIN_INTERFACE_MAP() virtual void BringToFront (); protected: - virtual NSWindowStyleMask GetStyle(); - - void UpdateStyle(); + virtual NSWindowStyleMask CalculateStyleMask() = 0; + virtual void UpdateStyle(); private: void CreateNSWindow (bool isDialog); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..59102e15a6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, lastSize = NSSize { 100, 100 }; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMinSize = NSSize { 0, 0 }; - lastMenu = nullptr; CreateNSWindow(usePanel); [Window setContentView:StandardContainer]; - [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - [Window setContentMinSize:lastMinSize]; [Window setContentMaxSize:lastMaxSize]; - [Window setOpaque:false]; } @@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() { return false; } -NSWindowStyleMask WindowBaseImpl::GetStyle() { - return NSWindowStyleMaskBorderless; -} - void WindowBaseImpl::UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask:CalculateStyleMask()]; } void WindowBaseImpl::CleanNSWindow() { @@ -580,21 +572,12 @@ void WindowBaseImpl::CleanNSWindow() { } } -void WindowBaseImpl::CreateNSWindow(bool isDialog) { - if (isDialog) { - if (![Window isKindOfClass:[AvnPanel class]]) { - CleanNSWindow(); - - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - - [Window setHidesOnDeactivate:false]; - } +void WindowBaseImpl::CreateNSWindow(bool usePanel) { + if (usePanel) { + Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + [Window setHidesOnDeactivate:false]; } else { - if (![Window isKindOfClass:[AvnWindow class]]) { - CleanNSWindow(); - - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()]; - } + Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; } } diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.h b/native/Avalonia.Native/src/OSX/WindowImpl.h index 3861aaf170..29bb659039 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowImpl.h @@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP() WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); - void HideOrShowTrafficLights (); - virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT SetEnabled (bool enable) override; @@ -100,9 +98,11 @@ BEGIN_INTERFACE_MAP() bool CanBecomeKeyWindow (); protected: - virtual NSWindowStyleMask GetStyle() override; + virtual NSWindowStyleMask CalculateStyleMask() override; + void UpdateStyle () override; private: + void ZOrderChildWindows(); void OnInitialiseNSWindow(); NSString *_lastTitle; }; diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index f345043f61..cf1ee6943d 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase OnInitialiseNSWindow(); } -void WindowImpl::HideOrShowTrafficLights() { - if (Window == nil) { - return; - } - - bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); - bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; - - [[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights]; - [[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights]; -} - void WindowImpl::OnInitialiseNSWindow(){ [GetWindowProtocol() setCanBecomeKeyWindow:true]; @@ -66,9 +53,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) { _isModal = isDialog; WindowBaseImpl::Show(activate, isDialog); - - HideOrShowTrafficLights(); - + GetWindowState(&_actualWindowState); return SetWindowState(_lastWindowState); } } @@ -134,14 +119,19 @@ void WindowImpl::BringToFront() } [Window invalidateShadow]; + ZOrderChildWindows(); + } +} + +void WindowImpl::ZOrderChildWindows() +{ + for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) + { + auto window = (*iterator)->Window; - for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) - { - auto window = (*iterator)->Window; - - // #9565: Only bring window to front if it's on the currently active space - if ([window isOnActiveSpace]) - (*iterator)->BringToFront(); + // #9565: Only bring window to front if it's on the currently active space + if ([window isOnActiveSpace]) { + (*iterator)->BringToFront(); } } } @@ -161,13 +151,15 @@ bool WindowImpl::CanBecomeKeyWindow() void WindowImpl::StartStateTransition() { _transitioningWindowState = true; + UpdateStyle(); } void WindowImpl::EndStateTransition() { _transitioningWindowState = false; - + UpdateStyle(); + // Ensure correct order of child windows after fullscreen transition. - BringToFront(); + ZOrderChildWindows(); } SystemDecorations WindowImpl::Decorations() { @@ -225,16 +217,12 @@ bool WindowImpl::IsZoomed() { } void WindowImpl::DoZoom() { - switch (_decorations) { - case SystemDecorationsNone: - case SystemDecorationsBorderOnly: - [Window setFrame:[Window screen].visibleFrame display:true]; - break; - - - case SystemDecorationsFull: - [Window performZoom:Window]; - break; + if (_decorations == SystemDecorationsNone || + _decorations == SystemDecorationsBorderOnly || + _canResize == false) { + [Window setFrame:[Window screen].visibleFrame display:true]; + } else { + [Window performZoom:Window]; } } @@ -261,8 +249,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) { UpdateStyle(); - HideOrShowTrafficLights(); - switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; @@ -419,9 +405,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) { } [GetWindowProtocol() setIsExtended:enable]; - - HideOrShowTrafficLights(); - UpdateStyle(); } @@ -577,14 +560,16 @@ bool WindowImpl::IsOwned() { return _parent != nullptr; } -NSWindowStyleMask WindowImpl::GetStyle() { - unsigned long s = NSWindowStyleMaskBorderless; +NSWindowStyleMask WindowImpl::CalculateStyleMask() { + // Use the current style mask and only clear the flags we're going to be modifying. + unsigned long s = [Window styleMask] & + ~(NSWindowStyleMaskFullSizeContentView | + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskResizable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskTexturedBackground); - if(_actualWindowState == FullScreen) - { - s |= NSWindowStyleMaskFullScreen; - } - switch (_decorations) { case SystemDecorationsNone: s = s | NSWindowStyleMaskFullSizeContentView; @@ -597,7 +582,7 @@ NSWindowStyleMask WindowImpl::GetStyle() { case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; - if (_canResize && _isEnabled) { + if ((_canResize && _isEnabled) || _transitioningWindowState) { s = s | NSWindowStyleMaskResizable; } break; @@ -612,3 +597,25 @@ NSWindowStyleMask WindowImpl::GetStyle() { } return s; } + +void WindowImpl::UpdateStyle() { + WindowBaseImpl::UpdateStyle(); + + if (Window == nil) { + return; + } + + bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull; + + NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton]; + NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton]; + NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton]; + + [closeButton setHidden:!hasTrafficLights]; + [closeButton setEnabled:_isEnabled]; + [miniaturizeButton setHidden:!hasTrafficLights]; + [miniaturizeButton setEnabled:_isEnabled]; + [zoomButton setHidden:!hasTrafficLights]; + [zoomButton setEnabled:_isEnabled && _canResize]; +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 972927b99d..4353737dc8 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -26,6 +26,7 @@ extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnApplicationCommands* CreateApplicationCommands(); +extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnPlatformSettings* CreatePlatformSettings(); extern void SetAppMenu(IAvnMenu *menu); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 99063e600e..4bfda4b531 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -408,6 +408,17 @@ public: return S_OK; } } + + virtual HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreatePlatformBehaviorInhibition(); + return S_OK; + } + } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/readme.md b/readme.md index c2be487af3..2600cf83cc 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +[![GH_Banner](https://user-images.githubusercontent.com/552074/218457976-92e76834-9e22-4e35-acfa-aa50281bc0f9.png)](https://avaloniaui.net/xpf) + [![Telegram](https://raw.githubusercontent.com/Patrolavia/telegram-badge/master/chat.svg)](https://t.me/Avalonia) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Discord](https://img.shields.io/badge/discord-join%20chat-46BC99)]( https://aka.ms/dotnet-discord) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 5a8e65ed22..84f54293ef 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -2,13 +2,6 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="BindingDemo.App"> - - - - - - - diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs index f38db2b055..c331625664 100644 --- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs +++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser.Blazor; @@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor; public partial class App { - protected override void OnParametersSet() - { - AppBuilder.Configure() - .UseBlazor() - // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } } diff --git a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj index d0fb614840..733a4b7194 100644 --- a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj +++ b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj @@ -9,8 +9,8 @@
- - + + diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index eb99ca518e..e68e9b14d9 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Blazor; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ControlCatalog.Browser.Blazor; @@ -9,9 +11,17 @@ public class Program { public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + var host = CreateHostBuilder(args).Build(); + await StartAvaloniaApp(); + await host.RunAsync(); } + public static async Task StartAvaloniaApp() + { + await AppBuilder.Configure() + .StartBlazorAppAsync(); + } + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 53b7c60a6f..e1a4500173 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; +using Avalonia.Controls; using ControlCatalog; using ControlCatalog.Browser; @@ -8,15 +10,27 @@ using ControlCatalog.Browser; internal partial class Program { - private static void Main(string[] args) + public static async Task Main(string[] args) { - BuildAvaloniaApp() + await BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }).SetupBrowserApp("out"); + }) + .StartBrowserAppAsync("out"); } + // Example without a ISingleViewApplicationLifetime + // private static AvaloniaView _avaloniaView; + // public static async Task Main(string[] args) + // { + // await BuildAvaloniaApp() + // .SetupBrowserApp(); + // + // _avaloniaView = new AvaloniaView("out"); + // _avaloniaView.Content = new TextBlock { Text = "Hello world" }; + // } + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js index 87f8a4f943..9d90db8bd2 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await registerAvaloniaModule(dotnetRuntime); - const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e4c83dca49..e465e9caf3 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -31,7 +31,6 @@ - diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index d5e5cb14dc..e55f003133 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -55,8 +55,7 @@ namespace ControlCatalog.NetCore return builder .UseHeadless(new AvaloniaHeadlessPlatformOptions { - UseHeadlessDrawing = true, - UseCompositor = true + UseHeadlessDrawing = true }) .AfterSetup(_ => { diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 8f32fa01dd..3b847adcbb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -6,18 +6,34 @@ x:Class="ControlCatalog.App"> + + + + + #33000000 + #99000000 + #FFE6E6E6 + #FF000000 + + + #33FFFFFF + #99FFFFFF + #FF1F1F1F + #FFFFFFFF + + + #FF0078D7 + #FF005A9E + + - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6c99eb5289..d71d51f068 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,6 @@ namespace ControlCatalog private readonly Styles _themeStylesContainer = new(); private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; - private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors; private IStyle? _colorPickerFluent, _colorPickerSimple; private IStyle? _dataGridFluent, _dataGridSimple; @@ -33,16 +32,12 @@ namespace ControlCatalog _fluentTheme = new FluentTheme(); _simpleTheme = new SimpleTheme(); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!); _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; - _fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!; - _fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!; - SetThemeVariant(CatalogTheme.FluentLight); + SetCatalogThemes(CatalogTheme.Fluent); } public override void OnFrameworkInitializationCompleted() @@ -61,19 +56,12 @@ namespace ControlCatalog private CatalogTheme _prevTheme; public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; - public static void SetThemeVariant(CatalogTheme theme) + public static void SetCatalogThemes(CatalogTheme theme) { var app = (App)Current!; var prevTheme = app._prevTheme; app._prevTheme = theme; - var shouldReopenWindow = theme switch - { - CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null) - }; + var shouldReopenWindow = prevTheme != theme; if (app._themeStylesContainer.Count == 0) { @@ -81,36 +69,16 @@ namespace ControlCatalog app._themeStylesContainer.Add(new Style()); app._themeStylesContainer.Add(new Style()); } - - if (theme == CatalogTheme.FluentLight) - { - app._fluentTheme!.Mode = FluentThemeMode.Light; - app._themeStylesContainer[0] = app._fluentTheme; - app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; - } - else if (theme == CatalogTheme.FluentDark) + + if (theme == CatalogTheme.Fluent) { - app._fluentTheme!.Mode = FluentThemeMode.Dark; - app._themeStylesContainer[0] = app._fluentTheme; + app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; app._themeStylesContainer[2] = app._dataGridFluent!; } - else if (theme == CatalogTheme.SimpleLight) - { - app._simpleTheme!.Mode = SimpleThemeMode.Light; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!); - app._themeStylesContainer[0] = app._simpleTheme; - app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; - } - else if (theme == CatalogTheme.SimpleDark) + else if (theme == CatalogTheme.Simple) { - app._simpleTheme!.Mode = SimpleThemeMode.Dark; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!); - app._themeStylesContainer[0] = app._simpleTheme; + app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; app._themeStylesContainer[2] = app._dataGridSimple!; } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 18f0dd16ba..c223bfe1a9 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -26,6 +26,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..3681298a72 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -14,8 +14,8 @@ - - + + @@ -144,9 +144,12 @@ - + + + + @@ -165,6 +168,9 @@ + + + @@ -198,14 +204,22 @@ Full + + + Default + Light + Dark + + - FluentLight - FluentDark - SimpleLight - SimpleDark + Fluent + Simple PlatformThemeVariant.Light, - CatalogTheme.FluentDark => PlatformThemeVariant.Dark, - CatalogTheme.SimpleLight => PlatformThemeVariant.Light, - CatalogTheme.SimpleDark => PlatformThemeVariant.Dark, - _ => throw new ArgumentOutOfRangeException() - }); + App.SetCatalogThemes(theme); + } + }; + var themeVariants = this.Get("ThemeVariants"); + themeVariants.SelectedItem = Application.Current!.RequestedThemeVariant; + themeVariants.SelectionChanged += (sender, e) => + { + if (themeVariants.SelectedItem is ThemeVariant themeVariant) + { + Application.Current!.RequestedThemeVariant = themeVariant; } }; @@ -60,7 +61,7 @@ namespace ControlCatalog { if (flowDirections.SelectedItem is FlowDirection flowDirection) { - this.FlowDirection = flowDirection; + TopLevel.GetTopLevel(this).FlowDirection = flowDirection; } }; @@ -118,25 +119,13 @@ namespace ControlCatalog private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) { - var themes = this.Get("Themes"); - var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight; - var newTheme = (currentTheme, e.ThemeVariant) switch - { - (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight, - (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark, - (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight, - (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark, - _ => currentTheme - }; - themes.SelectedItem = newTheme; - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); + Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); + Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); static Color ChangeColorLuminosity(Color color, double luminosityFactor) { diff --git a/samples/ControlCatalog/Models/CatalogTheme.cs b/samples/ControlCatalog/Models/CatalogTheme.cs index 37224ed26e..79b3182d20 100644 --- a/samples/ControlCatalog/Models/CatalogTheme.cs +++ b/samples/ControlCatalog/Models/CatalogTheme.cs @@ -2,9 +2,7 @@ { public enum CatalogTheme { - FluentLight, - FluentDark, - SimpleLight, - SimpleDark + Fluent, + Simple } } diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 11e5e32cf1..938c45a4e2 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -59,10 +59,12 @@ namespace ControlCatalog.Pages }; StreamGeometry sg = new StreamGeometry(); - var cntx = sg.Open(); - cntx.BeginFigure(new Point(-25.0d, -10.0d), false); - cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); - cntx.EndFigure(true); + using (var cntx = sg.Open()) + { + cntx.BeginFigure(new Point(-25.0d, -10.0d), false); + cntx.ArcTo(new Point(25.0d, -10.0d), new Size(10.0d, 10.0d), 0.0d, false, SweepDirection.Clockwise); + cntx.EndFigure(true); + } _smileGeometry = sg.Clone(); } diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index 4c3c211ca5..c39e9f0a81 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -1,7 +1,9 @@ + xmlns:pages="clr-namespace:ControlCatalog.Pages" + x:Class="ControlCatalog.Pages.DataGridPage" + x:DataType="pages:DataGridPage"> @@ -33,7 +35,7 @@ - + - + + - - - - + + + + - + - + diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs index 3565d113bc..b0c3e3a553 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using Avalonia.Controls; @@ -48,20 +49,22 @@ namespace ControlCatalog.Pages var dg3 = this.Get("dataGridEdit"); dg3.IsReadOnly = false; - var items = new List + var list = new ObservableCollection { new Person { FirstName = "John", LastName = "Doe" , Age = 30}, new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 }, new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 } }; - var collectionView3 = new DataGridCollectionView(items); - - dg3.Items = collectionView3; + DataGrid3Source = list; var addButton = this.Get - + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index ded02330d5..c77d65ddf1 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -78,12 +78,7 @@ namespace ControlCatalog.Pages get => _info; private set => SetAndRaise(InfoProperty, ref _info, value); } - - static OpenGlPageControl() - { - AffectsRender(YawProperty, PitchProperty, RollProperty, DiscoProperty); - } - + private int _vertexShader; private int _fragmentShader; private int _shaderProgram; @@ -254,7 +249,7 @@ namespace ControlCatalog.Pages Console.WriteLine(err); } - protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) + protected override unsafe void OnOpenGlInit(GlInterface GL) { CheckError(GL); @@ -309,7 +304,7 @@ namespace ControlCatalog.Pages } - protected override void OnOpenGlDeinit(GlInterface GL, int fb) + protected override void OnOpenGlDeinit(GlInterface GL) { // Unbind everything GL.BindBuffer(GL_ARRAY_BUFFER, 0); @@ -366,7 +361,15 @@ namespace ControlCatalog.Pages CheckError(GL); if (_disco > 0.01) - Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + RequestNextFrameRendering(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty || change.Property == RollProperty || change.Property == PitchProperty || + change.Property == DiscoProperty) + RequestNextFrameRendering(); + base.OnPropertyChanged(change); } } } diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b5b80fb147..2cdd031693 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -12,6 +14,11 @@ namespace ControlCatalog.Pages public class ScreenPage : UserControl { private double _leftMost; + private double _topMost; + private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7"); + private IBrush _defaultBrush = Brushes.LightGray; + private IPen _activePen = new Pen(Brushes.Black); + private IPen _defaultPen = new Pen(Brushes.DarkGray); protected override bool BypassFlowDirectionPolicies => true; @@ -34,54 +41,88 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Black; - Pen p = new Pen(drawBrush); + var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); + double maxBottom = 0; - foreach (Screen screen in screens) + for (int i = 0; i + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen. + + + + + + + + + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs new file mode 100644 index 0000000000..384dc67c66 --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using MiniMvvm; + +namespace ControlCatalog.Pages +{ + public class ScrollSnapPageViewModel : ViewModelBase + { + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; + + public ScrollSnapPageViewModel() + { + + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } + } + + public class ScrollSnapPage : UserControl + { + public ScrollSnapPage() + { + this.InitializeComponent(); + + DataContext = new ScrollSnapPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1903e50ed7..fa8714959b 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -3,12 +3,13 @@ xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.ScrollViewerPage" x:DataType="pages:ScrollViewerPageViewModel"> - - Allows for horizontal and vertical content scrolling. + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. + @@ -24,6 +25,7 @@ @@ -31,6 +33,5 @@ Source="/Assets/delicate-arch-896885_640.jpg" /> - diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index dcd7a88a56..a097f1f951 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -9,6 +9,7 @@ namespace ControlCatalog.Pages public class ScrollViewerPageViewModel : ViewModelBase { private bool _allowAutoHide; + private bool _enableInertia; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; @@ -25,6 +26,7 @@ namespace ControlCatalog.Pages HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; + EnableInertia = true; } public bool AllowAutoHide @@ -33,6 +35,12 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); } + public bool EnableInertia + { + get => _enableInertia; + set => this.RaiseAndSetIfChanged(ref _enableInertia, value); + } + public ScrollBarVisibility HorizontalScrollVisibility { get => _horizontalScrollVisibility; diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 61bfb490b8..2edd895349 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -32,7 +32,7 @@ - SystemControlBackgroundChromeMediumLowBrush + CatalogChromeMediumColor Red Blue Green @@ -48,7 +48,7 @@ - - + @@ -89,11 +89,11 @@ - - - - - + + + + + diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 6bb428e2c7..6511e2136a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -9,7 +9,7 @@ @@ -101,7 +115,7 @@ VerticalAlignment="Center" Background="{DynamicResource TabItemHeaderSelectedPipeFill}" IsVisible="False" - CornerRadius="{DynamicResource ControlCornerRadius}"/> + CornerRadius="4"/> @@ -136,18 +150,18 @@ - - - + + + diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml index f601f9f78f..cf3e5e445a 100644 --- a/samples/Sandbox/App.axaml +++ b/samples/Sandbox/App.axaml @@ -3,6 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Sandbox.App"> - + diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs index fac06d74b0..c2d30c67bb 100644 --- a/samples/interop/WindowsInteropTest/Program.cs +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls; using ControlCatalog; using Avalonia; @@ -15,7 +14,15 @@ namespace WindowsInteropTest { System.Windows.Forms.Application.EnableVisualStyles(); System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AppBuilder.Configure().UseWin32().UseDirect2D1().SetupWithoutStarting(); + AppBuilder.Configure() + .UseWin32() + .UseDirect2D1() + .With(new Win32PlatformOptions + { + UseWindowsUIComposition = false, + ShouldRenderOnUIThread = true // necessary for WPF + }) + .SetupWithoutStarting(); System.Windows.Forms.Application.Run(new SelectorForm()); } } diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj index 1643ca3ee2..95f77f6df9 100644 --- a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -2,7 +2,7 @@ WinExe net461 - + x64 true true @@ -10,9 +10,6 @@ - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - + diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index c885a7768c..27dcfe8645 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -5,8 +5,10 @@ using Android.Text; using Android.Views; using Android.Views.InputMethods; using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Input.TextInput; +using Avalonia.Reactive; namespace Avalonia.Android { @@ -32,7 +34,7 @@ namespace Avalonia.Android ActionPrevious = 0x00000007, } - class AndroidInputMethod : ITextInputMethodImpl, IAndroidInputMethod + internal class AndroidInputMethod : ITextInputMethodImpl, IAndroidInputMethod where TView : View, IInitEditorInfo { private readonly TView _host; @@ -68,23 +70,10 @@ namespace Avalonia.Android public void SetClient(ITextInputMethodClient client) { - if (_client != null) - { - _client.SurroundingTextChanged -= SurroundingTextChanged; - } - - if(_inputConnection != null) - { - _inputConnection.ComposingText = null; - _inputConnection.ComposingRegion = default; - } - _client = client; if (IsActive) { - _client.SurroundingTextChanged += SurroundingTextChanged; - _host.RequestFocus(); _imm.RestartInput(View); @@ -101,24 +90,6 @@ namespace Avalonia.Android } } - private void SurroundingTextChanged(object sender, EventArgs e) - { - if (IsActive && _inputConnection != null) - { - var surroundingText = Client.SurroundingText; - - _inputConnection.SurroundingText = surroundingText; - - _imm.UpdateSelection(_host, surroundingText.AnchorOffset, surroundingText.CursorOffset, surroundingText.AnchorOffset, surroundingText.CursorOffset); - - if (_inputConnection.ComposingText != null && !_inputConnection.IsCommiting && surroundingText.AnchorOffset == surroundingText.CursorOffset) - { - _inputConnection.CommitText(_inputConnection.ComposingText, 0); - _inputConnection.SetSelection(surroundingText.AnchorOffset, surroundingText.CursorOffset); - } - } - } - public void SetCursorRect(Rect rect) { @@ -157,17 +128,20 @@ namespace Avalonia.Android TextInputReturnKeyType.Search => (ImeFlags)CustomImeFlags.ActionSearch, TextInputReturnKeyType.Next => (ImeFlags)CustomImeFlags.ActionNext, TextInputReturnKeyType.Previous => (ImeFlags)CustomImeFlags.ActionPrevious, - _ => (ImeFlags)CustomImeFlags.ActionDone + TextInputReturnKeyType.Done => (ImeFlags)CustomImeFlags.ActionDone, + _ => options.Multiline ? ImeFlags.NoEnterAction : (ImeFlags)CustomImeFlags.ActionDone }; outAttrs.ImeOptions |= ImeFlags.NoFullscreen | ImeFlags.NoExtractUi; + _client.TextEditable = _inputConnection.InputEditable; + return _inputConnection; }); } } - public readonly record struct ComposingRegion + internal readonly record struct ComposingRegion { private readonly int _start = -1; private readonly int _end = -1; diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index c73fd92423..daecb58a60 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -32,7 +32,6 @@ namespace Avalonia.Android public static AndroidPlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } - internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -55,16 +54,11 @@ namespace Avalonia.Android EglPlatformGraphics.TryInitialize(); } - if (Options.UseCompositor) - { - Compositor = new Compositor( - AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); - } - else - RenderInterface = - new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current - .GetService()); + Compositor = new Compositor( + AvaloniaLocator.Current.GetRequiredService(), + AvaloniaLocator.Current.GetService()); + + } } @@ -72,6 +66,5 @@ namespace Avalonia.Android { public bool UseDeferredRendering { get; set; } = false; public bool UseGpu { get; set; } = true; - public bool UseCompositor { get; set; } = true; } } diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 66557418dd..2533016e9f 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -5,6 +5,7 @@ true true portable + Avalonia.Android.Internal diff --git a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs index c06f5c74ec..247008c503 100644 --- a/src/Android/Avalonia.Android/AvaloniaMainActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaMainActivity.cs @@ -1,6 +1,8 @@ using System; using Android.App; using Android.Content; +using Android.Content.PM; +using Android.Content.Res; using Android.OS; using Android.Runtime; using Android.Views; @@ -13,6 +15,7 @@ namespace Avalonia.Android internal static object ViewContent; public Action ActivityResult { get; set; } + public Action RequestPermissionsResult { get; set; } internal AvaloniaView View; private GlobalLayoutListener _listener; @@ -82,6 +85,13 @@ namespace Avalonia.Android ActivityResult?.Invoke(requestCode, resultCode, data); } + public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults) + { + base.OnRequestPermissionsResult(requestCode, permissions, grantResults); + + RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults); + } + class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener { private AvaloniaView _view; diff --git a/src/Android/Avalonia.Android/IActivityResultHandler.cs b/src/Android/Avalonia.Android/IActivityResultHandler.cs index 14094ee185..40a8b5cbcf 100644 --- a/src/Android/Avalonia.Android/IActivityResultHandler.cs +++ b/src/Android/Avalonia.Android/IActivityResultHandler.cs @@ -1,11 +1,14 @@ using System; using Android.App; using Android.Content; +using Android.Content.PM; namespace Avalonia.Android { public interface IActivityResultHandler { public Action ActivityResult { get; set; } + + public Action RequestPermissionsResult { get; set; } } } diff --git a/src/Android/Avalonia.Android/InputEditable.cs b/src/Android/Avalonia.Android/InputEditable.cs new file mode 100644 index 0000000000..c5b68d2652 --- /dev/null +++ b/src/Android/Avalonia.Android/InputEditable.cs @@ -0,0 +1,127 @@ +using System; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.SkiaPlatform; +using Avalonia.Controls.Presenters; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Java.Lang; +using static System.Net.Mime.MediaTypeNames; + +namespace Avalonia.Android +{ + internal class InputEditable : SpannableStringBuilder, ITextEditable + { + private readonly TopLevelImpl _topLevel; + private readonly IAndroidInputMethod _inputMethod; + private readonly AvaloniaInputConnection _avaloniaInputConnection; + private int _currentBatchLevel; + private string _previousText; + private int _previousSelectionStart; + private int _previousSelectionEnd; + + public event EventHandler TextChanged; + public event EventHandler SelectionChanged; + public event EventHandler CompositionChanged; + + public InputEditable(TopLevelImpl topLevel, IAndroidInputMethod inputMethod, AvaloniaInputConnection avaloniaInputConnection) + { + _topLevel = topLevel; + _inputMethod = inputMethod; + _avaloniaInputConnection = avaloniaInputConnection; + } + + public InputEditable(ICharSequence text) : base(text) + { + } + + public InputEditable(string text) : base(text) + { + } + + public InputEditable(ICharSequence text, int start, int end) : base(text, start, end) + { + } + + public InputEditable(string text, int start, int end) : base(text, start, end) + { + } + + protected InputEditable(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public int SelectionStart + { + get => Selection.GetSelectionStart(this); set + { + var end = SelectionEnd < 0 ? 0 : SelectionEnd; + _avaloniaInputConnection.SetSelection(value, end); + _inputMethod.IMM.UpdateSelection(_topLevel.View, value, end, value, end); + } + } + public int SelectionEnd + { + get => Selection.GetSelectionEnd(this); set + { + var start = SelectionStart < 0 ? 0 : SelectionStart; + _avaloniaInputConnection.SetSelection(start, value); + _inputMethod.IMM.UpdateSelection(_topLevel.View, start, value, start, value); + } + } + + public string? Text + { + get => ToString(); set + { + if (Text != value) + { + Clear(); + Insert(0, value ?? ""); + } + } + } + + public int CompositionStart => BaseInputConnection.GetComposingSpanStart(this); + + public int CompositionEnd => BaseInputConnection.GetComposingSpanEnd(this); + + public void BeginBatchEdit() + { + _currentBatchLevel++; + + if (_currentBatchLevel == 1) + { + _previousText = ToString(); + _previousSelectionStart = SelectionStart; + _previousSelectionEnd = SelectionEnd; + } + } + + public void EndBatchEdit() + { + if (_currentBatchLevel == 1) + { + if(_previousText != Text) + { + TextChanged?.Invoke(this, EventArgs.Empty); + } + + if (_previousSelectionStart != SelectionStart || _previousSelectionEnd != SelectionEnd) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + } + + _currentBatchLevel--; + } + + public void RaiseCompositionChanged() + { + CompositionChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs index bb9dc66f5a..4ed91c248d 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs @@ -4,11 +4,11 @@ using Avalonia.Platform; namespace Avalonia.Android.Platform { - internal class AndroidSystemNavigationManager : ISystemNavigationManager + internal class AndroidSystemNavigationManagerImpl : ISystemNavigationManagerImpl { public event EventHandler BackRequested; - public AndroidSystemNavigationManager(IActivityNavigationService? navigationService) + public AndroidSystemNavigationManagerImpl(IActivityNavigationService? navigationService) { if(navigationService != null) { diff --git a/src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs b/src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs index 726ccdbbdd..ab84801e57 100644 --- a/src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs +++ b/src/Android/Avalonia.Android/Platform/Input/AndroidKeyboardDevice.cs @@ -5,7 +5,7 @@ using Avalonia.Input; namespace Avalonia.Android.Platform.Input { - public class AndroidKeyboardDevice : KeyboardDevice, IKeyboardDevice { + internal class AndroidKeyboardDevice : KeyboardDevice, IKeyboardDevice { private static readonly Dictionary KeyDic = new Dictionary { // { Keycode.Cancel?, Key.Cancel }, diff --git a/src/Android/Avalonia.Android/Platform/PlatformSupport.cs b/src/Android/Avalonia.Android/Platform/PlatformSupport.cs new file mode 100644 index 0000000000..9877f48664 --- /dev/null +++ b/src/Android/Avalonia.Android/Platform/PlatformSupport.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Android.App; +using Android.Content; +using Android.Content.PM; + +namespace Avalonia.Android.Platform; + +internal static class PlatformSupport +{ + private static int s_lastRequestCode = 20000; + + public static int GetNextRequestCode() => s_lastRequestCode++; + + public static async Task CheckPermission(this Activity activity, string permission) + { + if (activity is not IActivityResultHandler mainActivity) + { + throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface."); + } + + if (!OperatingSystem.IsAndroidVersionAtLeast(23)) + { + return true; + } + + if (activity.CheckSelfPermission(permission) == Permission.Granted) + { + return true; + } + + var currentRequestCode = GetNextRequestCode(); + var tcs = new TaskCompletionSource(); + mainActivity.RequestPermissionsResult += RequestPermissionsResult; + activity.RequestPermissions(new [] { permission }, currentRequestCode); + + return await tcs.Task; + + void RequestPermissionsResult(int requestCode, string[] arg2, Permission[] arg3) + { + if (currentRequestCode != requestCode) + { + return; + } + + mainActivity.RequestPermissionsResult -= RequestPermissionsResult; + + _ = tcs.TrySetResult(arg3.All(p => p == Permission.Granted)); + } + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index f205458f0e..47297a4f76 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -10,7 +10,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformNativeSurfaceHandle + internal abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformNativeSurfaceHandle { bool _invalidateQueued; readonly object _lock = new object(); diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 0e452b0bdd..e511ed9a8b 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -28,12 +28,11 @@ using Math = System.Math; using AndroidRect = Android.Graphics.Rect; using Window = Android.Views.Window; using Android.Graphics.Drawables; +using Java.Util; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, - ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider, - ITopLevelWithSystemNavigationManager + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -41,6 +40,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidInputMethod _textInputMethod; + private readonly INativeControlHostImpl _nativeControlHost; + private readonly IStorageProvider _storageProvider; + private readonly ISystemNavigationManagerImpl _systemNavigationManager; private ViewImpl _view; public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) @@ -57,10 +59,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); - NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); - StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); + _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); + _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); - SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService); + _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -109,16 +111,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => - AndroidPlatform.Options.UseCompositor - ? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces) - : AndroidPlatform.Options.UseDeferredRendering - ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), - () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), - AndroidPlatform.RenderInterface) - { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer((Visual)root, - () => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces), - AndroidPlatform.RenderInterface); + new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces); public virtual void Hide() { @@ -303,14 +296,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform public double Scaling => RenderScaling; - public ITextInputMethodImpl TextInputMethod => _textInputMethod; - - public INativeControlHostImpl NativeControlHost { get; } - - public IStorageProvider StorageProvider { get; } - - public ISystemNavigationManager SystemNavigationManager { get; } - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { if (TransparencyLevel != transparencyLevel) @@ -395,165 +380,104 @@ namespace Avalonia.Android.Platform.SkiaPlatform } } } + + public virtual object TryGetFeature(Type featureType) + { + if (featureType == typeof(IStorageProvider)) + { + return _storageProvider; + } + + if (featureType == typeof(ITextInputMethodImpl)) + { + return _textInputMethod; + } + + if (featureType == typeof(ISystemNavigationManagerImpl)) + { + return _systemNavigationManager; + } + + if (featureType == typeof(INativeControlHostImpl)) + { + return _nativeControlHost; + } + + return null; + } } internal class AvaloniaInputConnection : BaseInputConnection { private readonly TopLevelImpl _topLevel; private readonly IAndroidInputMethod _inputMethod; + private readonly InputEditable _editable; public AvaloniaInputConnection(TopLevelImpl topLevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true) { _topLevel = topLevel; _inputMethod = inputMethod; + _editable = new InputEditable(_topLevel, _inputMethod, this); } - public TextInputMethodSurroundingText SurroundingText { get; set; } - - public string ComposingText { get; internal set; } - - public ComposingRegion? ComposingRegion { get; internal set; } + public override IEditable Editable => _editable; - public bool IsComposing => !string.IsNullOrEmpty(ComposingText); - public bool IsCommiting { get; private set; } + internal InputEditable InputEditable => _editable; public override bool SetComposingRegion(int start, int end) { - //System.Diagnostics.Debug.WriteLine($"Composing Region: [{start}|{end}] {SurroundingText.Text?.Substring(start, end - start)}"); + var ret = base.SetComposingRegion(start, end); - ComposingRegion = new ComposingRegion(start, end); + InputEditable.RaiseCompositionChanged(); - return base.SetComposingRegion(start, end); + return ret; } public override bool SetComposingText(ICharSequence text, int newCursorPosition) { var composingText = text.ToString(); - ComposingText = composingText; - - _inputMethod.Client?.SetPreeditText(ComposingText); - - return base.SetComposingText(text, newCursorPosition); - } - - public override bool FinishComposingText() - { - if (!string.IsNullOrEmpty(ComposingText)) + if (string.IsNullOrEmpty(composingText)) { - CommitText(ComposingText, ComposingText.Length); + return CommitText(text, newCursorPosition); } else { - ComposingRegion = new ComposingRegion(SurroundingText.CursorOffset, SurroundingText.CursorOffset); - } - - return base.FinishComposingText(); - } + var ret = base.SetComposingText(text, newCursorPosition); - public override ICharSequence GetTextBeforeCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) - { - if (!string.IsNullOrEmpty(SurroundingText.Text) && length > 0) - { - var start = System.Math.Max(SurroundingText.CursorOffset - length, 0); + InputEditable.RaiseCompositionChanged(); - var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset); - - var text = SurroundingText.Text.Substring(start, end - start); - - //System.Diagnostics.Debug.WriteLine($"Text Before: {text}"); - - return new Java.Lang.String(text); + return ret; } - - return null; } - public override ICharSequence GetTextAfterCursorFormatted(int length, [GeneratedEnum] GetTextFlags flags) + public override bool BeginBatchEdit() { - if (!string.IsNullOrEmpty(SurroundingText.Text)) - { - var start = SurroundingText.CursorOffset; - - var end = System.Math.Min(start + length, SurroundingText.Text.Length); - - var text = SurroundingText.Text.Substring(start, end - start); - - //System.Diagnostics.Debug.WriteLine($"Text After: {text}"); + _editable.BeginBatchEdit(); - return new Java.Lang.String(text); - } - - return null; + return base.BeginBatchEdit(); } - public override bool CommitText(ICharSequence text, int newCursorPosition) + public override bool EndBatchEdit() { - IsCommiting = true; - var committedText = text.ToString(); - - _inputMethod.Client.SetPreeditText(null); + var ret = base.EndBatchEdit(); + _editable.EndBatchEdit(); - int? start, end; - - if(SurroundingText.CursorOffset != SurroundingText.AnchorOffset) - { - start = Math.Min(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - end = Math.Max(SurroundingText.CursorOffset, SurroundingText.AnchorOffset); - } - else if (ComposingRegion != null) - { - start = ComposingRegion?.Start; - end = ComposingRegion?.End; - - ComposingRegion = null; - } - else - { - start = end = _inputMethod.Client.SurroundingText.CursorOffset; - } - - _inputMethod.Client.SelectInSurroundingText((int)start, (int)end); - - var time = DateTime.Now.TimeOfDay; - - var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (ulong)time.Ticks, _topLevel.InputRoot, committedText); - - _topLevel.Input(rawTextEvent); - - ComposingText = null; - - ComposingRegion = new ComposingRegion(newCursorPosition, newCursorPosition); - - return base.CommitText(text, newCursorPosition); + return ret; } - public override bool DeleteSurroundingText(int beforeLength, int afterLength) + public override bool FinishComposingText() { - var surroundingText = _inputMethod.Client.SurroundingText; - - var selectionStart = surroundingText.CursorOffset; - - _inputMethod.Client.SelectInSurroundingText(selectionStart - beforeLength, selectionStart + afterLength); - - _inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel)); - - surroundingText = _inputMethod.Client.SurroundingText; - - selectionStart = surroundingText.CursorOffset; - - ComposingRegion = new ComposingRegion(selectionStart, selectionStart); - - return base.DeleteSurroundingText(beforeLength, afterLength); + var ret = base.FinishComposingText(); + InputEditable.RaiseCompositionChanged(); + return ret; } - public override bool SetSelection(int start, int end) + public override bool CommitText(ICharSequence text, int newCursorPosition) { - _inputMethod.Client.SelectInSurroundingText(start, end); - - ComposingRegion = new ComposingRegion(start, end); - - return base.SetSelection(start, end); + var ret = base.CommitText(text, newCursorPosition); + InputEditable.RaiseCompositionChanged(); + return ret; } public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 078f70db60..9d6dd46d0e 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -2,10 +2,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; +using Android; +using Android.App; using Android.Content; using Android.Provider; using Avalonia.Logging; @@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage; internal abstract class AndroidStorageItem : IStorageBookmarkItem { - private Context? _context; + private Activity? _activity; + private readonly bool _needsExternalFilesPermission; - protected AndroidStorageItem(Context context, AndroidUri uri) + protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) { - _context = context; + _activity = activity; + _needsExternalFilesPermission = needsExternalFilesPermission; Uri = uri; } internal AndroidUri Uri { get; } + + protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); - protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); - - public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName) + public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName) ?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; + public Uri Path => new(Uri.ToString()!); + public bool CanBookmark => true; - public Task SaveBookmarkAsync() + public async Task SaveBookmarkAsync() { - Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); - return Task.FromResult(Uri.ToString()); - } + if (!await EnsureExternalFilesPermission(false)) + { + return null; + } - public Task ReleaseBookmarkAsync() - { - Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); - return Task.CompletedTask; + Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); + return Uri.ToString(); } - public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + public async Task ReleaseBookmarkAsync() { - uri = new Uri(Uri.ToString()!); - return true; - } + if (!await EnsureExternalFilesPermission(false)) + { + return; + } + Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); + } + public abstract Task GetBasicPropertiesAsync(); protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) @@ -77,29 +85,44 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem return null; } - public Task GetParentAsync() + public async Task GetParentAsync() { + if (!await EnsureExternalFilesPermission(false)) + { + return null; + } + using var javaFile = new JavaFile(Uri.Path!); // Java file represents files AND directories. Don't be confused. if (javaFile.ParentFile is {} parentFile && AndroidUri.FromFile(parentFile) is {} androidUri) { - return Task.FromResult(new AndroidStorageFolder(Context, androidUri)); + return new AndroidStorageFolder(Activity, androidUri, false); } - return Task.FromResult(null); + return null; } + protected async Task EnsureExternalFilesPermission(bool write) + { + if (!_needsExternalFilesPermission) + { + return true; + } + + return await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage); + } + public void Dispose() { - _context = null; + _activity = null; } } -internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder +internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder { - public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri) + public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission) { } @@ -110,6 +133,11 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar public async Task> GetItemsAsync() { + if (!await EnsureExternalFilesPermission(false)) + { + return Array.Empty(); + } + using var javaFile = new JavaFile(Uri.Path!); // Java file represents files AND directories. Don't be confused. @@ -124,8 +152,8 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar .Where(t => t.uri is not null) .Select(t => t.file switch { - { IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), - { IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), + { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!), + { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false), _ => null }) .Where(i => i is not null) @@ -133,20 +161,27 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar } } -internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile +internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder { - public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri) + public WellKnownAndroidStorageFolder(Activity activity, string identifier, AndroidUri uri, bool needsExternalFilesPermission) + : base(activity, uri, needsExternalFilesPermission) { + Name = identifier; } - public bool CanOpenRead => true; + public override string Name { get; } +} - public bool CanOpenWrite => true; - - public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) +internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile +{ + public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) + { + } + + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); - public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) + public Task OpenWriteAsync() => Task.FromResult(OpenContentStream(Activity, Uri, true) ?? throw new InvalidOperationException("Failed to open content stream")); private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) @@ -210,7 +245,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, MediaStore.IMediaColumns.DateModified }; - using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null); + using var cursor = Activity.ContentResolver!.Query(Uri, projection, null, null, null); if (cursor?.MoveToFirst() == true) { diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index 62e43ff2ef..e35bde0acd 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -4,18 +4,21 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Android; using Android.App; using Android.Content; using Android.Provider; using Avalonia.Platform.Storage; +using Java.Lang; using AndroidUri = Android.Net.Uri; +using Exception = System.Exception; +using JavaFile = Java.IO.File; namespace Avalonia.Android.Platform.Storage; internal class AndroidStorageProvider : IStorageProvider { private readonly Activity _activity; - private int _lastRequestCode = 20000; public AndroidStorageProvider(Activity activity) { @@ -31,7 +34,108 @@ internal class AndroidStorageProvider : IStorageProvider public Task OpenFolderBookmarkAsync(string bookmark) { var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); - return Task.FromResult(new AndroidStorageFolder(_activity, uri)); + return Task.FromResult(new AndroidStorageFolder(_activity, uri, false)); + } + + public async Task TryGetFileFromPathAsync(Uri filePath) + { + if (filePath is null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (filePath is not { IsAbsoluteUri: true, Scheme: "file" or "content" }) + { + throw new ArgumentException("File path is expected to be an absolute link with \"file\" or \"content\" scheme."); + } + + var androidUri = AndroidUri.Parse(filePath.ToString()); + if (androidUri?.Path is not {} androidUriPath) + { + return null; + } + + var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage); + if (!hasPerms) + { + throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it."); + } + + var javaFile = new JavaFile(androidUriPath); + if (javaFile.Exists() && javaFile.IsFile) + { + return null; + } + + return new AndroidStorageFile(_activity, androidUri); + } + + public async Task TryGetFolderFromPathAsync(Uri folderPath) + { + if (folderPath is null) + { + throw new ArgumentNullException(nameof(folderPath)); + } + + if (folderPath is not { IsAbsoluteUri: true, Scheme: "file" or "content" }) + { + throw new ArgumentException("Folder path is expected to be an absolute link with \"file\" or \"content\" scheme."); + } + + var androidUri = AndroidUri.Parse(folderPath.ToString()); + if (androidUri?.Path is not {} androidUriPath) + { + return null; + } + + var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage); + if (!hasPerms) + { + throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it."); + } + + var javaFile = new JavaFile(androidUriPath); + if (javaFile.Exists() && javaFile.IsDirectory) + { + return null; + } + + return new AndroidStorageFolder(_activity, androidUri, false); + } + + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) + { + var dirCode = wellKnownFolder switch + { + WellKnownFolder.Desktop => null, + WellKnownFolder.Documents => global::Android.OS.Environment.DirectoryDocuments, + WellKnownFolder.Downloads => global::Android.OS.Environment.DirectoryDownloads, + WellKnownFolder.Music => global::Android.OS.Environment.DirectoryMusic, + WellKnownFolder.Pictures => global::Android.OS.Environment.DirectoryPictures, + WellKnownFolder.Videos => global::Android.OS.Environment.DirectoryMovies, + _ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null) + }; + if (dirCode is null) + { + return Task.FromResult(null); + } + + var dir = _activity.GetExternalFilesDir(dirCode); + if (dir is null || !dir.Exists()) + { + return Task.FromResult(null); + } + + var uri = AndroidUri.FromFile(dir); + if (uri is null) + { + return Task.FromResult(null); + } + + // To make TryGetWellKnownFolder API easier to use, we don't check for the permissions. + // It will work with file picker activities, but it will fail on any direct access to the folder, like getting list of children. + // We pass "needsExternalFilesPermission" parameter here, so folder itself can check for permissions on any FS access. + return Task.FromResult(new WellKnownAndroidStorageFolder(_activity, dirCode, uri, true)); } public Task OpenFileBookmarkAsync(string bookmark) @@ -110,19 +214,21 @@ internal class AndroidStorageProvider : IStorageProvider var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder"); var uris = await StartActivity(pickerIntent, false); - return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray(); + return uris.Select(u => new AndroidStorageFolder(_activity, u, false)).ToArray(); } private async Task> StartActivity(Intent? pickerIntent, bool singleResult) { var resultList = new List(1); var tcs = new TaskCompletionSource(); - var currentRequestCode = _lastRequestCode++; + var currentRequestCode = PlatformSupport.GetNextRequestCode(); - if (_activity is IActivityResultHandler mainActivity) + if (!(_activity is IActivityResultHandler mainActivity)) { - mainActivity.ActivityResult += OnActivityResult; + throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface."); } + + mainActivity.ActivityResult += OnActivityResult; _activity.StartActivityForResult(pickerIntent, currentRequestCode); var result = await tcs.Task; @@ -161,11 +267,7 @@ internal class AndroidStorageProvider : IStorageProvider return; } - - if (_activity is IActivityResultHandler mainActivity) - { - mainActivity.ActivityResult -= OnActivityResult; - } + mainActivity.ActivityResult -= OnActivityResult; _ = tcs.TrySetResult(resultCode == Result.Ok ? data : null); } diff --git a/src/Android/Avalonia.Android/PlatformIconLoader.cs b/src/Android/Avalonia.Android/PlatformIconLoader.cs index 88677a9375..f557685dd2 100644 --- a/src/Android/Avalonia.Android/PlatformIconLoader.cs +++ b/src/Android/Avalonia.Android/PlatformIconLoader.cs @@ -3,7 +3,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - class PlatformIconLoader : IPlatformIconLoader + internal class PlatformIconLoader : IPlatformIconLoader { public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) { @@ -29,7 +29,7 @@ namespace Avalonia.Android } // Stores the icon created as a stream to support saving even though an icon is never shown - public class FakeIcon : IWindowIconImpl + internal class FakeIcon : IWindowIconImpl { private Stream stream = new MemoryStream(); diff --git a/src/Android/Avalonia.Android/Stubs.cs b/src/Android/Avalonia.Android/Stubs.cs index f36c01dbc8..05638cdf88 100644 --- a/src/Android/Avalonia.Android/Stubs.cs +++ b/src/Android/Avalonia.Android/Stubs.cs @@ -4,7 +4,7 @@ using Avalonia.Platform; namespace Avalonia.Android { - class WindowingPlatformStub : IWindowingPlatform + internal class WindowingPlatformStub : IWindowingPlatform { public IWindowImpl CreateWindow() => throw new NotSupportedException(); @@ -13,7 +13,7 @@ namespace Avalonia.Android public ITrayIconImpl CreateTrayIcon() => null; } - class PlatformIconLoaderStub : IPlatformIconLoader + internal class PlatformIconLoaderStub : IPlatformIconLoader { public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) { @@ -38,7 +38,7 @@ namespace Avalonia.Android } } - public class IconStub : IWindowIconImpl + internal class IconStub : IWindowIconImpl { private readonly MemoryStream _ms; diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index edaa76233e..5208c8b218 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -27,7 +27,11 @@ namespace Avalonia.Animation AvaloniaProperty.Register(nameof(Transitions)); private bool _transitionsEnabled = true; + private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; + private NotifyCollectionChangedEventHandler? _collectionChanged; + private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => + _collectionChanged ??= TransitionsCollectionChanged; /// /// Gets or sets the clock which controls the animations on the control. @@ -60,9 +64,14 @@ namespace Avalonia.Animation { _transitionsEnabled = true; - if (Transitions is object) + if (Transitions is Transitions transitions) { - AddTransitions(Transitions); + if (!_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = true; + transitions.CollectionChanged += TransitionsCollectionChangedHandler; + } + AddTransitions(transitions); } } } @@ -72,7 +81,7 @@ namespace Avalonia.Animation /// /// /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. + /// when a control is removed from the visual tree. /// protected void DisableTransitions() { @@ -80,9 +89,14 @@ namespace Avalonia.Animation { _transitionsEnabled = false; - if (Transitions is object) + if (Transitions is Transitions transitions) { - RemoveTransitions(Transitions); + if (_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = false; + transitions.CollectionChanged -= TransitionsCollectionChangedHandler; + } + RemoveTransitions(transitions); } } } @@ -109,7 +123,8 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += TransitionsCollectionChanged; + newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; + _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -122,19 +137,19 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler; RemoveTransitions(toRemove); } } else if (_transitionsEnabled && - Transitions is object && + Transitions is Transitions transitions && _transitionState is object && !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - for (var i = Transitions.Count -1; i >= 0; --i) + for (var i = transitions.Count - 1; i >= 0; --i) { - var transition = Transitions[i]; + var transition = transitions[i]; if (transition.Property == change.Property && _transitionState.TryGetValue(transition, out var state)) @@ -154,11 +169,11 @@ namespace Avalonia.Animation { oldValue = animatedValue; } - + var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService(); state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetRequiredService(), + clock, oldValue, newValue); return; diff --git a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs index 068c190fa1..801f247754 100644 --- a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Avalonia.Data; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Media.Transformation; #nullable enable @@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators return new ImmutableRadialGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), @@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators return new ImmutableConicGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), @@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators return new ImmutableLinearGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), @@ -72,6 +73,25 @@ namespace Avalonia.Animation.Animators return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } + private static ImmutableTransform? InterpolateTransform(double progress, + ITransform? oldTransform, ITransform? newTransform) + { + if (oldTransform is TransformOperations oldTransformOperations + && newTransform is TransformOperations newTransformOperations) + { + + return new ImmutableTransform(TransformOperations + .Interpolate(oldTransformOperations, newTransformOperations, progress).Value); + } + + if (oldTransform is not null) + { + return new ImmutableTransform(oldTransform.Value); + } + + return null; + } + private static IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) { var resultCount = Math.Max(oldValue.Count, newValue.Count); diff --git a/src/Avalonia.Base/Animation/KeySpline.cs b/src/Avalonia.Base/Animation/KeySpline.cs index 6ca5b2e759..ed6adb79b8 100644 --- a/src/Avalonia.Base/Animation/KeySpline.cs +++ b/src/Avalonia.Base/Animation/KeySpline.cs @@ -79,15 +79,12 @@ namespace Avalonia.Animation /// culture of the string /// Thrown if the string does not have 4 values /// A with the appropriate values set - public static KeySpline Parse(string value, CultureInfo culture) + public static KeySpline Parse(string value, CultureInfo? culture) { - if (culture is null) - culture = CultureInfo.InvariantCulture; + culture ??= CultureInfo.InvariantCulture; - using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) - { - return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); - } + using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."); + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } /// diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index a43194153c..31b6cad8ab 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -24,11 +24,9 @@ namespace Avalonia Func? validate = null) : base(name, ownerType, metadata, inherits, validate) { + IsAttached = true; } - /// - public override bool IsAttached => true; - /// /// Attaches the property as a non-attached property on the specified type. /// diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index cd122a8b67..639c27bf03 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -30,6 +30,7 @@ + @@ -47,6 +48,9 @@ + + + @@ -66,8 +70,4 @@ - - - - diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index dc94dfba40..d89d6f3690 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -118,7 +118,7 @@ namespace Avalonia { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values.ClearLocalValue(property); + _values.ClearValue(property); } /// @@ -132,7 +132,7 @@ namespace Avalonia switch (property) { - case StyledPropertyBase styled: + case StyledProperty styled: ClearValue(styled); break; case DirectPropertyBase direct: @@ -147,12 +147,12 @@ namespace Avalonia /// Clears a 's local value. /// /// The property. - public void ClearValue(StyledPropertyBase property) + public void ClearValue(StyledProperty property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); - _values?.ClearLocalValue(property); + _values.ClearValue(property); } /// @@ -220,7 +220,7 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - public T GetValue(StyledPropertyBase property) + public T GetValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); @@ -242,8 +242,15 @@ namespace Avalonia return registered.InvokeGetter(this); } - /// - public Optional GetBaseValue(StyledPropertyBase property) + /// + /// Gets an base value. + /// + /// The property. + /// + /// Gets the value of the property excluding animated values, otherwise . + /// Note that this method does not return property values that come from inherited or default values. + /// + public Optional GetBaseValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); @@ -261,7 +268,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsAnimating(property) ?? false; + return _values.IsAnimating(property); } /// @@ -270,8 +277,8 @@ namespace Avalonia /// The property. /// True if the property is set, otherwise false. /// - /// Checks whether a value is assigned to the property, or that there is a binding to the - /// property that is producing a value other than . + /// Returns true if is a styled property which has a value + /// assigned to it or a binding targeting it; otherwise false. /// public bool IsSet(AvaloniaProperty property) { @@ -279,7 +286,7 @@ namespace Avalonia VerifyAccess(); - return _values?.IsSet(property) ?? false; + return _values.IsSet(property); } /// @@ -309,7 +316,7 @@ namespace Avalonia /// An if setting the property can be undone, otherwise null. /// public IDisposable? SetValue( - StyledPropertyBase property, + StyledProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { @@ -322,7 +329,7 @@ namespace Avalonia if (value is UnsetValueType) { if (priority == BindingPriority.LocalValue) - _values.ClearLocalValue(property); + _values.ClearValue(property); } else if (value is not DoNothingType) { @@ -348,6 +355,57 @@ namespace Avalonia SetDirectValueUnchecked(property, value); } + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// + public void SetCurrentValue(AvaloniaProperty property, object? value) => + property.RouteSetCurrentValue(this, value); + + /// + /// Sets the value of a dependency property without changing its value source. + /// + /// The type of the property. + /// The property. + /// The value. + /// + /// This method is used by a component that programmatically sets the value of one of its + /// own properties without disabling an application's declared use of the property. The + /// method changes the effective value of the property, but existing data bindings and + /// styles will continue to work. + /// + /// The new value will have the property's current , even if + /// that priority is or + /// . + /// + public void SetCurrentValue(StyledProperty property, T value) + { + _ = property ?? throw new ArgumentNullException(nameof(property)); + VerifyAccess(); + + LogPropertySet(property, value, BindingPriority.LocalValue); + + if (value is UnsetValueType) + { + _values.ClearValue(property); + } + else if (value is not DoNothingType) + { + _values.SetCurrentValue(property, value); + } + } + /// /// Binds a to an observable. /// @@ -373,7 +431,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { @@ -396,7 +454,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { @@ -419,7 +477,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { @@ -515,14 +573,12 @@ namespace Avalonia /// The property. public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); - /// internal void AddInheritanceChild(AvaloniaObject child) { _inheritanceChildren ??= new List(); _inheritanceChildren.Add(child); } - - /// + internal void RemoveInheritanceChild(AvaloniaObject child) { _inheritanceChildren?.Remove(child); @@ -541,24 +597,12 @@ namespace Avalonia return new AvaloniaPropertyValue( property, GetValue(property), - BindingPriority.Unset, - "Local Value"); - } - else if (_values != null) - { - var result = _values.GetDiagnostic(property); - - if (result != null) - { - return result; - } + BindingPriority.LocalValue, + null, + false); } - return new AvaloniaPropertyValue( - property, - GetValue(property), - BindingPriority.Unset, - "Unset"); + return _values.GetDiagnostic(property); } internal ValueStore GetValueStore() => _values; @@ -620,14 +664,12 @@ namespace Avalonia /// The property that has changed. /// The old property value. /// The new property value. - /// The priority of the binding that produced the value. protected void RaisePropertyChanged( DirectPropertyBase property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority = BindingPriority.LocalValue) + T oldValue, + T newValue) { - RaisePropertyChanged(property, oldValue, newValue, priority, true); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true); } /// @@ -676,7 +718,7 @@ namespace Avalonia /// /// True if the value changed, otherwise false. /// - protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) + protected bool SetAndRaise(DirectPropertyBase property, ref T field, T value) { VerifyAccess(); diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 7b17b9152d..6231483ff8 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -146,7 +146,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.Bind(styled, source, priority), + StyledProperty styled => target.Bind(styled, source, priority), DirectPropertyBase direct => target.Bind(direct, source), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), }; @@ -170,7 +170,7 @@ namespace Avalonia { return property switch { - StyledPropertyBase styled => target.Bind(styled, source, priority), + StyledProperty styled => target.Bind(styled, source, priority), DirectPropertyBase direct => target.Bind(direct, source), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), }; @@ -231,7 +231,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.GetValue(styled), + StyledProperty styled => target.GetValue(styled), DirectPropertyBase direct => target.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; @@ -280,7 +280,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.GetBaseValue(styled), + StyledProperty styled => target.GetBaseValue(styled), DirectPropertyBase direct => target.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index e0782c51a2..45ab293a89 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -107,22 +107,22 @@ namespace Avalonia /// /// Gets a value indicating whether the property inherits its value. /// - public virtual bool Inherits => false; + public bool Inherits { get; private protected set; } /// /// Gets a value indicating whether this is an attached property. /// - public virtual bool IsAttached => false; + public bool IsAttached { get; private protected set; } /// /// Gets a value indicating whether this is a direct property. /// - public virtual bool IsDirect => false; + public bool IsDirect { get; private protected set; } /// /// Gets a value indicating whether this is a readonly property. /// - public virtual bool IsReadOnly => false; + public bool IsReadOnly { get; private protected set; } /// /// Gets an observable that is fired when this property changes on any @@ -225,13 +225,8 @@ namespace Avalonia /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. - /// A value validation callback. + /// A value validation callback. /// A value coercion callback. - /// - /// A method that gets called before and after the property starts being notified on an - /// object; the bool argument will be true before and false afterwards. This callback is - /// intended to support IsDataContextChanging. - /// /// A public static StyledProperty Register( string name, @@ -239,8 +234,40 @@ namespace Avalonia bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, - Func? coerce = null, - Action? notifying = null) + Func? coerce = null) + where TOwner : AvaloniaObject + { + _ = name ?? throw new ArgumentNullException(nameof(name)); + + var metadata = new StyledPropertyMetadata( + defaultValue, + defaultBindingMode: defaultBindingMode, + coerce: coerce); + + var result = new StyledProperty( + name, + typeof(TOwner), + metadata, + inherits, + validate); + AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); + return result; + } + + /// + /// + /// A method that gets called before and after the property starts being notified on an + /// object; the bool argument will be true before and false afterwards. This callback is + /// intended to support IsDataContextChanging. + /// + internal static StyledProperty Register( + string name, + TValue defaultValue, + bool inherits, + BindingMode defaultBindingMode, + Func? validate, + Func? coerce, + Action? notifying) where TOwner : AvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); @@ -496,6 +523,13 @@ namespace Avalonia object? value, BindingPriority priority); + /// + /// Routes an untyped SetCurrentValue call to a typed call. + /// + /// The object instance. + /// The value. + internal abstract void RouteSetCurrentValue(AvaloniaObject o, object? value); + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 35a391f2cb..d4c7137fdc 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -14,11 +14,7 @@ namespace Avalonia.Collections /// /// The type of the dictionary key. /// The type of the dictionary value. - public class AvaloniaDictionary : IDictionary, - IDictionary, - INotifyCollectionChanged, - INotifyPropertyChanged - where TKey : notnull + public class AvaloniaDictionary : IAvaloniaDictionary where TKey : notnull { private Dictionary _inner; @@ -29,6 +25,14 @@ namespace Avalonia.Collections { _inner = new Dictionary(); } + + /// + /// Initializes a new instance of the class. + /// + public AvaloniaDictionary(int capacity) + { + _inner = new Dictionary(capacity); + } /// /// Occurs when the collection changes. @@ -62,6 +66,10 @@ namespace Avalonia.Collections object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; + IEnumerable IReadOnlyDictionary.Keys => _inner.Keys; + + IEnumerable IReadOnlyDictionary.Values => _inner.Values; + /// /// Gets or sets the named resource. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs new file mode 100644 index 0000000000..e350a019d4 --- /dev/null +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Reactive; + +namespace Avalonia.Collections +{ + /// + /// Defines extension methods for working with s. + /// + public static class AvaloniaDictionaryExtensions + { + /// + /// Invokes an action for each item in a collection and subsequently each item added or + /// removed from the collection. + /// + /// The key type of the collection items. + /// The value type of the collection items. + /// The collection. + /// + /// An action called initially for each item in the collection and subsequently for each + /// item added to the collection. The parameters passed are the index in the collection and + /// the item. + /// + /// + /// An action called for each item removed from the collection. The parameters passed are + /// the index in the collection and the item. + /// + /// + /// An action called when the collection is reset. This will be followed by calls to + /// for each item present in the collection after the reset. + /// + /// + /// Indicates if a weak subscription should be used to track changes to the collection. + /// + /// A disposable used to terminate the subscription. + internal static IDisposable ForEachItem( + this IAvaloniaReadOnlyDictionary collection, + Action added, + Action removed, + Action reset, + bool weakSubscription = false) + where TKey : notnull + { + void Add(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + added(pair.Key, pair.Value); + } + } + + void Remove(IEnumerable items) + { + foreach (KeyValuePair pair in items) + { + removed(pair.Key, pair.Value); + } + } + + NotifyCollectionChangedEventHandler handler = (_, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + Remove(e.OldItems!); + int newIndex = e.NewStartingIndex; + if(newIndex > e.OldStartingIndex) + { + newIndex -= e.OldItems!.Count; + } + Add(e.NewItems!); + break; + + case NotifyCollectionChangedAction.Remove: + Remove(e.OldItems!); + break; + + case NotifyCollectionChangedAction.Reset: + if (reset == null) + { + throw new InvalidOperationException( + "Reset called on collection without reset handler."); + } + + reset(); + Add(collection); + break; + } + }; + + Add(collection); + + if (weakSubscription) + { + return collection.WeakSubscribe(handler); + } + else + { + collection.CollectionChanged += handler; + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } + } + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs new file mode 100644 index 0000000000..b79cfe2b9c --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaDictionary.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Collections +{ + public interface IAvaloniaDictionary + : IDictionary, + IAvaloniaReadOnlyDictionary, + IDictionary + where TKey : notnull + { + } +} diff --git a/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs new file mode 100644 index 0000000000..d772de2f59 --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace Avalonia.Collections +{ + public interface IAvaloniaReadOnlyDictionary + : IReadOnlyDictionary, + INotifyCollectionChanged, + INotifyPropertyChanged + where TKey : notnull + { + } +} diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 3a68dde31e..2bd1f65638 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Styling; #nullable enable @@ -13,5 +14,10 @@ namespace Avalonia.Controls /// Gets a collection of child resource dictionaries. /// IList MergedDictionaries { get; } + + /// + /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. + /// + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IResourceNode.cs b/src/Avalonia.Base/Controls/IResourceNode.cs index d6c900f97f..d2fa3c7af3 100644 --- a/src/Avalonia.Base/Controls/IResourceNode.cs +++ b/src/Avalonia.Base/Controls/IResourceNode.cs @@ -1,5 +1,5 @@ -using System; -using Avalonia.Metadata; +using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -23,6 +23,7 @@ namespace Avalonia.Controls /// Tries to find a resource within the object. /// /// The resource key. + /// Theme used to select theme dictionary. /// /// When this method returns, contains the value associated with the specified key, /// if the key is found; otherwise, null. @@ -30,6 +31,6 @@ namespace Avalonia.Controls /// /// True if the resource if found, otherwise false. /// - bool TryGetResource(object key, out object? value); + bool TryGetResource(object key, ThemeVariant? theme, out object? value); } } diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index d6197c50c6..5123803f6e 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -1,9 +1,12 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Templates; +using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -15,6 +18,7 @@ namespace Avalonia.Controls private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -69,14 +73,14 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { - if (Owner is object) + if (Owner is not null) { x.AddOwner(Owner); } }, x => { - if (Owner is object) + if (Owner is not null) { x.RemoveOwner(Owner); } @@ -88,6 +92,34 @@ namespace Avalonia.Controls } } + public IDictionary ThemeDictionaries + { + get + { + if (_themeDictionary == null) + { + _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.AddOwner(Owner); + } + }, + (_, x) => + { + if (Owner is not null) + { + x.RemoveOwner(Owner); + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + return _themeDictionary; + } + } + bool IResourceNode.HasResources { get @@ -152,16 +184,47 @@ namespace Avalonia.Controls return false; } - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { if (TryGetValue(key, out value)) return true; + if (_themeDictionary is not null) + { + IResourceProvider? themeResourceProvider; + if (theme is not null && theme != ThemeVariant.Default) + { + if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + var themeInherit = theme.InheritVariant; + while (themeInherit is not null) + { + if (_themeDictionary.TryGetValue(themeInherit, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + + themeInherit = themeInherit.InheritVariant; + } + } + + if (_themeDictionary.TryGetValue(ThemeVariant.Default, out themeResourceProvider) + && themeResourceProvider.TryGetResource(key, theme, out value)) + { + return true; + } + } + if (_mergedDictionaries != null) { for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) { - if (_mergedDictionaries[i].TryGetResource(key, out value)) + if (_mergedDictionaries[i].TryGetResource(key, theme, out value)) { return true; } @@ -248,7 +311,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -256,6 +319,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.AddOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { @@ -273,7 +344,7 @@ namespace Avalonia.Controls var hasResources = _inner?.Count > 0; - if (_mergedDictionaries is object) + if (_mergedDictionaries is not null) { foreach (var i in _mergedDictionaries) { @@ -281,6 +352,14 @@ namespace Avalonia.Controls hasResources |= i.HasResources; } } + if (_themeDictionary is not null) + { + foreach (var i in _themeDictionary.Values) + { + i.RemoveOwner(owner); + hasResources |= i.HasResources; + } + } if (hasResources) { diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 0bf1073098..8aed1545a5 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Data.Converters; -using Avalonia.LogicalTree; using Avalonia.Reactive; using Avalonia.Styling; @@ -41,21 +39,66 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceNode? current = control; + return control.TryFindResource(key, null, out value); + } + + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// Theme used to select theme dictionary. + /// The resource key. + /// The resource, or if not found. + public static object? FindResource(this IResourceHost control, ThemeVariant? theme, object key) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + if (control.TryFindResource(key, theme, out var value)) + { + return value; + } + + return AvaloniaProperty.UnsetValue; + } + + /// + /// Tries to the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// Theme used to select theme dictionary. + /// On return, contains the resource if found, otherwise null. + /// True if the resource was found; otherwise false. + public static bool TryFindResource(this IResourceHost control, object key, ThemeVariant? theme, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + IResourceHost? current = control; while (current != null) { - if (current.TryGetResource(key, out value)) + if (current.TryGetResource(key, theme, out value)) { return true; } - current = (current as IStyleHost)?.StylingParent as IResourceNode; + current = (current as IStyleHost)?.StylingParent as IResourceHost; } value = null; return false; } + + /// + public static bool TryGetResource(this IResourceHost control, object key, out object? value) + { + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return control.TryGetResource(key, null, out value); + } public static IObservable GetResourceObservable( this IResourceHost control, @@ -95,24 +138,46 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; + if (_target is IThemeVariantHost themeVariantHost) + { + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; + } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; + if (_target is IThemeVariantHost themeVariantHost) + { + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; + } } protected override void Subscribed(IObserver observer, bool first) { - observer.OnNext(Convert(_target.FindResource(_key))); + observer.OnNext(GetValue()); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { - PublishNext(Convert(_target.FindResource(_key))); + PublishNext(GetValue()); } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private void ActualThemeVariantChanged(object? sender, EventArgs e) + { + PublishNext(GetValue()); + } + + private object? GetValue() + { + if (_target is not IThemeVariantHost themeVariantHost + || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) + { + value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } private class FloatingResourceObservable : LightweightObservableBase @@ -134,7 +199,7 @@ namespace Avalonia.Controls _target.OwnerChanged += OwnerChanged; _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } @@ -148,43 +213,66 @@ namespace Avalonia.Controls protected override void Subscribed(IObserver observer, bool first) { - if (_target.Owner is object) + if (_target.Owner is not null) { - observer.OnNext(Convert(_target.Owner.FindResource(_key))); + observer.OnNext(GetValue()); } } private void PublishNext() { - if (_target.Owner is object) + if (_target.Owner is not null) { - PublishNext(Convert(_target.Owner.FindResource(_key))); + PublishNext(GetValue()); } } private void OwnerChanged(object? sender, EventArgs e) { - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged -= ResourcesChanged; } + if (_owner is IThemeVariantHost themeVariantHost) + { + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; + } _owner = _target.Owner; - if (_owner is object) + if (_owner is not null) { _owner.ResourcesChanged += ResourcesChanged; } + if (_owner is IThemeVariantHost themeVariantHost2) + { + themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; + } + PublishNext(); } + private void ActualThemeVariantChanged(object? sender, EventArgs e) + { + PublishNext(); + } + private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) { PublishNext(); } - private object? Convert(object? value) => _converter?.Invoke(value) ?? value; + private object? GetValue() + { + if (!(_target.Owner is IThemeVariantHost themeVariantHost) + || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) + { + value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; + } + + return _converter?.Invoke(value) ?? value; + } } } } diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 0b737dd959..e53ae40cb1 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -41,54 +41,41 @@ namespace Avalonia.Data { case BindingMode.Default: case BindingMode.OneWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - return target.Bind(property, binding.Observable, binding.Priority); + return target.Bind(property, binding.Source, binding.Priority); case BindingMode.TwoWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + { + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); return new TwoWayBindingDisposable( - target.Bind(property, binding.Observable, binding.Priority), - target.GetObservable(property).Subscribe(binding.Subject)); + target.Bind(property, binding.Source, binding.Priority), + target.GetObservable(property).Subscribe(observer)); + } case BindingMode.OneTime: - if (binding.Observable is {} source) - { - // Perf: Avoid allocating closure in the outer scope. - var targetCopy = target; - var propertyCopy = property; - var bindingCopy = binding; - - return source - .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) - .Take(1) - .Subscribe(x => targetCopy.SetValue( - propertyCopy, - BindingNotification.ExtractValue(x), - bindingCopy.Priority)); - } - else - { - target.SetValue(property, binding.Value, binding.Priority); - return Disposable.Empty; - } + { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + + return binding.Source + .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) + .Take(1) + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); + } case BindingMode.OneWayToSource: { - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); - // Perf: Avoid allocating closure in the outer scope. - var bindingCopy = binding; - return Observable.CombineLatest( - binding.Observable, + binding.Source, target.GetObservable(property), (_, v) => v) - .Subscribe(x => bindingCopy.Subject.OnNext(x)); + .Subscribe(x => observer.OnNext(x)); } default: diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index f5c135459d..aeb71d16ae 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -30,7 +30,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; + return null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 536c14dcf9..92fc843394 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -3,7 +3,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { - public class AvaloniaPropertyAccessorNode : SettableNode + internal class AvaloniaPropertyAccessorNode : SettableNode { private IDisposable? _subscription; private readonly bool _enableValidation; diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 55caf8070e..79942cb9ce 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core /// that are sent and received. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription + internal class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 4e142fbee9..b333fa9047 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,6 +1,6 @@ namespace Avalonia.Data.Core { - public class EmptyExpressionNode : ExpressionNode + internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 5fb2bb5c13..30fc71cfb4 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Data.Core { - public abstract class ExpressionNode + internal abstract class ExpressionNode { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0818b5fa62..ce3549c4ad 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, IDescription + internal class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 2fad96701d..57e4fa4a8e 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : SettableNode, + internal abstract class IndexerNodeBase : SettableNode, IWeakEventSubscriber, IWeakEventSubscriber { @@ -22,7 +22,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } ValueChanged(GetValue(target)); @@ -39,7 +39,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 45837db73d..81b07bfe85 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -3,7 +3,7 @@ using System.Globalization; namespace Avalonia.Data.Core { - public class LogicalNotNode : ExpressionNode, ITransformNode + internal class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 34f8e568d4..f111d8917b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -8,7 +8,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Reads a property from a . /// - public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs new file mode 100644 index 0000000000..6d88d55774 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Avalonia.Data.Core.Plugins +{ + /// + /// Holds a registry of plugins used for bindings. + /// + public static class BindingPlugins + { + /// + /// An ordered collection of property accessor plugins that can be used to customize + /// the reading and subscription of property values on a type. + /// + public static IList PropertyAccessors => ExpressionObserver.PropertyAccessors; + + /// + /// An ordered collection of validation checker plugins that can be used to customize + /// the validation of view model and model data. + /// + public static IList DataValidators => ExpressionObserver.DataValidators; + + /// + /// An ordered collection of stream plugins that can be used to customize the behavior + /// of the '^' stream binding operator. + /// + public static IList StreamHandlers => ExpressionObserver.StreamHandlers; + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index bc300386b9..ba5f59ea23 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on that have s. /// - public class DataAnnotationsValidationPlugin : IDataValidationPlugin + internal class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 2bb8da2c74..e60a341309 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IDataValidationPlugin + internal class ExceptionValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 87a2f67ee8..3384a99333 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IDataValidationPlugin + internal class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 5b19e995cc..e8e3e6d509 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins /// Reads a property from a standard C# object that optionally supports the /// interface. /// - public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup = new Dictionary<(Type, string), PropertyInfo?>(); @@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 2397ce483d..8170edd653 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -6,7 +6,7 @@ using System.Reflection; namespace Avalonia.Data.Core.Plugins { - public class MethodAccessorPlugin : IPropertyAccessorPlugin + internal class MethodAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = new Dictionary<(Type, string), MethodInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index b40628fd35..9cf25281f2 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -10,12 +10,12 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class ObservableStreamPlugin : IStreamPlugin + internal class ObservableStreamPlugin : IStreamPlugin { private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableSelect; - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")] + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")] public ObservableStreamPlugin() { diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 715f4604cf..42a050778e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class TaskStreamPlugin : IStreamPlugin + internal class TaskStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 1b79fed6e7..3898d232ec 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class PropertyAccessorNode : SettableNode + internal class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessorPlugin? _customPlugin; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 9ad9ace814..4980e4487a 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Data.Core { - public abstract class SettableNode : ExpressionNode + internal abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object? value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index ba18a2173b..d3da6414ac 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class StreamNode : ExpressionNode + internal class StreamNode : ExpressionNode { private IStreamPlugin? _customPlugin = null; private IDisposable? _subscription; diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs index 3a2ca955fa..655bfbc7a1 100644 --- a/src/Avalonia.Base/Data/Core/TypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs @@ -4,7 +4,7 @@ using System.Text; namespace Avalonia.Data.Core { - public class TypeCastNode : ExpressionNode + internal class TypeCastNode : ExpressionNode { public override string Description => $"as {TargetType.FullName}"; diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index a60c1d72ec..c09c31632e 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Reactive; +using ObservableEx = Avalonia.Reactive.Observable; namespace Avalonia.Data { @@ -14,11 +15,23 @@ namespace Avalonia.Data /// public class InstancedBinding { - internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority) + /// + /// Initializes a new instance of the class. + /// + /// The binding source. + /// The binding mode. + /// The priority of the binding. + /// + /// This constructor can be used to create any type of binding and as such requires an + /// as the binding source because this is the only binding + /// source which can be used for all binding modes. If you wish to create an instance with + /// something other than a subject, use one of the static creation methods on this class. + /// + internal InstancedBinding(IObservable source, BindingMode mode, BindingPriority priority) { Mode = mode; Priority = priority; - Value = value; + Source = source ?? throw new ArgumentNullException(nameof(source)); } /// @@ -32,24 +45,12 @@ namespace Avalonia.Data public BindingPriority Priority { get; } /// - /// Gets the value or source of the binding. - /// - public object? Value { get; } - - /// - /// Gets the as an observable. + /// Gets the binding source observable. /// - public IObservable? Observable => Value as IObservable; + public IObservable Source { get; } - /// - /// Gets the as an observer. - /// - public IObserver? Observer => Value as IObserver; - - /// - /// Gets the as an subject. - /// - internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; + [Obsolete("Use Source property")] + public IObservable Observable => Source; /// /// Creates a new one-time binding with a fixed value. @@ -61,7 +62,7 @@ namespace Avalonia.Data object value, BindingPriority priority = BindingPriority.LocalValue) { - return new InstancedBinding(value, BindingMode.OneTime, priority); + return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority); } /// @@ -106,7 +107,7 @@ namespace Avalonia.Data { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); + return new InstancedBinding((IObservable)observer, BindingMode.OneWayToSource, priority); } /// @@ -135,7 +136,7 @@ namespace Avalonia.Data /// An instance. public InstancedBinding WithPriority(BindingPriority priority) { - return new InstancedBinding(Value, Mode, priority); + return new InstancedBinding(Source, Mode, priority); } } } diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index d7b1f2e053..270cac95f2 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs @@ -1,6 +1,3 @@ -using System; -using Avalonia.Data; - namespace Avalonia.Diagnostics { /// diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs index 4189fd5234..0b3e62f1cc 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs +++ b/src/Avalonia.Base/Diagnostics/AvaloniaPropertyValue.cs @@ -3,28 +3,23 @@ using Avalonia.Data; namespace Avalonia.Diagnostics { /// - /// Holds diagnostic-related information about the value of a - /// on a . + /// Holds diagnostic-related information about the value of an + /// on an . /// public class AvaloniaPropertyValue { - /// - /// Initializes a new instance of the class. - /// - /// The property. - /// The current property value. - /// The priority of the current value. - /// A diagnostic string. - public AvaloniaPropertyValue( + internal AvaloniaPropertyValue( AvaloniaProperty property, object? value, BindingPriority priority, - string? diagnostic) + string? diagnostic, + bool isOverriddenCurrentValue) { Property = property; Value = value; Priority = priority; Diagnostic = diagnostic; + IsOverriddenCurrentValue = isOverriddenCurrentValue; } /// @@ -46,5 +41,11 @@ namespace Avalonia.Diagnostics /// Gets a diagnostic string. /// public string? Diagnostic { get; } + + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverriddenCurrentValue { get; } } } diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 729240e5a1..d02e277074 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -33,6 +33,8 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; + IsDirect = true; + IsReadOnly = setter is null; } /// @@ -51,17 +53,10 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; + IsDirect = true; + IsReadOnly = setter is null; } - /// - public override bool IsDirect => true; - - /// - public override bool IsReadOnly => Setter == null; - - /// - public override Type Owner => typeof(TOwner); - /// /// Gets the getter function. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index ec9eba6d61..94dfaaab01 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,8 +1,6 @@ using System; using Avalonia.Data; using Avalonia.PropertyStore; -using Avalonia.Reactive; -using Avalonia.Styling; namespace Avalonia { @@ -28,6 +26,7 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(name, ownerType, metadata) { + Owner = ownerType; } /// @@ -42,12 +41,13 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(source, ownerType, metadata) { + Owner = ownerType; } /// /// Gets the type that registered the property. /// - public abstract Type Owner { get; } + public Type Owner { get; } /// /// Gets the value of the property on the instance. @@ -152,6 +152,11 @@ namespace Avalonia return null; } + internal override void RouteSetCurrentValue(AvaloniaObject o, object? value) + { + RouteSetValue(o, value, BindingPriority.LocalValue); + } + /// /// Routes an untyped Bind call to a typed call. /// diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 7a27c53023..8d7cc2b9a1 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -1,38 +1,33 @@ using System; using Avalonia.Interactivity; -using Avalonia.VisualTree; +using Avalonia.Metadata; namespace Avalonia.Input { public class DragEventArgs : RoutedEventArgs { - private Interactive _target; - private Point _targetLocation; + private readonly Interactive _target; + private readonly Point _targetLocation; public DragDropEffects DragEffects { get; set; } - public IDataObject Data { get; private set; } + public IDataObject Data { get; } - public KeyModifiers KeyModifiers { get; private set; } + public KeyModifiers KeyModifiers { get; } public Point GetPosition(Visual relativeTo) { - var point = new Point(0, 0); - if (relativeTo == null) { throw new ArgumentNullException(nameof(relativeTo)); } - if (_target != null) - { - point = _target.TranslatePoint(_targetLocation, relativeTo) ?? point; - } - - return point; + return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0); } - internal DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + [Unstable] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) : base(routedEvent) { Data = data; diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs index eea7c3b7d1..3b83d0cb87 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs @@ -57,7 +57,10 @@ namespace Avalonia.Input var scale = distance / _initialDistance; - _target?.RaiseEvent(new PinchEventArgs(scale, _origin)); + var pinchEventArgs = new PinchEventArgs(scale, _origin); + _target?.RaiseEvent(pinchEventArgs); + + e.Handled = pinchEventArgs.Handled; } } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 23bab13fc8..991694cc60 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Input.GestureRecognizers; namespace Avalonia.Input @@ -88,7 +89,10 @@ namespace Avalonia.Input } _pullInProgress = true; - _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); + var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection); + _target?.RaiseEvent(pullEventArgs); + + e.Handled = pullEventArgs.Handled; } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 790439245a..7c1ee13eed 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -8,6 +8,10 @@ namespace Avalonia.Input.GestureRecognizers : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise IGestureRecognizer { + // Pixels per second speed that is considered to be the stop of inertial scroll + internal const double InertialScrollSpeedEnd = 5; + public const double InertialResistance = 0.15; + private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; @@ -23,7 +27,8 @@ namespace Avalonia.Input.GestureRecognizers // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; - + private bool _isScrollInertiaEnabled; + /// /// Defines the property. /// @@ -42,6 +47,15 @@ namespace Avalonia.Input.GestureRecognizers o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsScrollInertiaEnabled), + o => o.IsScrollInertiaEnabled, + (o, v) => o.IsScrollInertiaEnabled = v); + /// /// Defines the property. /// @@ -68,6 +82,15 @@ namespace Avalonia.Input.GestureRecognizers get => _canVerticallyScroll; set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + + /// + /// Gets or sets whether the gesture should include inertia in it's behavior. + /// + public bool IsScrollInertiaEnabled + { + get => _isScrollInertiaEnabled; + set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); + } /// /// Gets or sets a value indicating the distance the pointer moves before scrolling is started @@ -97,9 +120,6 @@ namespace Avalonia.Input.GestureRecognizers } } - // Pixels per second speed that is considered to be the stop of inertial scroll - private const double InertialScrollSpeedEnd = 5; - public void PointerMoved(PointerEventArgs e) { if (e.Pointer == _tracking) @@ -168,7 +188,8 @@ namespace Avalonia.Input.GestureRecognizers if (_inertia == default || e.Timestamp == 0 || _lastMoveTimestamp == 0 - || e.Timestamp - _lastMoveTimestamp > 200) + || e.Timestamp - _lastMoveTimestamp > 200 + || !IsScrollInertiaEnabled) EndGesture(); else { @@ -176,6 +197,7 @@ namespace Avalonia.Input.GestureRecognizers var savedGestureId = _gestureId; var st = Stopwatch.StartNew(); var lastTime = TimeSpan.Zero; + _target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia)); DispatcherTimer.Run(() => { // Another gesture has started, finish the current one @@ -187,7 +209,7 @@ namespace Avalonia.Input.GestureRecognizers var elapsedSinceLastTick = st.Elapsed - lastTime; lastTime = st.Elapsed; - var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); + var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); _target!.RaiseEvent(scrollGestureEventArgs); diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index a9e42c2374..167f61eead 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -45,6 +45,10 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = + RoutedEvent.Register( + "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEndedEvent = RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs index f3de55ebae..8d15c3f9ec 100644 --- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs @@ -7,19 +7,18 @@ namespace Avalonia.Input /// public class GotFocusEventArgs : RoutedEventArgs { - internal GotFocusEventArgs() + public GotFocusEventArgs() : base(InputElement.GotFocusEvent) { - } /// /// Gets or sets a value indicating how the change in focus occurred. /// - public NavigationMethod NavigationMethod { get; set; } + public NavigationMethod NavigationMethod { get; init; } /// /// Gets or sets any key modifiers active at the time of focus. /// - public KeyModifiers KeyModifiers { get; set; } + public KeyModifiers KeyModifiers { get; init; } } } diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 35fa549995..9fa097c4b1 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,15 +5,10 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public KeyEventArgs() - { + public IKeyboardDevice? Device { get; init; } - } + public Key Key { get; init; } - public IKeyboardDevice? Device { get; set; } - - public Key Key { get; set; } - - public KeyModifiers KeyModifiers { get; set; } + public KeyModifiers KeyModifiers { get; init; } } } diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index c6618fd550..9ee8ae9711 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -136,7 +136,7 @@ namespace Avalonia.Input return StringBuilderCache.GetStringAndRelease(s); } - public bool Matches(KeyEventArgs keyEvent) => + public bool Matches(KeyEventArgs? keyEvent) => keyEvent != null && keyEvent.KeyModifiers == KeyModifiers && ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index 26ff71a4e7..c46834fff4 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -156,7 +156,6 @@ namespace Avalonia.Input interactive?.RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = method, KeyModifiers = keyModifiers, }); diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index b05d8f30bb..ba909de60f 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Avalonia.Input.Navigation; using Avalonia.VisualTree; @@ -51,7 +50,7 @@ namespace Avalonia.Input // If there's a custom keyboard navigation handler as an ancestor, use that. var custom = (element as Visual)?.FindAncestorOfType(true); - if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce)) + if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce)) return ce; var result = direction switch @@ -117,32 +116,27 @@ namespace Avalonia.Input NavigationDirection direction, [NotNullWhen(true)] out IInputElement? result) { - if (customHandler != null) + var (handled, next) = customHandler.GetNext(element, direction); + + if (handled) { - var (handled, next) = customHandler.GetNext(element, direction); + if (next is not null) + { + result = next; + return true; + } - if (handled) + var r = direction switch { - if (next != null) - { - result = next; - return true; - } - else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) - { - var r = direction switch - { - NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), - NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), - _ => throw new NotSupportedException(), - }; - - if (r is object) - { - result = r; - return true; - } - } + NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler), + NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler), + _ => null + }; + + if (r is not null) + { + result = r; + return true; } } diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index d218867cf2..c460ecf3b3 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using Avalonia.VisualTree; namespace Avalonia.Input.Navigation @@ -54,8 +52,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here for Cycle groups if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; var firstTabElementInside = GetNextTab(null, nextTabElement, true); if (firstTabElementInside != null) @@ -80,12 +77,9 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetLastInTree(container) is { } last) { - var last = GetLastInTree(container); - - if (last is object) - return GetNextTab(last, false); + return GetNextTab(last, false); } return null; @@ -93,11 +87,8 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly) { - if (e is null && container is null) - throw new InvalidOperationException("Either 'e' or 'container' must be non-null."); - - if (container is null) - container = GetGroupParent(e!); + container ??= + GetGroupParent(e ?? throw new InvalidOperationException("Either 'e' or 'container' must be non-null.")); KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container); @@ -163,8 +154,7 @@ namespace Avalonia.Input.Navigation // Avoid the endless loop here if (loopStartElement == nextTabElement) break; - if (loopStartElement == null) - loopStartElement = nextTabElement; + loopStartElement ??= nextTabElement; // At this point nextTabElement is TabGroup var lastTabElementInside = GetPrevTab(null, nextTabElement, true); @@ -189,22 +179,18 @@ namespace Avalonia.Input.Navigation public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e) { - if (e is IInputElement container) + if (e is IInputElement container && GetFirstChild(container) is { } first) { - var first = GetFirstChild(container); - - if (first is object) - return GetPrevTab(first, null, false); + return GetPrevTab(first, null, false); } return null; } - private static IInputElement? FocusedElement(IInputElement e) + private static IInputElement? FocusedElement(IInputElement? e) { - var iie = e; // Focus delegation is enabled only if keyboard focus is outside the container - if (iie != null && !iie.IsKeyboardFocusWithin) + if (e != null && !e.IsKeyboardFocusWithin) { var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e); if (focusedElement != null) @@ -229,13 +215,11 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetFirstChild(IInputElement e) { // If the element has a FocusedElement it should be its first child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the first visible element. - var uiElement = e as InputElement; - - if (uiElement is null || IsVisibleAndEnabled(uiElement)) + if (e is not InputElement uiElement || IsVisibleAndEnabled(uiElement)) { if (e is Visual elementAsVisual) { @@ -265,7 +249,7 @@ namespace Avalonia.Input.Navigation private static IInputElement? GetLastChild(IInputElement e) { // If the element has a FocusedElement it should be its last child - if (FocusedElement(e) is IInputElement focusedElement) + if (FocusedElement(e) is { } focusedElement) return focusedElement; // Return the last visible element. @@ -273,9 +257,7 @@ namespace Avalonia.Input.Navigation if (uiElement == null || IsVisibleAndEnabled(uiElement)) { - var elementAsVisual = e as Visual; - - if (elementAsVisual != null) + if (e is Visual elementAsVisual) { var children = elementAsVisual.VisualChildren; var count = children.Count; @@ -322,7 +304,7 @@ namespace Avalonia.Input.Navigation return firstTabElement; } - private static IInputElement? GetLastInTree(IInputElement container) + private static IInputElement GetLastInTree(IInputElement container) { IInputElement? result; IInputElement? c = container; diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index bf2a5a8602..3de352fc4f 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,9 +6,9 @@ namespace Avalonia.Input.Platform [NotClientImplementable] public interface IClipboard { - Task GetTextAsync(); + Task GetTextAsync(); - Task SetTextAsync(string text); + Task SetTextAsync(string? text); Task ClearAsync(); @@ -16,6 +16,6 @@ namespace Avalonia.Input.Platform Task GetFormatsAsync(); - Task GetDataAsync(string format); + Task GetDataAsync(string format); } } diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index af0fa83382..c405cdfacd 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -1,13 +1,17 @@ +using System; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input { public class PointerDeltaEventArgs : PointerEventArgs { - public Vector Delta { get; set; } + public Vector Delta { get; } - internal PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, + [Unstable] + [Obsolete("This constructor might be removed in 12.0.")] + public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(routedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index d736253728..50d7cc5dc5 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -13,7 +14,9 @@ namespace Avalonia.Input private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; - internal PointerEventArgs(RoutedEvent routedEvent, + [Unstable] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + public PointerEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual? rootVisual, Point rootVisualPosition, @@ -124,7 +127,9 @@ namespace Avalonia.Input public class PointerPressedEventArgs : PointerEventArgs { - internal PointerPressedEventArgs( + [Unstable] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + public PointerPressedEventArgs( object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, @@ -143,7 +148,9 @@ namespace Avalonia.Input public class PointerReleasedEventArgs : PointerEventArgs { - internal PointerReleasedEventArgs( + [Unstable] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] + public PointerReleasedEventArgs( object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, @@ -164,7 +171,9 @@ namespace Avalonia.Input { public IPointer Pointer { get; } - internal PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) + [Unstable] + [Obsolete("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")] + public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) { Pointer = pointer; Source = source; diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index a3de0eaaea..903019d85d 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -1,13 +1,17 @@ +using System; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { - public Vector Delta { get; set; } + public Vector Delta { get; } - internal PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, + [Unstable] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] + public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs index 48c882197f..cd1cf29bcf 100644 --- a/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs @@ -12,6 +12,6 @@ namespace Avalonia.Input.Raw Text = text; } - public string Text { get; set; } + public string Text { get; } } } diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index f1a0887b60..dd78080708 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input public static int GetNextFreeId() => _nextId++; - internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) { Id = id; Delta = delta; @@ -25,9 +25,21 @@ namespace Avalonia.Input { public int Id { get; } - internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) { Id = id; } } + + public class ScrollGestureInertiaStartingEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Inertia { get; } + + internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent) + { + Id = id; + Inertia = inertia; + } + } } diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs index 3e15c4843a..663207a104 100644 --- a/src/Avalonia.Base/Input/TappedEventArgs.cs +++ b/src/Avalonia.Base/Input/TappedEventArgs.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -7,7 +8,7 @@ namespace Avalonia.Input { private readonly PointerEventArgs lastPointerEventArgs; - internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) + public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) : base(routedEvent) { this.lastPointerEventArgs = lastPointerEventArgs; diff --git a/src/Avalonia.Base/Input/TextInput/ITextEditable.cs b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs new file mode 100644 index 0000000000..f9f5ec8d40 --- /dev/null +++ b/src/Avalonia.Base/Input/TextInput/ITextEditable.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Input.TextInput +{ + [NotClientImplementable] + public interface ITextEditable + { + event EventHandler TextChanged; + event EventHandler SelectionChanged; + event EventHandler CompositionChanged; + int SelectionStart { get; set; } + int SelectionEnd { get; set; } + int CompositionStart { get; } + int CompositionEnd { get; } + + string? Text { get; set; } + } +} diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 531cf3c704..2cdcd33626 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Media.TextFormatting; using Avalonia.VisualTree; namespace Avalonia.Input.TextInput @@ -30,6 +31,11 @@ namespace Avalonia.Input.TextInput /// void SetPreeditText(string? text); + /// + /// Sets the current composing region. This doesn't remove the composing text from the commited text. + /// + void SetComposingRegion(TextRange? region); + /// /// Indicates if text input client is capable of providing the text around the cursor /// @@ -43,6 +49,11 @@ namespace Avalonia.Input.TextInput /// event EventHandler? SurroundingTextChanged; + /// + /// Gets or sets a platform editable. Text and selection changes made in the editable are forwarded to the IM client. + /// + ITextEditable? TextEditable { get; set; } + void SelectInSurroundingText(int start, int end); } diff --git a/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs b/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs index 09716b4246..36ee1fff27 100644 --- a/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs +++ b/src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs @@ -105,5 +105,13 @@ namespace Avalonia.Input.TextInput UnsubscribeFromParents(); UpdateMatrix(); } + + public static IDisposable Track(Visual visual, Action cb) + { + var rv = new TransformTrackingHelper(); + rv.MatrixChanged += () => cb(visual, rv.Matrix); + rv.SetVisual(visual); + return rv; + } } } diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs index a027bec0c6..cda0103749 100644 --- a/src/Avalonia.Base/Input/TextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs @@ -4,10 +4,6 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public TextInputEventArgs() - { - - } public IKeyboardDevice? Device { get; set; } public string? Text { get; set; } diff --git a/src/Avalonia.Base/Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs index 3e8098f904..2ce95cf35a 100644 --- a/src/Avalonia.Base/Input/VectorEventArgs.cs +++ b/src/Avalonia.Base/Input/VectorEventArgs.cs @@ -5,11 +5,6 @@ namespace Avalonia.Input { public class VectorEventArgs : RoutedEventArgs { - internal VectorEventArgs() - { - - } - - public Vector Vector { get; set; } + public Vector Vector { get; init; } } } diff --git a/src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs b/src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs new file mode 100644 index 0000000000..b6913939ab --- /dev/null +++ b/src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs @@ -0,0 +1,39 @@ +namespace Avalonia.Interactivity +{ + /// + /// Provides state information and data specific to a cancelable routed event. + /// + public class CancelRoutedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + public CancelRoutedEventArgs() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. + public CancelRoutedEventArgs(RoutedEvent? routedEvent) + : base(routedEvent) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The routed event associated with these event args. + /// The source object that raised the routed event. + public CancelRoutedEventArgs(RoutedEvent? routedEvent, object? source) + : base(routedEvent, source) + { + } + + /// + /// Gets or sets a value indicating whether the routed event should be canceled. + /// + public bool Cancel { get; set; } = false; + } +} diff --git a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs index 749d2ecc2b..1cdc775b13 100644 --- a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs +++ b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs @@ -7,7 +7,7 @@ namespace Avalonia.Layout /// public class EffectiveViewportChangedEventArgs : EventArgs { - internal EffectiveViewportChangedEventArgs(Rect effectiveViewport) + public EffectiveViewportChangedEventArgs(Rect effectiveViewport) { EffectiveViewport = effectiveViewport; } diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs new file mode 100644 index 0000000000..9b821053a2 --- /dev/null +++ b/src/Avalonia.Base/Layout/LayoutInformation.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Layout; + +/// +/// Provides access to layout information of a control. +/// +public static class LayoutInformation +{ + /// + /// Gets the available size constraint passed in the previous layout pass. + /// + /// The control. + /// Previous control measure constraint, if any. + public static Size? GetPreviousMeasureConstraint(Layoutable control) + { + return control.PreviousMeasure; + } + + /// + /// Gets the control bounds used in the previous layout arrange pass. + /// + /// The control. + /// Previous control arrange bounds, if any. + public static Rect? GetPreviousArrangeBounds(Layoutable control) + { + return control.PreviousArrange; + } +} diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 242d70821a..c4742bcba4 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -3,8 +3,9 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using Avalonia.Logging; +using Avalonia.Rendering; using Avalonia.Threading; -using Avalonia.VisualTree; +using Avalonia.Utilities; #nullable enable @@ -24,6 +25,7 @@ namespace Avalonia.Layout private bool _disposed; private bool _queued; private bool _running; + private int _totalPassCount; public LayoutManager(ILayoutRoot owner) { @@ -33,6 +35,8 @@ namespace Avalonia.Layout public virtual event EventHandler? LayoutUpdated; + internal Action? LayoutPassTimed { get; set; } + /// public virtual void InvalidateMeasure(Layoutable control) { @@ -116,10 +120,9 @@ namespace Avalonia.Layout if (!_running) { - Stopwatch? stopwatch = null; - const LogEventLevel timingLogLevel = LogEventLevel.Information; - bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout); + var captureTiming = LayoutPassTimed is not null || Logger.IsEnabled(timingLogLevel, LogArea.Layout); + var startingTimestamp = 0L; if (captureTiming) { @@ -129,8 +132,7 @@ namespace Avalonia.Layout _toMeasure.Count, _toArrange.Count); - stopwatch = new Stopwatch(); - stopwatch.Start(); + startingTimestamp = Stopwatch.GetTimestamp(); } _toMeasure.BeginLoop(MaxPasses); @@ -139,6 +141,7 @@ namespace Avalonia.Layout try { _running = true; + ++_totalPassCount; for (var pass = 0; pass < MaxPasses; ++pass) { @@ -160,9 +163,10 @@ namespace Avalonia.Layout if (captureTiming) { - stopwatch!.Stop(); + var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); + LayoutPassTimed?.Invoke(new LayoutPassTiming(_totalPassCount, elapsed)); - Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed); + Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", elapsed); } } diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..4a273b0291 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -323,6 +323,9 @@ namespace Avalonia.Layout set { SetValue(UseLayoutRoundingProperty, value); } } + /// + /// Gets the available size passed in the previous layout pass, if any. + /// internal Size? PreviousMeasure => _previousMeasure; /// diff --git a/src/Avalonia.Base/Logging/TraceLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs index fc3897fade..a1b4dfe3aa 100644 --- a/src/Avalonia.Base/Logging/TraceLogSink.cs +++ b/src/Avalonia.Base/Logging/TraceLogSink.cs @@ -141,7 +141,7 @@ namespace Avalonia.Logging result.Append(')'); } - return result.ToString(); + return StringBuilderCache.GetStringAndRelease(result); } } } diff --git a/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs index 74720c0a77..9ab7da3ff7 100644 --- a/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Base/LogicalTree/LogicalExtensions.cs @@ -48,7 +48,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First ancestor of given type. - public static T? FindLogicalAncestorOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalAncestorOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -120,7 +120,7 @@ namespace Avalonia.LogicalTree /// The logical. /// If given logical should be included in search. /// First descendant of given type. - public static T? FindLogicalDescendantOfType(this ILogical logical, bool includeSelf = false) where T : class + public static T? FindLogicalDescendantOfType(this ILogical? logical, bool includeSelf = false) where T : class { if (logical is null) { @@ -185,7 +185,7 @@ namespace Avalonia.LogicalTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target) + public static bool IsLogicalAncestorOf(this ILogical? logical, ILogical? target) { var current = target?.LogicalParent; diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index b9a560ad8f..accabce145 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : Animatable + public abstract class Brush : Animatable, IBrush { /// /// Defines the property. diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 5470a735b3..74e70b2a14 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -147,16 +147,11 @@ namespace Avalonia.Media /// The color string. /// The parsed color /// The status of the operation. - public static bool TryParse(string s, out Color color) + public static bool TryParse(string? s, out Color color) { color = default; - if (s is null) - { - return false; - } - - if (s.Length == 0) + if (string.IsNullOrEmpty(s)) { return false; } @@ -336,7 +331,7 @@ namespace Avalonia.Media /// /// Parses the given string representing a CSS color value into a new . /// - private static bool TryParseCssFormat(string s, out Color color) + private static bool TryParseCssFormat(string? s, out Color color) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 077816c645..a37fa6fd32 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -240,13 +240,13 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun); + PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); } } @@ -361,11 +361,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 7d3b4c056e..7b02649b6c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -13,14 +13,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityProperty = AvaloniaProperty.Register(nameof(Opacity), 1); - public static readonly StyledProperty TransformProperty = - AvaloniaProperty.Register(nameof(Transform)); + public static readonly StyledProperty TransformProperty = + AvaloniaProperty.Register(nameof(Transform)); - public static readonly StyledProperty ClipGeometryProperty = - AvaloniaProperty.Register(nameof(ClipGeometry)); + public static readonly StyledProperty ClipGeometryProperty = + AvaloniaProperty.Register(nameof(ClipGeometry)); - public static readonly StyledProperty OpacityMaskProperty = - AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly StyledProperty OpacityMaskProperty = + AvaloniaProperty.Register(nameof(OpacityMask)); public static readonly DirectProperty ChildrenProperty = AvaloniaProperty.RegisterDirect( @@ -36,19 +36,19 @@ namespace Avalonia.Media set => SetValue(OpacityProperty, value); } - public Transform Transform + public Transform? Transform { get => GetValue(TransformProperty); set => SetValue(TransformProperty, value); } - public Geometry ClipGeometry + public Geometry? ClipGeometry { get => GetValue(ClipGeometryProperty); set => SetValue(ClipGeometryProperty, value); } - public IBrush OpacityMask + public IBrush? OpacityMask { get => GetValue(OpacityMaskProperty); set => SetValue(OpacityMaskProperty, value); @@ -74,10 +74,12 @@ namespace Avalonia.Media public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -159,7 +161,7 @@ namespace Avalonia.Media public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - if (((brush == null) && (pen == null)) || (geometry == null)) + if ((brush == null) && (pen == null)) { return; } @@ -167,25 +169,24 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { - if (foreground == null || glyphRun == null) + if (foreground == null) { return; } - // Add a GlyphRunDrawing to the Drawing graph GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = glyphRun, + GlyphRun = new GlyphRun(glyphRun) }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { if (pen == null) { @@ -285,7 +286,7 @@ namespace Avalonia.Media throw new NotImplementedException(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 38ddbdfaed..1b22a1ee69 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -20,8 +20,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty DrawingProperty = - AvaloniaProperty.Register(nameof(Drawing)); + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); /// public event EventHandler? Invalidated; @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Gets or sets the drawing content. /// [Content] - public Drawing Drawing + public Drawing? Drawing { get => GetValue(DrawingProperty); set => SetValue(DrawingProperty, value); diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index da84861668..f4406bd010 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -119,7 +119,7 @@ namespace Avalonia.Media case 2: { - var source = segments[0].StartsWith("/") + var source = segments[0].StartsWith("/", StringComparison.Ordinal) ? new Uri(segments[0], UriKind.Relative) : new Uri(segments[0], UriKind.RelativeOrAbsolute); @@ -188,7 +188,7 @@ namespace Avalonia.Media { unchecked { - return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); + return (FamilyNames.GetHashCode() * 397) ^ (Key is not null ? Key.GetHashCode() : 0); } } diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index e82d5b7ba5..2dabb29e76 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = typeface.GlyphTypeface; + var glyphTypeface = GetOrAddGlyphTypeface(typeface); if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ return true; diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index eb42f6443b..f2350f5aea 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -1,13 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Text; using Avalonia.Utilities; namespace Avalonia.Media.Fonts { public sealed class FamilyNameCollection : IReadOnlyList { + private readonly string[] _names; + /// /// Initializes a new instance of the class. /// @@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); + _names = SplitNames(familyNames); - PrimaryFamilyName = Names[0]; + PrimaryFamilyName = _names[0]; - HasFallbacks = Names.Count > 1; + HasFallbacks = _names.Length > 1; } + private static string[] SplitNames(string names) +#if NET6_0_OR_GREATER + => names.Split(',', StringSplitOptions.TrimEntries); +#else + => Array.ConvertAll(names.Split(','), p => p.Trim()); +#endif + /// /// Gets the primary family name. /// @@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts /// public bool HasFallbacks { get; } - /// - /// Gets the internal collection of names. - /// - /// - /// The names. - /// - internal IReadOnlyList Names { get; } - /// /// Returns an enumerator for the name collection. /// @@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts /// A that represents this instance. /// public override string ToString() - { - var builder = StringBuilderCache.Acquire(); - - for (var index = 0; index < Names.Count; index++) - { - builder.Append(Names[index]); - - if (index == Names.Count - 1) - { - break; - } - - builder.Append(", "); - } - - return StringBuilderCache.GetStringAndRelease(builder); - } + => String.Join(", ", _names); /// /// Returns a hash code for this instance. @@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - if (Count == 0) + if (_names.Length == 0) { return 0; } @@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts { int hash = 17; - for (var i = 0; i < Names.Count; i++) + for (var i = 0; i < _names.Length; i++) { - string name = Names[i]; + string name = _names[i]; hash = hash * 23 + name.GetHashCode(); } @@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts /// true if the specified is equal to this instance; otherwise, false. /// public override bool Equals(object? obj) - { - if (!(obj is FamilyNameCollection other)) - { - return false; - } - - if (other.Count != Count) - { - return false; - } - - for (int i = 0; i < Count; i++) - { - if (Names[i] != other.Names[i]) - { - return false; - } - } - - return true; - } + => obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names); - public int Count => Names.Count; + public int Count => _names.Length; - public string this[int index] => Names[index]; + public string this[int index] => _names[index]; } } diff --git a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs index f607c67fed..12bb7e77e7 100644 --- a/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs +++ b/src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs @@ -41,10 +41,7 @@ namespace Avalonia.Media.Fonts { var hash = (int)2166136261; - if (Source != null) - { - hash = (hash * 16777619) ^ Source.GetHashCode(); - } + hash = (hash * 16777619) ^ Source.GetHashCode(); if (BaseUri != null) { diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 0bab473442..3b63a98720 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -741,6 +741,11 @@ namespace Avalonia.Media null // no previous line break ); + if(Current is null) + { + return false; + } + // check if this line fits the text height if (_totalHeight + Current.Height > _that._maxTextHeight) { @@ -779,7 +784,7 @@ namespace Avalonia.Media // maybe there is no next line at all if (Position + Current.Length < _that._text.Length) { - bool nextLineFits; + bool nextLineFits = false; if (_lineCount + 1 >= _that._maxLineCount) { @@ -795,7 +800,10 @@ namespace Avalonia.Media currentLineBreak ); - nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + if(_nextLine != null) + { + nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + } } if (!nextLineFits) @@ -819,16 +827,22 @@ namespace Avalonia.Media _previousLineBreak ); - currentLineBreak = Current.TextLineBreak; + if(Current != null) + { + currentLineBreak = Current.TextLineBreak; + } _that._defaultParaProps.SetTextWrapping(currentWrap); } } } - _previousHeight = Current.Height; + if(Current != null) + { + _previousHeight = Current.Height; - Length = Current.Length; + Length = Current.Length; + } _previousLineBreak = currentLineBreak; @@ -838,7 +852,7 @@ namespace Avalonia.Media /// /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// - private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) + private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) { var line = _formatter.FormatLine( textSource, @@ -848,7 +862,7 @@ namespace Avalonia.Media lineBreak ); - if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) + if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) { // what I really need here is the last displayed text run of the line // textSourcePosition + line.Length - 1 works except the end of paragraph case, @@ -1340,7 +1354,7 @@ namespace Avalonia.Media { var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0); - if (highlightBounds != null) + if (highlightBounds.Count > 0) { foreach (var bound in highlightBounds) { @@ -1351,7 +1365,7 @@ namespace Avalonia.Media // Convert logical units (which extend leftward from the right edge // of the paragraph) to physical units. // - // Note that since rect is in logical units, rect.Right corresponds to + // Note that since rect is in logical units, rect.Right corresponds to // the visual *left* edge of the rectangle in the RTL case. Specifically, // is the distance leftward from the right edge of the formatting rectangle // whose width is the paragraph width passed to FormatLine. @@ -1370,7 +1384,7 @@ namespace Avalonia.Media else { accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union); - } + } } } } @@ -1601,11 +1615,11 @@ namespace Avalonia.Media } /// - public TextRun? GetTextRun(int textSourceCharacterIndex) + public TextRun GetTextRun(int textSourceCharacterIndex) { if (textSourceCharacterIndex >= _that._text.Length) { - return null; + return new TextEndOfParagraph(); } var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 26cc2c3cab..ac2dce1e42 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -15,8 +15,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty GeometryProperty = - AvaloniaProperty.Register(nameof(Geometry)); + public static readonly StyledProperty GeometryProperty = + AvaloniaProperty.Register(nameof(Geometry)); /// /// Defines the property. @@ -34,7 +34,7 @@ namespace Avalonia.Media /// Gets or sets the that describes the shape of this . /// [Content] - public Geometry Geometry + public Geometry? Geometry { get => GetValue(GeometryProperty); set => SetValue(GeometryProperty, value); diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index fc4bc6aa1c..2966ceee8d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,64 +13,133 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private static readonly IComparer s_ascendingComparer = Comparer.Default; - private static readonly IComparer s_descendingComparer = new ReverseComparer(); + private readonly static IPlatformRenderInterface s_renderInterface; - private IGlyphRunImpl? _glyphRunImpl; - private IGlyphTypeface _glyphTypeface; + private IRef? _platformImpl; private double _fontRenderingEmSize; private int _biDiLevel; - private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlyMemory _characters; - private IReadOnlyList _glyphIndices; - private IReadOnlyList? _glyphAdvances; - private IReadOnlyList? _glyphOffsets; - private IReadOnlyList? _glyphClusters; + private IReadOnlyList _glyphInfos; + private Point? _baselineOrigin; + private bool _hasOneCharPerCluster; // if true, character index and cluster are similar + + static GlyphRun() + { + s_renderInterface = AvaloniaLocator.Current.GetRequiredService(); + } /// - /// Initializes a new instance of the class by specifying properties of the class. + /// Initializes a new instance of the class by specifying properties of the class. /// /// The glyph typeface. /// The rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. /// The characters. - /// The glyph clusters. + /// The glyph indices. + /// The baseline origin of the run. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, - IReadOnlyList? glyphAdvances = null, - IReadOnlyList? glyphOffsets = null, - IReadOnlyList? glyphClusters = null, + Point? baselineOrigin = null, + int biDiLevel = 0) + : this(glyphTypeface, fontRenderingEmSize, characters, + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), baselineOrigin, biDiLevel) + { + _hasOneCharPerCluster = true; + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The characters. + /// The list of glyphs used. + /// The baseline origin of the run. + /// The bidi level. + public GlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlyMemory characters, + IReadOnlyList glyphInfos, + Point? baselineOrigin = null, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + GlyphTypeface = glyphTypeface; _fontRenderingEmSize = fontRenderingEmSize; _characters = characters; - _glyphIndices = glyphIndices; + _glyphInfos = glyphInfos; - _glyphAdvances = glyphAdvances; + _baselineOrigin = baselineOrigin; - _glyphOffsets = glyphOffsets; + _biDiLevel = biDiLevel; + } - _glyphClusters = glyphClusters; + internal GlyphRun(IRef platformImpl) + { + _glyphInfos = Array.Empty(); + GlyphTypeface = Typeface.Default.GlyphTypeface; + _platformImpl = platformImpl; + _baselineOrigin = platformImpl.Item.BaselineOrigin; + } - _biDiLevel = biDiLevel; + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, + double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + { + var glyphIndexSpan = ListToSpan(glyphIndices); + var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); + + var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + } + + return glyphInfos; + } + + private static ReadOnlySpan ListToSpan(IReadOnlyList list) + { + var count = list.Count; + + if (count == 0) + { + return default; + } + + if (list is ushort[] array) + { + return array.AsSpan(); + } + +#if NET6_0_OR_GREATER + if (list is List concreteList) + { + return CollectionsMarshal.AsSpan(concreteList); + } +#endif + + array = new ushort[count]; + for (var i = 0; i < count; ++i) + { + array[i] = list[i]; + } + + return array.AsSpan(); } /// /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface => _glyphTypeface; + public IGlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -82,62 +153,23 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height); + public Size Size => PlatformImpl.Item.Size; /// /// /// public GlyphRunMetrics Metrics - { - get - { - _glyphRunMetrics ??= CreateGlyphRunMetrics(); - - return _glyphRunMetrics.Value; - } - } + => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// /// Gets or sets the baseline origin of the. /// public Point BaselineOrigin { - get - { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; - } + get => PlatformImpl.Item.BaselineOrigin; set => Set(ref _baselineOrigin, value); } - /// - /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. - /// - public IReadOnlyList GlyphIndices - { - get => _glyphIndices; - set => Set(ref _glyphIndices, value); - } - - /// - /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. - /// - public IReadOnlyList? GlyphAdvances - { - get => _glyphAdvances; - set => Set(ref _glyphAdvances, value); - } - - /// - /// Gets or sets an array of values representing the offsets of the glyphs in the . - /// - public IReadOnlyList? GlyphOffsets - { - get => _glyphOffsets; - set => Set(ref _glyphOffsets, value); - } - /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// @@ -148,12 +180,16 @@ namespace Avalonia.Media } /// - /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// Gets or sets the list of glyphs to use to render this run. /// - public IReadOnlyList? GlyphClusters + public IReadOnlyList GlyphInfos { - get => _glyphClusters; - set => Set(ref _glyphClusters, value); + get => _glyphInfos; + set + { + Set(ref _glyphInfos, value); + _hasOneCharPerCluster = false; + } } /// @@ -178,18 +214,8 @@ namespace Avalonia.Media /// /// The platform implementation of the . /// - public IGlyphRunImpl GlyphRunImpl - { - get - { - if (_glyphRunImpl == null) - { - Initialize(); - } - - return _glyphRunImpl!; - } - } + public IRef PlatformImpl + => _platformImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -197,9 +223,7 @@ namespace Avalonia.Media /// The geometry returned contains the combined geometry of all glyphs in the glyph run. public Geometry BuildGeometry() { - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - - var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this); + var geometryImpl = s_renderInterface.BuildGlyphRunGeometry(this); return new PlatformGeometry(geometryImpl); } @@ -221,38 +245,32 @@ namespace Avalonia.Media if (IsLeftToRight) { - if (GlyphClusters != null) + if (characterIndex < Metrics.FirstCluster) { - if (characterIndex < Metrics.FirstCluster) - { - return 0; - } + return 0; + } - if (characterIndex > Metrics.LastCluster) - { - return Metrics.WidthIncludingTrailingWhitespace; - } + if (characterIndex > Metrics.LastCluster) + { + return Size.Width; } var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) - { - var currentCluster = GlyphClusters[glyphIndex]; + var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; - //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + //Move to the end of the glyph cluster + if (characterHit.TrailingLength > 0) + { + while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { - while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) - { - glyphIndex++; - } + glyphIndex++; } } for (var i = 0; i < glyphIndex; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return distance; @@ -262,22 +280,19 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null && GlyphClusters.Count > 0) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex > Metrics.LastCluster) - { - return 0; - } + return 0; + } - if (characterIndex <= Metrics.FirstCluster) - { - return Size.Width; - } + if (characterIndex <= Metrics.FirstCluster) + { + return Size.Width; } - for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) + for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return Size.Width - distance; @@ -322,11 +337,12 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; if (distance > currentX && distance <= currentX + advance) { @@ -340,11 +356,12 @@ namespace Avalonia.Media { currentX = Size.Width; - for (var index = GlyphIndices.Count - 1; index >= 0; index--) + for (var index = _glyphInfos.Count - 1; index >= 0; index--) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; var offsetX = currentX - advance; @@ -424,7 +441,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null || GlyphClusters.Count == 0) + if (_hasOneCharPerCluster) { return characterIndex; } @@ -433,7 +450,7 @@ namespace Avalonia.Media { if (IsLeftToRight) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return 0; @@ -446,15 +463,13 @@ namespace Avalonia.Media return 0; } - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } - var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - - var clusters = GlyphClusters; + var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer; // Find the start of the cluster at the character index. - var start = clusters.BinarySearch(characterIndex, comparer); + var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); // No cluster found. if (start < 0) @@ -463,40 +478,38 @@ namespace Avalonia.Media { characterIndex--; - start = clusters.BinarySearch(characterIndex, comparer); + start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); } if (start < 0) { - goto result; + return 0; } } if (IsLeftToRight) { - while (start > 0 && clusters[start - 1] == clusters[start]) + while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start--; } } else { - while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) + while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start++; } } - result: - if (start < 0) { return 0; } - if (start > GlyphIndices.Count - 1) + if (start > _glyphInfos.Count - 1) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return start; @@ -516,14 +529,14 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(index); - if (GlyphClusters == null) + if (_hasOneCharPerCluster) { - width = GetGlyphAdvance(index, out _); + width = _glyphInfos[index].GlyphAdvance; return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[glyphIndex]; + var cluster = _glyphInfos[glyphIndex].GlyphCluster; var nextCluster = cluster; @@ -531,13 +544,13 @@ namespace Avalonia.Media while (nextCluster == cluster) { - width += GetGlyphAdvance(currentIndex, out _); + width += _glyphInfos[currentIndex].GlyphAdvance; if (IsLeftToRight) { currentIndex++; - if (currentIndex == GlyphClusters.Count) + if (currentIndex == _glyphInfos.Count) { break; } @@ -552,7 +565,7 @@ namespace Avalonia.Media } } - nextCluster = GlyphClusters[currentIndex]; + nextCluster = _glyphInfos[currentIndex].GlyphCluster; } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -565,9 +578,9 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (int i = 1; i < GlyphClusters.Count; i++) + for (int i = 1; i < _glyphInfos.Count; i++) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -583,9 +596,9 @@ namespace Avalonia.Media } else { - for (int i = GlyphClusters.Count - 1; i >= 0; i--) + for (int i = _glyphInfos.Count - 1; i >= 0; i--) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -613,51 +626,19 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Gets a glyph's width. - /// - /// The glyph index. - /// The current cluster. - /// The glyph's width. - private double GetGlyphAdvance(int index, out int cluster) - { - cluster = GlyphClusters != null ? GlyphClusters[index] : index; - - if (GlyphAdvances != null) - { - return GlyphAdvances[index]; - } - - var glyph = GlyphIndices[index]; - - return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; - } - - /// - /// Calculates the default baseline origin of the . - /// - /// The baseline origin. - private Point CalculateBaselineOrigin() - { - return new Point(0, -GlyphTypeface.Metrics.Ascent * Scale); - } - private GlyphRunMetrics CreateGlyphRunMetrics() { - int firstCluster = 0, lastCluster = 0; + int firstCluster, lastCluster; - if (_glyphClusters != null && _glyphClusters.Count > 0) + if (Characters.IsEmpty) { - firstCluster = _glyphClusters[0]; - lastCluster = _glyphClusters[_glyphClusters.Count - 1]; + firstCluster = 0; + lastCluster = 0; } else { - if (!Characters.IsEmpty) - { - firstCluster = 0; - lastCluster = Characters.Length - 1; - } + firstCluster = _glyphInfos[0].GlyphCluster; + lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } if (!IsLeftToRight) @@ -671,9 +652,9 @@ namespace Avalonia.Media var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out _); + var advance = _glyphInfos[index].GlyphAdvance; widthIncludingTrailingWhitespace += advance; } @@ -684,21 +665,19 @@ namespace Avalonia.Media { for (var index = 0; index < glyphCount; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } else { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } return new GlyphRunMetrics( width, - widthIncludingTrailingWhitespace, - height, trailingWhitespaceLength, newLineLength, firstCluster, @@ -710,7 +689,7 @@ namespace Avalonia.Media { if (isReversed) { - return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); + return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } glyphCount = 0; @@ -720,84 +699,59 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = charactersSpan.Length - 1; i >= 0;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); - - if (!codepoint.IsWhiteSpace) - { - break; - } + var characterIndex = charactersSpan.Length - 1; - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i -= count; - glyphCount++; - } - } - else + for (var i = _glyphInfos.Count - 1; i >= 0; i--) { - var characterIndex = charactersSpan.Length - 1; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = _glyphInfos[i - 1].GlyphCluster; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - - characterIndex -= characterLength; - } + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } return trailingWhitespaceLength; } - private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) { glyphCount = 0; newLineLength = 0; @@ -806,71 +760,46 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = 0; i < charactersSpan.Length;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i += count; - glyphCount++; - } - } - else + for (var i = 0; i < _glyphInfos.Count; i++) { - var characterIndex = 0; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = 0; i < GlyphClusters.Count; i++) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex += characterLength; - characterIndex += characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + var j = i; - var j = i; + while (j - 1 >= 0) + { + var nextCluster = _glyphInfos[--j].GlyphCluster; - while (j - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[--j]; - - if (currentCluster == nextCluster) - { - clusterLength++; + clusterLength++; - continue; - } - - break; + continue; } - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; - } - - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount += clusterLength; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount += clusterLength; } } @@ -879,55 +808,32 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - _glyphRunImpl?.Dispose(); + _platformImpl?.Dispose(); - _glyphRunImpl = null; + _platformImpl = null; _glyphRunMetrics = null; - _baselineOrigin = null; - field = value; } - /// - /// Initializes the . - /// - private void Initialize() + private IRef CreateGlyphRunImpl() { - if (GlyphIndices == null) - { - throw new InvalidOperationException(); - } + var platformImpl = s_renderInterface.CreateGlyphRun( + GlyphTypeface, + FontRenderingEmSize, + GlyphInfos, + _baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale)); - var glyphCount = GlyphIndices.Count; + _platformImpl = RefCountable.Create(platformImpl); - if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - - _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + return _platformImpl; } public void Dispose() { - _glyphRunImpl?.Dispose(); - } - - private class ReverseComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y, x); - } + _platformImpl?.Dispose(); + _platformImpl = null; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 242b9913fa..06d92fd81c 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -2,19 +2,19 @@ { public class GlyphRunDrawing : Drawing { - public static readonly StyledProperty ForegroundProperty = - AvaloniaProperty.Register(nameof(Foreground)); + public static readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); - public static readonly StyledProperty GlyphRunProperty = - AvaloniaProperty.Register(nameof(GlyphRun)); + public static readonly StyledProperty GlyphRunProperty = + AvaloniaProperty.Register(nameof(GlyphRun)); - public IBrush Foreground + public IBrush? Foreground { get => GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); } - public GlyphRun GlyphRun + public GlyphRun? GlyphRun { get => GetValue(GlyphRunProperty); set => SetValue(GlyphRunProperty, value); diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index 492b5214cd..09b183d044 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,27 +2,20 @@ { public readonly record struct GlyphRunMetrics { - public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, - int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) + public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) { Width = width; - WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - Height = height; TrailingWhitespaceLength = trailingWhitespaceLength; - NewLineLength= newLineLength; + NewLineLength = newLineLength; FirstCluster = firstCluster; LastCluster = lastCluster; } public double Width { get; } - public double WidthIncludingTrailingWhitespace { get; } - - public double Height { get; } - public int TrailingWhitespaceLength { get; } - - public int NewLineLength { get; } + + public int NewLineLength { get; } public int FirstCluster { get; } diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 425a3138c3..b4bf6fd217 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSL color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HslColor hslColor) + public static bool TryParse(string? s, out HslColor hslColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 9f95b31518..f97457c54d 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -254,7 +254,7 @@ namespace Avalonia.Media /// The HSV color string to parse. /// The parsed . /// True if parsing was successful; otherwise, false. - public static bool TryParse(string s, out HsvColor hsvColor) + public static bool TryParse(string? s, out HsvColor hsvColor) { bool prefixMatched = false; diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 732f1957d0..07fd2d56fa 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -12,6 +12,6 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap Source { get; } + IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs index 6662613ff4..a7d3e4da10 100644 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ b/src/Avalonia.Base/Media/IVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Metadata; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -12,6 +11,6 @@ namespace Avalonia.Media /// /// Gets the visual to draw. /// - Visual Visual { get; } + Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 2f2a0fb627..718ebf1686 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -11,8 +11,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); static ImageBrush() { @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap source) + public ImageBrush(IBitmap? source) { Source = source; } @@ -38,7 +38,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap Source + public IBitmap? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index ce38fc5abc..6577532891 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Avalonia.Platform; using Avalonia.Utilities; @@ -10,6 +12,7 @@ namespace Avalonia.Media.Imaging /// public class Bitmap : IBitmap { + private bool _isTranscoded; /// /// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained. /// This is more efficient than loading and then resizing. @@ -100,7 +103,28 @@ namespace Avalonia.Media.Imaging /// The number of bytes per row. public Bitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) { - PlatformImpl = RefCountable.Create(GetFactory().LoadBitmap(format, alphaFormat, data, size, dpi, stride)); + var factory = GetFactory(); + if (factory.IsSupportedBitmapPixelFormat(format)) + PlatformImpl = RefCountable.Create(factory.LoadBitmap(format, alphaFormat, data, size, dpi, stride)); + else + { + var transcoded = Marshal.AllocHGlobal(size.Width * size.Height * 4); + var transcodedStride = size.Width * 4; + try + { + PixelFormatReader.Transcode(transcoded, data, size, stride, transcodedStride, format); + var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : AlphaFormat.Opaque; + + PlatformImpl = RefCountable.Create(factory.LoadBitmap(PixelFormat.Rgba8888, transcodedAlphaFormat, + transcoded, size, dpi, transcodedStride)); + } + finally + { + Marshal.FreeHGlobal(transcoded); + } + + _isTranscoded = true; + } } /// @@ -145,6 +169,57 @@ namespace Avalonia.Media.Imaging PlatformImpl.Item.Save(stream, quality); } + public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format; + + protected internal unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride, + ILockedFramebuffer fb) + { + if ((sourceRect.Width <= 0 || sourceRect.Height <= 0) && (sourceRect.X != 0 || sourceRect.Y != 0)) + throw new ArgumentOutOfRangeException(nameof(sourceRect)); + + if (sourceRect.X < 0 || sourceRect.Y < 0) + throw new ArgumentOutOfRangeException(nameof(sourceRect)); + + if (sourceRect.Width <= 0) + sourceRect = sourceRect.WithWidth(PixelSize.Width); + if (sourceRect.Height <= 0) + sourceRect = sourceRect.WithHeight(PixelSize.Height); + + if (sourceRect.Right > PixelSize.Width || sourceRect.Bottom > PixelSize.Height) + throw new ArgumentOutOfRangeException(nameof(sourceRect)); + + int minStride = checked(((sourceRect.Width * fb.Format.BitsPerPixel) + 7) / 8); + if (stride < minStride) + throw new ArgumentOutOfRangeException(nameof(stride)); + + var minBufferSize = stride * sourceRect.Height; + if (minBufferSize > bufferSize) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + + for (var y = 0; y < sourceRect.Height; y++) + { + var srcAddress = fb.Address + fb.RowBytes * y; + var dstAddress = buffer + stride * y; + Unsafe.CopyBlock(dstAddress.ToPointer(), srcAddress.ToPointer(), (uint)minStride); + } + } + + public virtual void CopyPixels(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride) + { + if ( + Format == null + || PlatformImpl.Item is not IReadableBitmapImpl readable + || Format != readable.Format + ) + throw new NotSupportedException("CopyPixels is not supported for this bitmap type"); + + if (_isTranscoded) + throw new NotSupportedException("CopyPixels is not supported for transcoded bitmaps"); + + using (var fb = readable.Lock()) + CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb); + } + /// void IImage.Draw( DrawingContext context, diff --git a/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs new file mode 100644 index 0000000000..68ae2e37a5 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/BitmapMemory.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Avalonia.Platform; + +namespace Avalonia.Media.Imaging; + +internal class BitmapMemory : IDisposable +{ + private readonly int _memorySize; + + public BitmapMemory(PixelFormat format, PixelSize size) + { + Format = format; + Size = size; + RowBytes = (size.Width * format.BitsPerPixel + 7) / 8; + _memorySize = RowBytes * size.Height; + Address = Marshal.AllocHGlobal(_memorySize); + GC.AddMemoryPressure(_memorySize); + } + + private void ReleaseUnmanagedResources() + { + if (Address != IntPtr.Zero) + { + GC.RemoveMemoryPressure(_memorySize); + Marshal.FreeHGlobal(Address); + } + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~BitmapMemory() + { + ReleaseUnmanagedResources(); + } + + public IntPtr Address { get; private set; } + public PixelSize Size { get; } + public int RowBytes { get; } + public PixelFormat Format { get; } + + + + public void CopyToRgba(IntPtr buffer, int rowBytes) => + PixelFormatReader.Transcode(buffer, Address, Size, RowBytes, rowBytes, Format); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs new file mode 100644 index 0000000000..fc7c174ed6 --- /dev/null +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -0,0 +1,280 @@ +using System; +using Avalonia.Platform; +namespace Avalonia.Media.Imaging; + +internal struct Rgba8888Pixel +{ + public byte R; + public byte G; + public byte B; + public byte A; +} + +static unsafe class PixelFormatReader +{ + public interface IPixelFormatReader + { + Rgba8888Pixel ReadNext(); + void Reset(IntPtr address); + } + + private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel + { + A = 255, + B = 255, + G = 255, + R = 255 + }; + + private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel + { + A = 255, + B = 0, + G = 0, + R = 0 + }; + + public unsafe struct BlackWhitePixelReader : IPixelFormatReader + { + private int _bit; + private byte* _address; + + public void Reset(IntPtr address) + { + _address = (byte*)address; + _bit = 0; + } + + public Rgba8888Pixel ReadNext() + { + var shift = 7 - _bit; + var value = (*_address >> shift) & 1; + _bit++; + if (_bit == 8) + { + _address++; + _bit = 0; + } + return value == 1 ? s_white : s_black; + } + } + + public unsafe struct Gray2PixelReader : IPixelFormatReader + { + private int _bit; + private byte* _address; + + public void Reset(IntPtr address) + { + _address = (byte*)address; + _bit = 0; + } + + private static Rgba8888Pixel[] Palette = new[] + { + s_black, + new Rgba8888Pixel + { + A = 255, B = 0x55, G = 0x55, R = 0x55 + }, + new Rgba8888Pixel + { + A = 255, B = 0xAA, G = 0xAA, R = 0xAA + }, + s_white + }; + + public Rgba8888Pixel ReadNext() + { + var shift = 6 - _bit; + var value = (byte)((*_address >> shift)); + value = (byte)((value & 3)); + _bit += 2; + if (_bit == 8) + { + _address++; + _bit = 0; + } + + return Palette[value]; + } + } + + public unsafe struct Gray4PixelReader : IPixelFormatReader + { + private int _bit; + private byte* _address; + + public void Reset(IntPtr address) + { + _address = (byte*)address; + _bit = 0; + } + + public Rgba8888Pixel ReadNext() + { + var shift = 4 - _bit; + var value = (byte)((*_address >> shift)); + value = (byte)((value & 0xF)); + value = (byte)(value | (value << 4)); + _bit += 4; + if (_bit == 8) + { + _address++; + _bit = 0; + } + + return new Rgba8888Pixel + { + A = 255, + B = value, + G = value, + R = value + }; + } + } + + public unsafe struct Gray8PixelReader : IPixelFormatReader + { + private byte* _address; + public void Reset(IntPtr address) + { + _address = (byte*)address; + } + + public Rgba8888Pixel ReadNext() + { + var value = *_address; + _address++; + + return new Rgba8888Pixel + { + A = 255, + B = value, + G = value, + R = value + }; + } + } + + public unsafe struct Gray16PixelReader : IPixelFormatReader + { + private ushort* _address; + public Rgba8888Pixel ReadNext() + { + var value16 = *_address; + _address++; + var value8 = (byte)(value16 >> 8); + return new Rgba8888Pixel + { + A = 255, + B = value8, + G = value8, + R = value8 + }; + } + + public void Reset(IntPtr address) => _address = (ushort*)address; + } + + public unsafe struct Gray32FloatPixelReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var f = *(float*)_address; + var srgb = Math.Pow(f, 1 / 2.2); + var value = (byte)(srgb * 255); + + _address += 4; + return new Rgba8888Pixel + { + A = 255, + B = value, + G = value, + R = value + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + struct Rgba64 + { +#pragma warning disable CS0649 + public ushort R; + public ushort G; + public ushort B; + public ushort A; +#pragma warning restore CS0649 + } + + public unsafe struct Rgba64PixelFormatReader : IPixelFormatReader + { + private Rgba64* _address; + public Rgba8888Pixel ReadNext() + { + var value = *_address; + + _address++; + return new Rgba8888Pixel + { + A = (byte)(value.A >> 8), + B = (byte)(value.B >> 8), + G = (byte)(value.G >> 8), + R = (byte)(value.R >> 8), + }; + } + + public void Reset(IntPtr address) => _address = (Rgba64*)address; + } + + public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, + PixelFormat format) + { + if (format == PixelFormats.BlackWhite) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Gray2) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Gray4) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Gray8) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Gray16) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Gray32Float) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Rgba64) + Transcode(dst, src, size, strideSrc, strideDst); + else + throw new NotSupportedException($"Pixel format {format} is not supported"); + } + + public static bool SupportsFormat(PixelFormat format) + { + return format == PixelFormats.BlackWhite + || format == PixelFormats.Gray2 + || format == PixelFormats.Gray4 + || format == PixelFormats.Gray8 + || format == PixelFormats.Gray16 + || format == PixelFormats.Gray32Float + || format == PixelFormats.Rgba64; + } + + public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader + { + var w = size.Width; + var h = size.Height; + TReader reader = default; + for (var y = 0; y < h; y++) + { + reader.Reset(src + strideSrc * y); + var dstRow = (Rgba8888Pixel*)(dst + strideDst * y); + for (var x = 0; x < w; x++) + { + *dstRow = reader.ReadNext(); + dstRow++; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs index 1aac8efac7..868f4439c4 100644 --- a/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using Avalonia.Platform; namespace Avalonia.Media.Imaging @@ -9,7 +10,9 @@ namespace Avalonia.Media.Imaging /// public class WriteableBitmap : Bitmap { - + // Holds a buffer with pixel format that requires transcoding + private BitmapMemory? _pixelFormatMemory = null; + /// /// Initializes a new instance of the class. /// @@ -19,16 +22,67 @@ namespace Avalonia.Media.Imaging /// The alpha format (optional). /// An . public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null, AlphaFormat? alphaFormat = null) - : base(CreatePlatformImpl(size, dpi, format, alphaFormat)) + : this(CreatePlatformImpl(size, dpi, format, alphaFormat)) { } - private WriteableBitmap(IWriteableBitmapImpl impl) : base(impl) + private WriteableBitmap((IBitmapImpl impl, BitmapMemory? mem) bitmapWithMem) : this(bitmapWithMem.impl, + bitmapWithMem.mem) { } + + private WriteableBitmap(IBitmapImpl impl, BitmapMemory? pixelFormatMemory = null) : base(impl) + { + _pixelFormatMemory = pixelFormatMemory; + } + + /// + /// Initializes a new instance of the class with existing pixel data + /// The data is copied to the bitmap + /// + /// The pixel format. + /// The alpha format. + /// The pointer to the source bytes. + /// The size of the bitmap in device pixels. + /// The DPI of the bitmap. + /// The number of bytes per row. + public unsafe WriteableBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) + : this(size, dpi, format, alphaFormat) + { + var minStride = (format.BitsPerPixel * size.Width + 7) / 8; + if (minStride > stride) + throw new ArgumentOutOfRangeException(nameof(stride)); - public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock(); + using (var locked = Lock()) + { + for (var y = 0; y < size.Height; y++) + Unsafe.CopyBlock((locked.Address + locked.RowBytes * y).ToPointer(), + (data + y * stride).ToPointer(), (uint)minStride); + } + } + + public override PixelFormat? Format => _pixelFormatMemory?.Format ?? base.Format; + + public ILockedFramebuffer Lock() + { + if (_pixelFormatMemory == null) + return ((IWriteableBitmapImpl)PlatformImpl.Item).Lock(); + + return new LockedFramebuffer(_pixelFormatMemory.Address, _pixelFormatMemory.Size, + _pixelFormatMemory.RowBytes, + Dpi, _pixelFormatMemory.Format, () => + { + using var inner = ((IWriteableBitmapImpl)PlatformImpl.Item).Lock(); + _pixelFormatMemory.CopyToRgba(inner.Address, inner.RowBytes); + }); + } + + public override void CopyPixels(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride) + { + using (var fb = Lock()) + CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb); + } public static WriteableBitmap Decode(Stream stream) { @@ -67,14 +121,25 @@ namespace Avalonia.Media.Imaging return new WriteableBitmap(ri.LoadWriteableBitmapToHeight(stream, height, interpolationMode)); } - private static IBitmapImpl CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat) + private static (IBitmapImpl, BitmapMemory?) CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat) { + if (size.Width <= 0 || size.Height <= 0) + throw new ArgumentException("Size should be >= (1,1)", nameof(size)); + var ri = GetFactory(); PixelFormat finalFormat = format ?? ri.DefaultPixelFormat; AlphaFormat finalAlphaFormat = alphaFormat ?? ri.DefaultAlphaFormat; - return ri.CreateWriteableBitmap(size, dpi, finalFormat, finalAlphaFormat); + if (ri.IsSupportedBitmapPixelFormat(finalFormat)) + return (ri.CreateWriteableBitmap(size, dpi, finalFormat, finalAlphaFormat), null); + + if (!PixelFormatReader.SupportsFormat(finalFormat)) + throw new NotSupportedException($"Pixel format {finalFormat} is not supported"); + + var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888, + finalFormat.HasAlpha ? finalAlphaFormat : AlphaFormat.Opaque); + return (impl, new BitmapMemory(finalFormat, size)); } private static IPlatformRenderInterface GetFactory() diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index eb6f105680..2564d89bac 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IImmutableBrush foreground, IRef glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 1f53f06955..6dff006045 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -39,17 +39,8 @@ namespace Avalonia.Media.Immutable { return true; } - else if (other is null) - { - return false; - } - if (Offset != other.Offset) - { - return false; - } - - return SequenceEqual(Dashes, other.Dashes); + return other is not null && Offset == other.Offset && SequenceEqual(_dashes, other.Dashes); } /// @@ -58,30 +49,27 @@ namespace Avalonia.Media.Immutable var hashCode = 717868523; hashCode = hashCode * -1521134295 + Offset.GetHashCode(); - if (_dashes != null) + foreach (var i in _dashes) { - foreach (var i in _dashes) - { - hashCode = hashCode * -1521134295 + i.GetHashCode(); - } + hashCode = hashCode * -1521134295 + i.GetHashCode(); } return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) + private static bool SequenceEqual(double[] left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { return true; } - if (left == null || right == null || left.Count != right.Count) + if (right is null || left.Length != right.Count) { return false; } - for (var c = 0; c < left.Count; c++) + for (var c = 0; c < left.Length; c++) { if (left[c] != right[c]) { diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index f9892bf60c..668a907fdf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// The bitmap interpolation mode. public ImmutableImageBrush( - IBitmap source, + IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap Source { get; } + public IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs index 9b443391c5..e9086eee37 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Imaging; -using Avalonia.VisualTree; namespace Avalonia.Media.Immutable { @@ -25,17 +24,17 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// Controls the quality of interpolation. public ImmutableVisualBrush( - Visual visual, + Visual? visual, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, - Imaging.BitmapInterpolationMode bitmapInterpolationMode = Imaging.BitmapInterpolationMode.Default) + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) : base( alignmentX, alignmentY, @@ -62,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public Visual Visual { get; } + public Visual? Visual { get; } } } diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index d6b2841214..b74b7df9c5 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -22,8 +22,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty StrokeProperty = - AvaloniaProperty.Register(nameof(Stroke)); + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); /// /// Defines the property. @@ -34,8 +34,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>(nameof(StrokeDashArray)); + public static readonly StyledProperty?> StrokeDashArrayProperty = + AvaloniaProperty.Register?>(nameof(StrokeDashArray)); /// /// Defines the property. @@ -82,7 +82,7 @@ namespace Avalonia.Media /// /// Gets or sets the that specifies how the is painted. /// - public IBrush Stroke + public IBrush? Stroke { get { return GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } @@ -101,7 +101,7 @@ namespace Avalonia.Media /// Gets or sets a collection of values that indicate the pattern of dashes and gaps /// that is used to draw the . /// - public AvaloniaList StrokeDashArray + public AvaloniaList? StrokeDashArray { get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } @@ -218,9 +218,9 @@ namespace Avalonia.Media { var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; - var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); - if (intersections != null && intersections.Count > 0) + if (intersections.Count > 0) { var last = baselineOrigin.X; var finalPos = last + glyphRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs new file mode 100644 index 0000000000..4db55fae6d --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -0,0 +1,268 @@ +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Reorders text runs according to their bidi level. + /// + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiReorderer + { + [ThreadStatic] private static BidiReorderer? t_instance; + + private ArrayBuilder _runs; + private ArrayBuilder _ranges; + + public static BidiReorderer Instance + => t_instance ??= new(); + + public void BidiReorder(Span textRuns, FlowDirection flowDirection) + { + Debug.Assert(_runs.Length == 0); + Debug.Assert(_ranges.Length == 0); + + if (textRuns.IsEmpty) + { + return; + } + + try + { + _runs.Add(textRuns.Length); + + // Build up the collection of ordered runs. + for (var i = 0; i < textRuns.Length; i++) + { + var textRun = textRuns[i]; + _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); + + if (i > 0) + { + _runs[i - 1].NextRunIndex = i; + } + } + + // Reorder them into visual order. + var firstIndex = LinearReorder(); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + sbyte max = 0; + var min = sbyte.MaxValue; + + for (var i = 0; i < textRuns.Length; i++) + { + var level = GetRunBidiLevel(textRuns[i], flowDirection); + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + var minLevelToReverse = max; + int currentIndex; + + while (minLevelToReverse >= min) + { + currentIndex = firstIndex; + + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + if (current.Level >= minLevelToReverse && current.Level % 2 != 0) + { + if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) + { + shapedTextCharacters.Reverse(); + } + } + + currentIndex = current.NextRunIndex; + } + + minLevelToReverse--; + } + + var index = 0; + + currentIndex = firstIndex; + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + textRuns[index++] = current.Run; + + currentIndex = current.NextRunIndex; + } + } + finally + { + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); + } + } + + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) + { + if (run is ShapedTextRun shapedTextRun) + { + return shapedTextRun.BidiLevel; + } + + var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; + return (sbyte)defaultLevel; + } + + /// + /// Reorders the runs from logical to visual order. + /// + /// + /// The first run index in visual order. + private int LinearReorder() + { + var runIndex = 0; + var rangeIndex = -1; + + while (runIndex >= 0) + { + ref var run = ref _runs[runIndex]; + var nextRunIndex = run.NextRunIndex; + + while (rangeIndex >= 0 + && _ranges[rangeIndex].Level > run.Level + && _ranges[rangeIndex].PreviousRangeIndex >= 0 + && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level) + { + + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex; + _ranges[rangeIndex].LeftRunIndex = runIndex; + } + else + { + // Even, range goes to the left of run. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex; + _ranges[rangeIndex].RightRunIndex = runIndex; + } + + _ranges[rangeIndex].Level = run.Level; + } + else + { + var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex); + _ranges.AddItem(r); + rangeIndex = _ranges.Length - 1; + } + + runIndex = nextRunIndex; + } + + while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0) + { + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + // Terminate. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1; + + return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex; + } + + private int MergeRangeWithPrevious(int index) + { + var previousIndex = _ranges[index].PreviousRangeIndex; + ref var previous = ref _ranges[previousIndex]; + + int leftIndex; + int rightIndex; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + leftIndex = index; + rightIndex = previousIndex; + } + else + { + // Even, previous goes to the left of range. + leftIndex = previousIndex; + rightIndex = index; + } + + // Stitch them + ref var left = ref _ranges[leftIndex]; + ref var right = ref _ranges[rightIndex]; + _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex; + previous.LeftRunIndex = left.LeftRunIndex; + previous.RightRunIndex = right.RightRunIndex; + + return previousIndex; + } + + private struct OrderedBidiRun + { + public OrderedBidiRun(int runIndex, TextRun run, sbyte level) + { + RunIndex = runIndex; + Run = run; + Level = level; + NextRunIndex = -1; + } + + public int RunIndex { get; } + + public sbyte Level { get; } + + public TextRun Run { get; } + + public int NextRunIndex { get; set; } // -1 if none + } + + private struct BidiRange + { + public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) + { + Level = level; + LeftRunIndex = leftRunIndex; + RightRunIndex = rightRunIndex; + PreviousRangeIndex = previousRangeIndex; + } + + public sbyte Level { get; set; } + + public int LeftRunIndex { get; set; } + + public int RightRunIndex { get; set; } + + public int PreviousRangeIndex { get; } // -1 if none + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 5c28989c7d..2f8c4ad263 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting var graphemeEnumerator = new GraphemeEnumerator(text); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; - - finalLength += grapheme.Text.Length; + finalLength += grapheme.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..c27903cd55 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge((uint) arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge((uint) list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); + + stack.Clear(); + + if (IsBufferTooLarge(approximateCapacity)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); + + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(approximateCapacity)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(uint capacity) + => (long) (uint) Unsafe.SizeOf() * capacity > MaxKeptBufferSizeInBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundUpToPowerOf2(uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs new file mode 100644 index 0000000000..c7cd58eb6d --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Contains various list pools that are commonly used during text layout. + /// + /// This class provides an instance per thread. + /// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout. + /// + /// + /// + internal sealed class FormattingObjectPool + { + [ThreadStatic] private static FormattingObjectPool? t_instance; + + /// + /// Gets an instance of this class for the current thread. + /// + /// + /// Since this is backed by a thread static field which is slower than a normal static field, + /// prefer passing the instance around when possible instead of calling this property each time. + /// + public static FormattingObjectPool Instance + => t_instance ??= new(); + + public ListPool TextRunLists { get; } = new(); + + public ListPool UnshapedTextRunLists { get; } = new(); + + public ListPool TextLines { get; } = new(); + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + TextRunLists.VerifyAllReturned(); + UnshapedTextRunLists.VerifyAllReturned(); + TextLines.VerifyAllReturned(); + } + + internal sealed class ListPool + { + // we don't need a big number here, these are for temporary usages only which should quickly be returned + private const int MaxSize = 16; + + private readonly RentedList[] _lists = new RentedList[MaxSize]; + private int _size; + private int _pendingReturnCount; + + /// + /// Rents a list. + /// See for the intended usages. + /// + /// A rented list instance that must be returned to the pool. + /// + public RentedList Rent() + { + var list = _size > 0 ? _lists[--_size] : new RentedList(); + + Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); + + ++_pendingReturnCount; + return list; + } + + /// + /// Returns a rented list to the pool. + /// + /// + /// On input, the list to return. + /// On output, the reference is set to null to avoid misuse. + /// + public void Return(ref RentedList? rentedList) + { + if (rentedList is null) + { + return; + } + + --_pendingReturnCount; + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); + + if (_size < MaxSize) + { + _lists[_size++] = rentedList; + } + + rentedList = null; + } + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + var pendingReturnCount = _pendingReturnCount; + _pendingReturnCount = 0; + + if (pendingReturnCount > 0) + { + throw new InvalidOperationException( + $"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!"); + } + + if (pendingReturnCount < 0) + { + throw new InvalidOperationException( + $"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!"); + } + } + } + + /// + /// Represents a list that has been rented through . + /// + /// This class can be used when a temporary list is needed to store some items during text layout. + /// It can also be used as a reusable array builder by calling when done. + /// + /// + /// NEVER use an instance of this type after it's been returned to the pool. + /// AVOID storing an instance of this type into a field or property. + /// AVOID casting an instance of this type to another type. + /// + /// AVOID passing an instance of this type as an argument to a method expecting a standard list, + /// unless you're absolutely sure it won't store it. + /// + /// + /// If you call a method returning an instance of this type, + /// you're now responsible for returning it to the pool. + /// + /// + /// + /// The type of elements in the list. + internal sealed class RentedList : List + { + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs new file mode 100644 index 0000000000..36a07721a6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a single glyph. + /// + public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) + { + internal static Comparer ClusterAscendingComparer { get; } = + Comparer.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); + + internal static Comparer ClusterDescendingComparer { get; } = + Comparer.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); + + /// + /// Get the glyph index. + /// + public ushort GlyphIndex { get; } = GlyphIndex; + + /// + /// Get the glyph cluster. + /// + public int GlyphCluster { get; } = GlyphCluster; + + /// + /// Get the glyph advance. + /// + public double GlyphAdvance { get; } = GlyphAdvance; + + /// + /// Get the glyph offset. + /// + public Vector GlyphOffset { get; } = GlyphOffset; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs index 26966b37bc..32012ab8e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs @@ -1,6 +1,4 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Produces objects that are used by the . diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index b518d47a6d..0d85f3e7c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,9 +15,7 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { - var lineImpl = textLine as TextLineImpl; - - if(lineImpl is null) + if (textLine is not TextLineImpl lineImpl) { return; } @@ -34,22 +32,18 @@ namespace Avalonia.Media.TextFormatting return; } - var textLineBreak = lineImpl.TextLineBreak; - - if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) + if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) { - if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) - { - return; - } + return; } var breakOportunities = new Queue(); var currentPosition = textLine.FirstTextSourceIndex; - foreach (var textRun in lineImpl.TextRuns) + for (var i = 0; i < lineImpl.TextRuns.Count; ++i) { + var textRun = lineImpl.TextRuns[i]; var text = textRun.Text; if (text.IsEmpty) @@ -59,10 +53,8 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out var currentBreak)) { - var currentBreak = lineBreakEnumerator.Current; - if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) { breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); @@ -108,10 +100,11 @@ namespace Avalonia.Media.TextFormatting var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; - shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); + shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, + glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index b05fab08fa..f29bdd4459 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,24 +1,23 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList, IDisposable + public sealed class ShapedBuffer : IReadOnlyList, IDisposable { - private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _bufferRented; - - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(text, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, - bidiLevel) + private GlyphInfo[]? _rentedBuffer; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _bufferRented = true; - Length = bufferLength; + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + Text = text; + GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -28,12 +27,12 @@ namespace Avalonia.Media.TextFormatting GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; - Length = GlyphInfos.Length; } - internal ArraySlice GlyphInfos { get; } - - public int Length { get; } + internal ArraySlice GlyphInfos { get; private set; } + + public int Length + => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } public bool IsLeftToRight => (BidiLevel & 1) == 0; - - public IReadOnlyList GlyphIndices => new GlyphIndexList(GlyphInfos); - - public IReadOnlyList GlyphClusters => new GlyphClusterList(GlyphInfos); - - public IReadOnlyList GlyphAdvances => new GlyphAdvanceList(GlyphInfos); - - public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); public ReadOnlyMemory Text { get; } @@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting } - var comparer = s_clusterComparer; + var comparer = GlyphInfo.ClusterAscendingComparer; - var clusters = GlyphInfos.Span; + var glyphInfos = GlyphInfos.Span; - var searchValue = new GlyphInfo(0, characterIndex); + var searchValue = new GlyphInfo(default, characterIndex, default); - var start = clusters.BinarySearch(searchValue, comparer); + var start = glyphInfos.BinarySearch(searchValue, comparer); if (start < 0) { @@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting { characterIndex--; - searchValue = new GlyphInfo(0, characterIndex); + searchValue = new GlyphInfo(default, characterIndex, default); - start = clusters.BinarySearch(searchValue, comparer); + start = glyphInfos.BinarySearch(searchValue, comparer); } if (start < 0) @@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting } } - while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) + while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) { start--; } @@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphClusters[0]; - var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + var firstCluster = GlyphInfos[0].GlyphCluster; + var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; @@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(first, second); } - int ICollection.Count => throw new NotImplementedException(); - - bool ICollection.IsReadOnly => true; + int IReadOnlyCollection.Count => GlyphInfos.Length; public GlyphInfo this[int index] { @@ -144,177 +133,18 @@ namespace Avalonia.Media.TextFormatting set => GlyphInfos[index] = value; } - int IList.IndexOf(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.Insert(int index, GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.RemoveAt(int index) - { - throw new NotImplementedException(); - } - - void ICollection.Add(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection.Contains(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.CopyTo(GlyphInfo[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection.Remove(GlyphInfo item) - { - throw new NotImplementedException(); - } public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - - private class CompareClusters : IComparer - { - private static readonly Comparer s_intClusterComparer = Comparer.Default; - - public int Compare(GlyphInfo x, GlyphInfo y) - { - return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); - } - } - - private readonly struct GlyphAdvanceList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphAdvanceList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public double this[int index] => _glyphInfos[index].GlyphAdvance; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphIndexList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphIndexList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public ushort this[int index] => _glyphInfos[index].GlyphIndex; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphClusterList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphClusterList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public int this[int index] => _glyphInfos[index].GlyphCluster; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphOffsetList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphOffsetList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public Vector this[int index] => _glyphInfos[index].GlyphOffset; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() { - GC.SuppressFinalize(this); - if (_bufferRented) + if (_rentedBuffer is not null) { - GlyphInfos.ReturnRent(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } - - ~ShapedBuffer() - { - if (_bufferRented) - { - GlyphInfos.ReturnRent(); - } - } - } - - public readonly record struct GlyphInfo - { - public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) - { - GlyphIndex = glyphIndex; - GlyphAdvance = glyphAdvance; - GlyphCluster = glyphCluster; - GlyphOffset = glyphOffset; - } - - /// - /// Get the glyph index. - /// - public ushort GlyphIndex { get; } - - /// - /// Get the glyph cluster. - /// - public int GlyphCluster { get; } - - /// - /// Get the glyph advance. - /// - public double GlyphAdvance { get; } - - /// - /// Get the glyph offset. - /// - public Vector GlyphOffset { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 583f2e49f1..7f23ac98b4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { ShapedBuffer = shapedBuffer; Properties = properties; - TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } @@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting public override Size Size => GlyphRun.Size; - public GlyphRun GlyphRun - { - get - { - if(_glyphRun is null) - { - _glyphRun = CreateGlyphRun(); - } - - return _glyphRun; - } - } + public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { - if (GlyphRun.GlyphIndices.Count == 0) + if (GlyphRun.GlyphInfos.Count == 0) { return; } @@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (width + advance > availableWidth) { @@ -195,11 +184,8 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, Text, - ShapedBuffer.GlyphIndices, - ShapedBuffer.GlyphAdvances, - ShapedBuffer.GlyphOffsets, - ShapedBuffer.GlyphClusters, - BidiLevel); + ShapedBuffer, + biDiLevel: BidiLevel); } public void Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 53021c4656..c1ac57ce46 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -26,5 +26,16 @@ /// The second part. /// public T? Second { get; } + + /// + /// Deconstructs the split results into its components. + /// + /// On return, contains the first part. + /// On return, contains the second part. + public void Deconstruct(out T first, out T? second) + { + first = First; + second = Second; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 2525f0dbf9..b4734d702b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + FontManager fontManager, ref TextRunProperties? previousProperties, RentedList results) { - var shapeableCharacters = new List(2); var properties = Properties; while (!text.IsEmpty) { - var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties); - shapeableCharacters.Add(shapeableRun); + results.Add(shapeableRun); text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } - - return shapeableCharacters; } /// @@ -72,37 +69,31 @@ namespace Avalonia.Media.TextFormatting /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. + /// The font manager to use. /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, - TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) + TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager, + ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; - var currentTypeface = defaultTypeface; + var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface; var previousTypeface = previousProperties?.Typeface; + var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) { - if (script == Script.Common && previousTypeface is not null) - { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) - { - return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); - } - } - - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } - if (previousTypeface is not null) + if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count)) { return new UnshapedTextRun(text.Slice(0, count), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } @@ -110,48 +101,47 @@ namespace Avalonia.Media.TextFormatting var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); - while (codepointEnumerator.MoveNext()) + while (codepointEnumerator.MoveNext(out var cp)) { - if (codepointEnumerator.Current.IsWhiteSpace) + if (cp.IsWhiteSpace) { continue; } - codepoint = codepointEnumerator.Current; + codepoint = cp; break; } //ToDo: Fix FontFamily fallback var matchFound = - FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, - out currentTypeface); - - if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) + out var fallbackTypeface); + + if (matchFound) { - //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), - biDiLevel); + // Fallback found + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } } // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(textSpan); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var grapheme)) { - var grapheme = enumerator.Current; - - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } - count += grapheme.Text.Length; + count += grapheme.Length; } return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); @@ -161,44 +151,40 @@ namespace Avalonia.Media.TextFormatting /// Tries to get a shapeable length that is supported by the specified typeface. /// /// The characters to shape. - /// The typeface that is used to find matching characters. - /// + /// The typeface that is used to find matching characters. + /// The default typeface. /// The shapeable length. - /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, - Typeface typeface, - Typeface? defaultTypeface, - out int length, - out Script script) + IGlyphTypeface glyphTypeface, + IGlyphTypeface? defaultGlyphTypeface, + out int length) { length = 0; - script = Script.Unknown; + var script = Script.Unknown; if (text.IsEmpty) { return false; } - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var currentGrapheme)) { - var currentGrapheme = enumerator.Current; - - var currentScript = currentGrapheme.FirstCodepoint.Script; + var currentCodepoint = currentGrapheme.FirstCodepoint; + var currentScript = currentCodepoint.Script; - if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsWhiteSpace + && defaultGlyphTypeface != null + && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } @@ -219,7 +205,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Text.Length; + length += currentGrapheme.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 01804e1ce3..72882df0b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract TextRun[]? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 528cd45581..4c93a1d851 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,15 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -22,7 +23,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return Array.Empty(); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -47,9 +48,9 @@ namespace Avalonia.Media.TextFormatting var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; + var nextBreakPosition = lineBreak.PositionMeasure; if (nextBreakPosition == 0) { @@ -70,18 +71,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -94,18 +84,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -121,5 +100,31 @@ namespace Avalonia.Media.TextFormatting return null; } + + private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, + TextRun shapedSymbol) + { + if (collapsedLength <= 0) + { + return new[] { shapedSymbol }; + } + + var objectPool = FormattingObjectPool.Instance; + + var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); + + try + { + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } + finally + { + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + } + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs index 0b5d7649d7..ff8c1c4860 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs @@ -38,7 +38,7 @@ /// 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, + public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8afecb09e2..7f74f49982 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,69 +1,86 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Buffers; using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { - internal class TextFormatterImpl : TextFormatter + internal sealed class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; + private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; + + [ThreadStatic] private static BidiData? t_bidiData; + [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { - var textWrapping = paragraphProperties.TextWrapping; - FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList textRuns; - - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, - out var textEndOfLine, out var textSourceLength); + var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; - if (previousLineBreak?.RemainingRuns != null) + // we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead + if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak + && wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns + && paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = previousLineBreak.RemainingRuns; - nextLineBreak = previousLineBreak; + return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth, + paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool); } - else + + RentedList? fetchedRuns = null; + RentedList? shapedTextRuns = null; + try { - textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); + fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, + out var textSourceLength); + + if (fetchedRuns.Count == 0) + { + return null; + } + + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, + out var resolvedFlowDirection); if (nextLineBreak == null && textEndOfLine != null) { nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); } - } - - TextLineImpl textLine; - switch (textWrapping) - { - case TextWrapping.NoWrap: + switch (paragraphProperties.TextWrapping) + { + case TextWrapping.NoWrap: { - textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, + var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, + textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - textLine.FinalizeLine(); + textLine.FinalizeLine(); - break; + return textLine; } - case TextWrapping.WrapWithOverflow: - case TextWrapping.Wrap: + case TextWrapping.WrapWithOverflow: + case TextWrapping.Wrap: { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, - resolvedFlowDirection, nextLineBreak); - break; + return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); } - default: - throw new ArgumentOutOfRangeException(nameof(textWrapping)); + default: + throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); + } + } + finally + { + objectPool.TextRunLists.Return(ref shapedTextRuns); + objectPool.TextRunLists.Return(ref fetchedRuns); } - - return textLine; } /// @@ -71,25 +88,27 @@ namespace Avalonia.Media.TextFormatting /// /// The text run's. /// The length to split at. + /// A pool used to get reusable formatting objects. /// The split text runs. - internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length, + FormattingObjectPool objectPool) { + var first = objectPool.TextRunLists.Rent(); var currentLength = 0; for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; + var currentRunLength = currentRun.Length; - if (currentLength + currentRun.Length < length) + if (currentLength + currentRunLength < length) { - currentLength += currentRun.Length; + currentLength += currentRunLength; continue; } - var firstCount = currentRun.Length >= 1 ? i + 1 : i; - - var first = new List(firstCount); + var firstCount = currentRunLength >= 1 ? i + 1 : i; if (firstCount > 1) { @@ -101,13 +120,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.Length == length) + if (currentLength + currentRunLength == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { - var offset = currentRun.Length >= 1 ? 1 : 0; + var offset = currentRunLength >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -117,13 +136,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = objectPool.TextRunLists.Rent(); if (currentRun is ShapedTextRun shapedTextCharacters) { @@ -139,11 +158,16 @@ namespace Avalonia.Media.TextFormatting second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + for (var i = 0; i < textRuns.Count; i++) + { + first.Add(textRuns[i]); + } + + return new SplitResult>(first, null); } /// @@ -152,90 +176,122 @@ namespace Avalonia.Media.TextFormatting /// The text runs to shape. /// The default paragraph properties. /// The resolved flow direction. + /// A pool used to get reusable formatting objects. + /// The font manager to use. /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, - out FlowDirection resolvedFlowDirection) + private static RentedList ShapeTextRuns(IReadOnlyList textRuns, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, + FontManager fontManager, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var shapedRuns = new List(); - using var biDiData = new BidiData((sbyte)flowDirection); + var shapedRuns = objectPool.TextRunLists.Rent(); - foreach (var textRun in textRuns) + if (textRuns.Count == 0) { - if (textRun.Text.IsEmpty) - { - biDiData.Append(new char[textRun.Length]); - } - else - { - biDiData.Append(textRun.Text.Span); - } + resolvedFlowDirection = flowDirection; + return shapedRuns; } - using var biDi = new BidiAlgorithm(); + var bidiData = t_bidiData ??= new(); + bidiData.Reset(); + bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; + + for (var i = 0; i < textRuns.Count; ++i) + { + var textRun = textRuns[i]; + + ReadOnlySpan text; + if (!textRun.Text.IsEmpty) + text = textRun.Text.Span; + else if (textRun.Length == TextRun.DefaultTextSourceLength) + text = s_defaultText; + else + text = new char[textRun.Length]; + + bidiData.Append(text); + } - biDi.Process(biDiData); + var bidiAlgorithm = t_bidiAlgorithm ??= new(); + bidiAlgorithm.Process(bidiData); - var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); + var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - var processedRuns = new List(textRuns.Count); - - CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); + var processedRuns = objectPool.TextRunLists.Rent(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); - for (var index = 0; index < processedRuns.Count; index++) + try { - var currentRun = processedRuns[index]; + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); - switch (currentRun) + bidiData.Reset(); + bidiAlgorithm.Reset(); + + + var textShaper = TextShaper.Current; + + for (var index = 0; index < processedRuns.Count; index++) { - case UnshapedTextRun shapeableRun: - { - var groupedRuns = new List(2) { shapeableRun }; - var text = shapeableRun.Text; + var currentRun = processedRuns[index]; - while (index + 1 < processedRuns.Count) + switch (currentRun) + { + case UnshapedTextRun shapeableRun: { - if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); + + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; + + while (index + 1 < processedRuns.Count) { + if (processedRuns[index + 1] is not UnshapedTextRun nextRun) + { + break; + } + + if (shapeableRun.BidiLevel == nextRun.BidiLevel + && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) + && CanShapeTogether(properties, nextRun.Properties)) + { + groupedRuns.Add(nextRun); + index++; + shapeableRun = nextRun; + text = joinedText; + continue; + } + break; } - if (shapeableRun.BidiLevel == nextRun.BidiLevel - && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) - { - groupedRuns.Add(nextRun); - index++; - shapeableRun = nextRun; - text = joinedText; - continue; - } + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + + ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); break; } + default: + { + shapedRuns.Add(currentRun); - var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - - shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); - - break; - } - default: - { - shapedRuns.Add(currentRun); - - break; - } + break; + } + } } } + finally + { + objectPool.TextRunLists.Return(ref processedRuns); + objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + } return shapedRuns; } @@ -303,18 +359,15 @@ namespace Avalonia.Media.TextFormatting } } - private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) + private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, + TextShaperOptions options, TextShaper textShaper, RentedList results) { - var shapedRuns = new List(textRuns.Count); - - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = textShaper.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) { @@ -322,12 +375,10 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); + results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } - - return shapedRuns; } /// @@ -335,10 +386,11 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. - /// + /// The font manager to use. + /// A list that will be filled with the processed runs. /// - private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, - List processedRuns) + private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, + FontManager fontManager, RentedList processedRuns) { if (levels.Length == 0) { @@ -385,7 +437,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runLevel = levels[levelIndex]; @@ -398,7 +451,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -415,7 +469,7 @@ namespace Avalonia.Media.TextFormatting return; } - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns); } /// @@ -423,34 +477,26 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. - /// - /// + /// A pool used to get reusable formatting objects. + /// On return, the end of line, if any. + /// On return, the processed text source length. /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, - out TextEndOfLine? endOfLine, out int textSourceLength) + private static RentedList FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) { textSourceLength = 0; endOfLine = null; - var textRuns = new List(); + var textRuns = objectPool.TextRunLists.Rent(); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); while (textRunEnumerator.MoveNext()) { - var textRun = textRunEnumerator.Current; - - if (textRun == null) - { - textRuns.Add(new TextEndOfParagraph()); - - textSourceLength += TextRun.DefaultTextSourceLength; - - break; - } + TextRun textRun = textRunEnumerator.Current!; if (textRun is TextEndOfLine textEndOfLine) { @@ -509,15 +555,13 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out lineBreak)) { - if (!lineBreakEnumerator.Current.Required) + if (!lineBreak.Required) { continue; } - lineBreak = lineBreakEnumerator.Current; - return lineBreak.PositionWrap >= textRun.Length || true; } @@ -529,8 +573,10 @@ namespace Avalonia.Media.TextFormatting measuredLength = 0; var currentWidth = 0.0; - foreach (var currentRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var currentRun = textRuns[i]; + switch (currentRun) { case ShapedTextRun shapedTextCharacters: @@ -540,15 +586,15 @@ namespace Avalonia.Media.TextFormatting var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; - for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) + for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; + var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { measuredLength += Math.Max(0, lastCluster - firstCluster); - goto found; + return measuredLength != 0; } lastCluster = glyphInfo.GlyphCluster; @@ -565,7 +611,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - goto found; + return measuredLength != 0; } measuredLength += currentRun.Length; @@ -582,8 +628,6 @@ namespace Avalonia.Media.TextFormatting } } - found: - return measuredLength != 0; } @@ -591,35 +635,40 @@ namespace Avalonia.Media.TextFormatting /// Creates an empty text line. /// /// The empty text line. - public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; + var glyphTypeface = properties.CachedGlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; + var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); + var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } /// /// Performs text wrapping returns a list of text lines. /// /// + /// Whether can be reused to store the split runs. /// The first text source index. /// The paragraph width. /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. + /// A pool used to get reusable formatting objects. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, - double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak) + private static TextLineImpl PerformTextWrapping(List textRuns, bool canReuseTextRunList, + int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, + FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { @@ -646,23 +695,23 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case ShapedTextRun: - { + { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - if (lineBreaker.Current.Required && - currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) + if (lineBreak.Required && + currentLength + lineBreak.PositionMeasure <= measuredLength) { //Explicit break found breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) + if (currentLength + lineBreak.PositionMeasure > measuredLength) { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { @@ -678,21 +727,21 @@ namespace Avalonia.Media.TextFormatting //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { //We already found the next possible wrap position. breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - while (lineBreaker.MoveNext() && index < textRuns.Count) + while (lineBreaker.MoveNext(out lineBreak)) { - currentPosition += lineBreaker.Current.PositionWrap; + currentPosition += lineBreak.PositionWrap; - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { break; } @@ -711,7 +760,12 @@ namespace Avalonia.Media.TextFormatting } else { - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; + } + + if (currentPosition == 0 && measuredLength > 0) + { + currentPosition = measuredLength; } breakFound = true; @@ -727,9 +781,9 @@ namespace Avalonia.Media.TextFormatting break; } - if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) + if (lineBreak.PositionMeasure != lineBreak.PositionWrap) { - lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; + lastWrapPosition = currentLength + lineBreak.PositionWrap; } } @@ -749,24 +803,56 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitTextRuns(textRuns, measuredLength); + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); - var remainingCharacters = splitResult.Second; + try + { + TextLineBreak? textLineBreak; + if (postSplitRuns?.Count > 0) + { + List remainingRuns; - var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : - null; + // reuse the list as much as possible: + // if canReuseTextRunList == true it's coming from previous remaining runs + if (canReuseTextRunList) + { + remainingRuns = textRuns; + remainingRuns.Clear(); + } + else + { + remainingRuns = new List(); + } - if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) - { - lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); - } + for (var i = 0; i < postSplitRuns.Count; ++i) + { + remainingRuns.Add(postSplitRuns[i]); + } - var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, - lineBreak); + textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns); + } + else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine) + { + textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } + else + { + textLineBreak = null; + } + + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, + textLineBreak); + + textLine.FinalizeLine(); - return textLine.FinalizeLine(); + return textLine; + } + finally + { + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + } } private struct TextRunEnumerator @@ -816,7 +902,7 @@ namespace Avalonia.Media.TextFormatting { var textShaper = TextShaper.Current; - var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; + var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 468623b356..4dbc472133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; + private readonly TextLine[] _textLines; private int _textSourceLength; @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text lines. /// - public IReadOnlyList TextLines { get; private set; } + public IReadOnlyList TextLines + => _textLines; /// /// Gets the bounds of the layout. @@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting /// The origin. public void Draw(DrawingContext context, Point origin) { - if (!TextLines.Any()) + if (_textLines.Length == 0) { return; } var (currentX, currentY) = origin; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); @@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting /// public Rect HitTestTextPosition(int textPosition) { - if (TextLines.Count == 0) + if (_textLines.Length == 0) { return new Rect(); } @@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { var end = textLine.FirstTextSourceIndex + textLine.Length; @@ -230,14 +231,14 @@ namespace Avalonia.Media.TextFormatting return Array.Empty(); } - var result = new List(TextLines.Count); + var result = new List(_textLines.Length); var currentY = 0d; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { //Current line isn't covered. - if (textLine.FirstTextSourceIndex + textLine.Length < start) + if (textLine.FirstTextSourceIndex + textLine.Length <= start) { currentY += textLine.Height; @@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting { var currentY = 0d; - var lineIndex = 0; TextLine? currentLine = null; CharacterHit characterHit; - for (; lineIndex < TextLines.Count; lineIndex++) + for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++) { - currentLine = TextLines[lineIndex]; + currentLine = _textLines[lineIndex]; if (currentY + currentLine.Height > point.Y) { @@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting if (charIndex > _textSourceLength) { - return TextLines.Count - 1; + return _textLines.Length - 1; } - for (var index = 0; index < TextLines.Count; index++) + for (var index = 0; index < _textLines.Length; index++) { - var textLine = TextLines[index]; + var textLine = _textLines[index]; if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) { @@ -341,21 +341,43 @@ namespace Avalonia.Media.TextFormatting } } - return TextLines.Count - 1; + return _textLines.Length - 1; } private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) { var (x, y) = point; - var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; - var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height; - if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + var lastTrailingIndex = 0; + + if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) { - lastTrailingIndex -= textLine.NewLineLength; + lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; + + if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex -= textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex -= textEndOfLine.Length; + } } + else + { + if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex += textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex += textEndOfLine.Length; + } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, - TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, + TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, double letterSpacing) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); @@ -416,140 +438,156 @@ namespace Avalonia.Media.TextFormatting width = lineWidth; } - if (left > textLine.Start) + var start = textLine.Start; + + if (left > start) { - left = textLine.Start; + left = start; } height += textLine.Height; } - private IReadOnlyList CreateTextLines() + private TextLine[] CreateTextLines() { + var objectPool = FormattingObjectPool.Instance; + if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); Bounds = new Rect(0, 0, 0, textLine.Height); - return new List { textLine }; + return new TextLine[] { textLine }; } - var textLines = new List(); + var textLines = objectPool.TextLines.Rent(); - double left = double.PositiveInfinity, width = 0.0, height = 0.0; + try + { + double left = double.PositiveInfinity, width = 0.0, height = 0.0; - _textSourceLength = 0; + _textSourceLength = 0; - TextLine? previousLine = null; + TextLine? previousLine = null; - while (true) - { - var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, - _paragraphProperties, previousLine?.TextLineBreak); + var textFormatter = TextFormatter.Current; - if(textLine == null || textLine.Length == 0) + while (true) { - if (previousLine != null && previousLine.NewLineLength > 0) + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, + _paragraphProperties, previousLine?.TextLineBreak); + + if (textLine is null) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); + if (previousLine != null && previousLine.NewLineLength > 0) + { + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, + _paragraphProperties); - textLines.Add(emptyTextLine); + textLines.Add(emptyTextLine); - UpdateBounds(emptyTextLine, ref left, ref width, ref height); - } + UpdateBounds(emptyTextLine, ref left, ref width, ref height); + } - break; - } + break; + } - _textSourceLength += textLine.Length; + _textSourceLength += textLine.Length; - //Fulfill max height constraint - if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) - { - if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) + //Fulfill max height constraint + if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) + && height + textLine.Height > MaxHeight) { - var collapsedLine = - previousLine.Collapse(GetCollapsingProperties(MaxWidth)); + if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) + { + var collapsedLine = + previousLine.Collapse(GetCollapsingProperties(MaxWidth)); - textLines[textLines.Count - 1] = collapsedLine; - } + textLines[textLines.Count - 1] = collapsedLine; + } - break; - } + break; + } - var hasOverflowed = textLine.HasOverflowed; + var hasOverflowed = textLine.HasOverflowed; - if (hasOverflowed && _textTrimming != TextTrimming.None) - { - textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); - } + if (hasOverflowed && _textTrimming != TextTrimming.None) + { + textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); + } - textLines.Add(textLine); + textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref width, ref height); + UpdateBounds(textLine, ref left, ref width, ref height); - previousLine = textLine; + previousLine = textLine; - //Fulfill max lines constraint - if (MaxLines > 0 && textLines.Count >= MaxLines) - { - if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + //Fulfill max lines constraint + if (MaxLines > 0 && textLines.Count >= MaxLines) { - textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + if (textLine.TextLineBreak is { IsSplit: true }) + { + textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + } + + break; } - break; + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + { + break; + } } - if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + if (textLines.Count == 0) { - break; - } - } - - //Make sure the TextLayout always contains at least on empty line - if (textLines.Count == 0) - { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); - textLines.Add(textLine); + textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref width, ref height); - } + UpdateBounds(textLine, ref left, ref width, ref height); + } - Bounds = new Rect(left, 0, width, height); + Bounds = new Rect(left, 0, width, height); - if (_paragraphProperties.TextAlignment == TextAlignment.Justify) - { - var whitespaceWidth = 0d; - - foreach (var line in textLines) + if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; + var whitespaceWidth = 0d; - if (lineWhitespaceWidth > whitespaceWidth) + for (var i = 0; i < textLines.Count; i++) { - whitespaceWidth = lineWhitespaceWidth; - } - } + var line = textLines[i]; + var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - var justificationWidth = width - whitespaceWidth; + if (lineWhitespaceWidth > whitespaceWidth) + { + whitespaceWidth = lineWhitespaceWidth; + } + } - if (justificationWidth > 0) - { - var justificationProperties = new InterWordJustification(justificationWidth); + var justificationWidth = width - whitespaceWidth; - for (var i = 0; i < textLines.Count - 1; i++) + if (justificationWidth > 0) { - var line = textLines[i]; + var justificationProperties = new InterWordJustification(justificationWidth); + + for (var i = 0; i < textLines.Count - 1; i++) + { + var line = textLines[i]; - line.Justify(justificationProperties); + line.Justify(justificationProperties); + } } } - } - return textLines; + return textLines.ToArray(); + } + finally + { + objectPool.TextLines.Return(ref textLines); + objectPool.VerifyAllReturned(); + } } /// @@ -569,7 +607,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - foreach (var line in TextLines) + foreach (var line in _textLines) { line.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index e30a0fe9f4..2e85b1e187 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,5 +1,7 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Collections.Generic; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return Array.Empty(); } // Overview of ellipsis structure @@ -75,55 +78,63 @@ namespace Avalonia.Media.TextFormatting { shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - var collapsedRuns = new List(textRuns.Count); - if (measuredLength > 0) { - IReadOnlyList? preSplitRuns = null; - IReadOnlyList? postSplitRuns; + var objectPool = FormattingObjectPool.Instance; - if (_prefixLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + var collapsedRuns = objectPool.TextRunLists.Rent(); - collapsedRuns.AddRange(splitResult.First); + RentedList? rentedPreSplitRuns = null; + RentedList? rentedPostSplitRuns = null; - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; - } - else + try { - postSplitRuns = textRuns; - } + IReadOnlyList? effectivePostSplitRuns; - collapsedRuns.Add(shapedSymbol); + if (_prefixLength > 0) + { + (rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRuns, Math.Min(_prefixLength, measuredLength), objectPool); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns; - } + effectivePostSplitRuns = rentedPostSplitRuns; + + foreach (var preSplitRun in rentedPreSplitRuns) + { + collapsedRuns.Add(preSplitRun); + } + } + else + { + effectivePostSplitRuns = textRuns; + } - var availableSuffixWidth = availableWidth; + collapsedRuns.Add(shapedSymbol); - if (preSplitRuns is not null) - { - foreach (var run in preSplitRuns) + if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) { - if (run is DrawableTextRun drawableTextRun) + return collapsedRuns.ToArray(); + } + + var availableSuffixWidth = availableWidth; + + if (rentedPreSplitRuns is not null) + { + foreach (var run in rentedPreSplitRuns) { - availableSuffixWidth -= drawableTextRun.Size.Width; + if (run is DrawableTextRun drawableTextRun) + { + availableSuffixWidth -= drawableTextRun.Size.Width; + } } } - } - - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; - switch (run) + for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--) { - case ShapedTextRun endShapedRun: + var run = effectivePostSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, out var suffixCount, out var suffixWidth)) @@ -141,15 +152,20 @@ namespace Avalonia.Media.TextFormatting break; } + } } + + return collapsedRuns.ToArray(); + } + finally + { + objectPool.TextRunLists.Return(ref rentedPreSplitRuns); + objectPool.TextRunLists.Return(ref rentedPostSplitRuns); + objectPool.TextRunLists.Return(ref collapsedRuns); } - } - else - { - collapsedRuns.Add(shapedSymbol); } - return collapsedRuns; + return new TextRun[] { shapedSymbol }; } availableWidth -= shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index bf26ac5df4..3b3464b46e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { public class TextLineBreak { - public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, - IReadOnlyList? remainingRuns = null) + public TextLineBreak(TextEndOfLine? textEndOfLine = null, + FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; - RemainingRuns = remainingRuns; + IsSplit = isSplit; } /// @@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting public FlowDirection FlowDirection { get; } /// - /// Get the remaining runs that were split up by the during the formatting process. + /// Gets whether there were remaining runs after this line break, + /// that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public bool IsSplit { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ab9686a34a..187b3154ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,25 +1,25 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - internal class TextLineImpl : TextLine + internal sealed class TextLineImpl : TextLine { - private IReadOnlyList _textRuns; + private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; + private TextLineBreak? _textLineBreak; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { FirstTextSourceIndex = firstTextSourceIndex; Length = length; - TextLineBreak = lineBreak; + _textLineBreak = lineBreak; HasCollapsed = hasCollapsed; _textRuns = textRuns; @@ -39,7 +39,7 @@ namespace Avalonia.Media.TextFormatting public override int Length { get; } /// - public override TextLineBreak? TextLineBreak { get; } + public override TextLineBreak? TextLineBreak => _textLineBreak; /// public override bool HasCollapsed { get; } @@ -147,7 +147,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, _resolvedFlowDirection, TextLineBreak, true); - if (collapsedRuns.Count > 0) + if (collapsedRuns.Length > 0) { collapsedLine.FinalizeLine(); } @@ -166,40 +166,56 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetCharacterHitFromDistance(double distance) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { - return new CharacterHit(); + return new CharacterHit(FirstTextSourceIndex); } distance -= Start; + var lastIndex = _textRuns.Length - 1; + + if (_textRuns[lastIndex] is TextEndOfLine) + { + lastIndex--; + } + + var currentPosition = FirstTextSourceIndex; + + if (lastIndex < 0) + { + return new CharacterHit(currentPosition); + } + if (distance <= 0) { var firstRun = _textRuns[0]; - return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft) + { + currentPosition = Length - firstRun.Length; + } + + return GetRunCharacterHit(firstRun, currentPosition, 0); } if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Count - 1]; + var lastRun = _textRuns[lastIndex]; - var size = 0.0; - - if (lastRun is DrawableTextRun drawableTextRun) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { - size = drawableTextRun.Size.Width; + currentPosition = Length - lastRun.Length; } - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); + return GetRunCharacterHit(lastRun, currentPosition, distance); } // process hit that happens within the line var characterHit = new CharacterHit(); - var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i <= lastIndex; i++) { var currentRun = _textRuns[i]; @@ -208,7 +224,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = i; currentPosition += currentRun.Length; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1) { var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; @@ -224,14 +240,14 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if (j > _textRuns.Count - 1) + if (j > _textRuns.Length - 1) { break; } currentRun = _textRuns[j]; - if(currentRun is not ShapedTextRun) + if (currentRun is not ShapedTextRun) { continue; } @@ -254,7 +270,7 @@ namespace Avalonia.Media.TextFormatting if (currentRun is DrawableTextRun drawableTextRun) { - if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance) { currentDistance += drawableTextRun.Size.Width; @@ -263,10 +279,6 @@ namespace Avalonia.Media.TextFormatting continue; } } - else - { - continue; - } break; } @@ -328,7 +340,7 @@ namespace Avalonia.Media.TextFormatting if (flowDirection == FlowDirection.LeftToRight) { - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { var currentRun = _textRuns[index]; @@ -338,7 +350,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftWidth = shapedRun.Size.Width; - while (i + 1 <= _textRuns.Count - 1) + while (i + 1 <= _textRuns.Length - 1) { var nextRun = _textRuns[i + 1]; @@ -402,7 +414,7 @@ namespace Avalonia.Media.TextFormatting { currentDistance += WidthIncludingTrailingWhitespace; - for (var index = _textRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { var currentRun = _textRuns[index]; @@ -411,10 +423,10 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - distance = currentGlyphRun.Size.Width - distance; + currentDistance -= currentGlyphRun.Size.Width; } - return Math.Max(0, currentDistance - distance); + return currentDistance + distance; } if (currentRun is DrawableTextRun drawableTextRun) @@ -502,7 +514,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -564,392 +576,463 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCaretCharacterHit(characterHit); } - private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - var characterIndex = firstTextSourceIndex + textLength; + if (_textRuns.Length == 0) + { + return Array.Empty(); + } - var result = new List(TextRuns.Count); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var result = new List(); var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start; - double currentWidth = 0; - var currentRect = default(Rect); - - TextRunBounds lastRunBounds = default; - - for (var index = 0; index < TextRuns.Count; index++) + static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (textRun is ShapedTextRun shapedTextRun) { - continue; + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; } - var characterLength = 0; - var endX = startX; - - TextRunBounds currentRunBounds; + return currentDirection; + } - double combinedWidth; + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) + { + var currentX = Start; - if (currentRun is ShapedTextRun currentShapedRun) + for (int i = 0; i < _textRuns.Length; i++) { - var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; - - if (currentPosition + currentRun.Length <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; + var currentRun = _textRuns[i]; - currentPosition += currentRun.Length; + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight); + var directionalWidth = 0.0; - continue; + if (currentRun is DrawableTextRun currentDrawable) + { + directionalWidth = currentDrawable.Size.Width; } - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + // Find consecutive runs of same direction + for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) { - var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); + var nextRun = _textRuns[lastRunIndex + 1]; - double startOffset; + var nextDirection = GetDirection(nextRun, currentDirection); - double endOffset; - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + if (currentDirection != nextDirection) + { + break; + } - startX += startOffset; + if (nextRun is DrawableTextRun nextDrawable) + { + directionalWidth += nextDrawable.Size.Width; + } + } - endX += endOffset; + //Skip runs that are not part of the hit test range + switch (currentDirection) + { + case FlowDirection.RightToLeft: + { + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + currentPosition += currentRun.Length; - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + if (currentRun is DrawableTextRun drawableTextRun) + { + directionalWidth -= drawableTextRun.Size.Width; + currentX += drawableTextRun.Size.Width; + } - currentDirection = FlowDirection.LeftToRight; - } - else - { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; + if(lastRunIndex - 1 < 0) + { + break; + } + } - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) - { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) - { break; } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; + + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - rightToLeftIndex++; + currentPosition += currentRun.Length; - rightToLeftWidth += nextShapedRun.Size.Width; + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } + + if(firstRunIndex + 1 == _textRuns.Length) + { + break; + } + } - if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) - { break; } + } - currentShapedRun = nextShapedRun; - } - - startX += rightToLeftWidth; + i = lastRunIndex; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (directionalWidth == 0) + { + continue; + } - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + var coveredLength = 0; + TextBounds? textBounds = null; - var rightToLeftRunBounds = new List { currentRunBounds }; + switch (currentDirection) + { - for (int i = rightToLeftIndex - 1; i >= index; i--) - { - if (TextRuns[i] is not ShapedTextRun) + case FlowDirection.RightToLeft: { - continue; + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX += directionalWidth; + + break; } + default: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - currentShapedRun = (ShapedTextRun)TextRuns[i]; + currentX = textBounds.Rectangle.Right; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + break; + } + } - rightToLeftRunBounds.Insert(0, currentRunBounds); + if (coveredLength > 0) + { + result.Add(textBounds); - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + remainingLength -= coveredLength; + } - currentPosition += currentRunBounds.Length; - } + if (remainingLength <= 0) + { + break; + } + } + } + else + { + var currentX = Start + WidthIncludingTrailingWhitespace; - combinedWidth = endX - startX; + for (int i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft); + var directionalWidth = 0.0; - currentRect = new Rect(startX, 0, combinedWidth, Height); + if (currentRun is DrawableTextRun currentDrawable) + { + directionalWidth = currentDrawable.Size.Width; + } - currentDirection = FlowDirection.RightToLeft; + // Find consecutive runs of same direction + for (; firstRunIndex - 1 > 0; firstRunIndex--) + { + var previousRun = _textRuns[firstRunIndex - 1]; + + var previousDirection = GetDirection(previousRun, currentDirection); - if (!MathUtilities.IsZero(combinedWidth)) + if (currentDirection != previousDirection) { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); + break; } - startX = endX; + if (currentRun is DrawableTextRun previousDrawable) + { + directionalWidth += previousDrawable.Size.Width; + } } - } - else - { - if (currentPosition + currentRun.Length <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - currentPosition += currentRun.Length; + //Skip runs that are not part of the hit test range + switch (currentDirection) + { + case FlowDirection.RightToLeft: + { + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - continue; - } + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - if (currentPosition < firstTextSourceIndex) - { - startX += currentRun.Size.Width; - } + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX -= drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - if (currentPosition + currentRun.Length <= characterIndex) - { - endX += currentRun.Size.Width; + continue; + } - characterLength = currentRun.Length; - } - } + break; + } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - combinedWidth = endX - startX; + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + continue; + } - currentPosition += characterLength; + break; + } - remainingLength -= characterLength; + break; + } + } - startX = endX; + i = firstRunIndex; - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + if (directionalWidth == 0) { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + continue; + } - var textBounds = result[result.Count - 1]; + var coveredLength = 0; - textBounds.Rectangle = currentRect; + TextBounds? textBounds = null; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else + switch (currentDirection) { - currentRect = currentRunBounds.Rectangle; + case FlowDirection.LeftToRight: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX -= directionalWidth; + + break; + } + default: + { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + currentX = textBounds.Rectangle.Left; + + break; + } } - } - lastRunBounds = currentRunBounds; + //Visual order is always left to right so we need to insert + result.Insert(0, textBounds); - currentWidth += combinedWidth; + remainingLength -= coveredLength; - if (remainingLength <= 0 || currentPosition >= characterIndex) - { - break; + if (remainingLength <= 0) + { + break; + } } - - lastDirection = currentDirection; } return result; } - private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength) + private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) { - var characterIndex = firstTextSourceIndex + textLength; - - var result = new List(TextRuns.Count); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; - - var currentPosition = FirstTextSourceIndex; - var remainingLength = textLength; - - var startX = WidthIncludingTrailingWhitespace; - double currentWidth = 0; - var currentRect = default(Rect); + coveredLength = 0; + var textRunBounds = new List(); + var startX = endX; - for (var index = TextRuns.Count - 1; index >= 0; index--) + for (int i = lastRunIndex; i >= firstRunIndex; i--) { - if (TextRuns[index] is not DrawableTextRun currentRun) - { - continue; - } - - if (currentPosition + currentRun.Length < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; - - currentPosition += currentRun.Length; - - continue; - } - - var characterLength = 0; - var endX = startX; + var currentRun = _textRuns[i]; - if (currentRun is ShapedTextRun currentShapedRun) + if (currentRun is ShapedTextRun shapedTextRun) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; + var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - var startIndex = currentPosition; - double startOffset; - double endOffset; + textRunBounds.Insert(0, runBounds); - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + if (offset > 0) { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + endX = runBounds.Rectangle.Right; - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + startX = endX; } - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; + startX -= runBounds.Rectangle.Width; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + currentPosition += runBounds.Length + offset; - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + coveredLength += runBounds.Length; - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; + remainingLength -= runBounds.Length; } else { - if (currentPosition + currentRun.Length <= characterIndex) + if (currentRun is DrawableTextRun drawableTextRun) { - endX -= currentRun.Size.Width; + startX -= drawableTextRun.Size.Width; + + textRunBounds.Insert(0, + new TextRunBounds( + new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += currentRun.Length; - characterLength = currentRun.Length; - } - } + coveredLength += currentRun.Length; - if (endX < startX) - { - (endX, startX) = (startX, endX); + remainingLength -= currentRun.Length; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (remainingLength <= 0) { - characterLength = NewLineLength; + break; } + } - var runWidth = endX - startX; + newPosition = currentPosition; - var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + var runWidth = endX - startX; + + var bounds = new Rect(startX, 0, runWidth, Height); - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) + return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds); + } + + private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var endX = startX; + + for (int i = firstRunIndex; i <= lastRunIndex; i++) + { + var currentRun = _textRuns[i]; + + if (currentRun is ShapedTextRun shapedTextRun) { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) - { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - var textBounds = result[result.Count - 1]; + textRunBounds.Add(runBounds); - textBounds.Rectangle = currentRect; + if (offset > 0) + { + startX = runBounds.Rectangle.Left; - textBounds.TextRunBounds.Add(currentRunBounds); + endX = startX; } - else + + currentPosition += runBounds.Length + offset; + + endX += runBounds.Rectangle.Width; + + coveredLength += runBounds.Length; + + remainingLength -= runBounds.Length; + } + else + { + if (currentRun is DrawableTextRun drawableTextRun) { - currentRect = currentRunBounds.Rectangle; + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + endX += drawableTextRun.Size.Width; } - } - currentWidth += runWidth; - currentPosition += characterLength; + currentPosition += currentRun.Length; + + coveredLength += currentRun.Length; - if (currentPosition > characterIndex) - { - break; + remainingLength -= currentRun.Length; } - lastDirection = currentDirection; - remainingLength -= characterLength; - if (remainingLength <= 0) { break; } } - result.Reverse(); - - return result; - } + newPosition = currentPosition; - private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) - { - var startX = endX; + var runWidth = endX - startX; - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var bounds = new Rect(startX, 0, runWidth, Height); - currentPosition += offset; + return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds); + } + private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + { var startIndex = currentPosition; - double startOffset; - double endOffset; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + + var endX = startX + endOffset; + startX += startOffset; - var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); @@ -966,210 +1049,78 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); - } - - public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) - { - if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) - { - return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength); - } - - return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - public override void Dispose() + private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) { - for (int i = 0; i < _textRuns.Count; i++) - { - if (_textRuns[i] is ShapedTextRun shapedTextRun) - { - shapedTextRun.Dispose(); - } - } - } + var startX = endX; - public TextLineImpl FinalizeLine() - { - _textLineMetrics = CreateLineMetrics(); + var startIndex = currentPosition; - BidiReorder(); + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - return this; - } + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) - { - if (run is ShapedTextRun shapedTextCharacters) + if (currentPosition != firstCluster) { - return shapedTextCharacters.BidiLevel; + startIndex = firstCluster + offset; } - - var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; - - return (sbyte)defaultLevel; - } - - private void BidiReorder() - { - if (_textRuns.Count == 0) + else { - return; + startIndex += offset; } - // Build up the collection of ordered runs. - var run = _textRuns[0]; - - OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - var current = orderedRun; + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - for (var i = 1; i < _textRuns.Count; i++) - { - run = _textRuns[i]; - - current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection)); + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - current = current.Next; - } + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; - // Reorder them into visual order. - orderedRun = LinearReOrder(orderedRun); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - // Now perform a recursive reversal of each run. - // From the highest level found in the text to the lowest odd level on each line, including intermediate levels - // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. - // https://unicode.org/reports/tr9/#L2 - sbyte max = 0; - var min = sbyte.MaxValue; + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - for (var i = 0; i < _textRuns.Count; i++) + if (endX < startX) { - var currentRun = _textRuns[i]; - - var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection); - - if (level > max) - { - max = level; - } - - if ((level & 1) != 0 && level < min) - { - min = level; - } + (endX, startX) = (startX, endX); } - if (min > max) + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) { - min = max; + characterLength = NewLineLength; } - if (max == 0 || (min == max && (max & 1) == 0)) - { - // Nothing to reverse. - return; - } + var runWidth = endX - startX; - // Now apply the reversal and replace the original contents. - var minLevelToReverse = max; + return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + } - while (minLevelToReverse >= min) + public override void Dispose() + { + for (int i = 0; i < _textRuns.Length; i++) { - current = orderedRun; - - while (current != null) + if (_textRuns[i] is ShapedTextRun shapedTextRun) { - if (current.Level >= minLevelToReverse && current.Level % 2 != 0) - { - if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) - { - shapedTextCharacters.Reverse(); - } - } - - current = current.Next; + shapedTextRun.Dispose(); } - - minLevelToReverse--; } - - var textRuns = new List(_textRuns.Count); - - current = orderedRun; - - while (current != null) - { - textRuns.Add(current.Run); - - current = current.Next; - } - - _textRuns = textRuns; } - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run) + public void FinalizeLine() { - BidiRange? range = null; - - while (run != null) - { - var next = run.Next; - - while (range != null && range.Level > run.Level - && range.Previous != null && range.Previous.Level >= run.Level) - { - range = BidiRange.MergeWithPrevious(range); - } - - if (range != null && range.Level >= run.Level) - { - // Attach run to the range. - if ((run.Level & 1) != 0) - { - // Odd, range goes to the right of run. - run.Next = range.Left; - range.Left = run; - } - else - { - // Even, range goes to the left of run. - range.Right!.Next = run; - range.Right = run; - } - - range.Level = run.Level; - } - else - { - var r = new BidiRange(); - - r.Left = r.Right = run; - r.Level = run.Level; - r.Previous = range; - - range = r; - } - - run = next; - } + _textLineMetrics = CreateLineMetrics(); - while (range?.Previous != null) + if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) { - range = BidiRange.MergeWithPrevious(range); + _textLineBreak = new TextLineBreak(textEndOfLine); } - // Terminate. - range!.Right!.Next = null; - - return range.Left!; + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } /// @@ -1197,7 +1148,7 @@ namespace Avalonia.Media.TextFormatting var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1346,7 +1297,7 @@ namespace Avalonia.Media.TextFormatting textPosition = FirstTextSourceIndex; TextRun? previousRun = null; - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1395,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting } } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1411,7 +1362,7 @@ namespace Avalonia.Media.TextFormatting return runIndex; } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1432,7 +1383,7 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; + var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; @@ -1448,25 +1399,24 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Count - 1; + var lastRunIndex = _textRuns.Length - 1; if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) { case ShapedTextRun textRun: { - var textMetrics = - new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); + var textMetrics = textRun.TextMetrics; - if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) + if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) { - fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; + fontRenderingEmSize = textMetrics.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { @@ -1493,10 +1443,10 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewLineLength; + newLineLength += textRun.GlyphRun.Metrics.NewLineLength; } - widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; + widthIncludingWhitespace += textRun.Size.Width; break; } @@ -1505,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - switch (_paragraphProperties.FlowDirection) + if (index == lastRunIndex) { - case FlowDirection.LeftToRight: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } + width = widthIncludingWhitespace; + trailingWhitespaceLength = 0; } if (drawableTextRun.Size.Height > height) @@ -1620,59 +1549,5 @@ namespace Avalonia.Media.TextFormatting return 0; } } - - private sealed class OrderedBidiRun - { - public OrderedBidiRun(TextRun run, sbyte level) - { - Run = run; - Level = level; - } - - public sbyte Level { get; } - - public TextRun Run { get; } - - public OrderedBidiRun? Next { get; set; } - } - - private sealed class BidiRange - { - public int Level { get; set; } - - public OrderedBidiRun? Left { get; set; } - - public OrderedBidiRun? Right { get; set; } - - public BidiRange? Previous { get; set; } - - public static BidiRange MergeWithPrevious(BidiRange range) - { - var previous = range.Previous; - - BidiRange left; - BidiRange right; - - if ((previous!.Level & 1) != 0) - { - // Odd, previous goes to the right of range. - left = range; - right = previous; - } - else - { - // Even, previous goes to the left of range. - left = previous; - right = range; - } - - // Stitch them - left.Right!.Next = right.Left; - previous.Left = left.Left; - previous.Right = right.Right; - - return previous; - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 86b701cb4b..1622bc3b6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting /// public abstract class TextRunProperties : IEquatable { + private IGlyphTypeface? _cachedGlyphTypeFace; + /// /// Run typeface /// @@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting /// public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; + internal IGlyphTypeface CachedGlyphTypeface + => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface; + public bool Equals(TextRunProperties? other) { if (ReferenceEquals(null, other)) @@ -93,6 +98,9 @@ namespace Avalonia.Media.TextFormatting internal TextRunProperties WithTypeface(Typeface typeface) { + if (this is GenericTextRunProperties other && other.Typeface == typeface) + return this; + return new GenericTextRunProperties(typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index deecbbe476..ccae99cc75 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A collapsing properties to collapse whole line toward the end @@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c291e1dfb9..c622c76a60 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end @@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 100d381afe..3406432ce7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -5,8 +5,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; -using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -28,8 +26,13 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal struct BidiAlgorithm : IDisposable + internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -67,7 +70,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private BidiDictionary? _isolatePairs; + private readonly BidiDictionary _isolatePairs = new(); /// /// The working BiDi classes @@ -98,7 +101,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly Stack _statusStack = new Stack(); + private readonly Stack _statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +111,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly List _levelRuns = new(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +122,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private Stack? _pendingIsolateOpenings; + private readonly Stack _pendingIsolateOpenings = new(); /// /// The level of the isolating run currently being processed @@ -175,12 +178,12 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly List _pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly List _pairedBrackets = new(); /// /// Initializes a new instance of the class. @@ -228,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs?.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -324,7 +326,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) + if (_isolatePairs.TryGetValue(data.Start + i, out i)) { i -= data.Start; } @@ -341,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode return 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIsolateStart(BidiClass type) + { + const uint mask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate); + + return ((1U << (int)type) & mask) != 0U; + } + /// /// Build a list of matching isolates for a directionality slice /// Implements BD9 @@ -359,7 +372,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings?.Clear(); + _pendingIsolateOpenings.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -371,16 +384,14 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { - _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings?.Count > 0) + if (_pendingIsolateOpenings.Count > 0) { - _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -501,7 +512,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastType = _workingClasses[lastCharIndex]; int nextLevel; - switch (lastType) + if (IsIsolateStart(lastType)) + { + nextLevel = _paragraphEmbeddingLevel; + } + else { - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: + i = lastCharIndex + 1; + while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) { - nextLevel = _paragraphEmbeddingLevel; - - break; + i++; } - default: - { - i = lastCharIndex + 1; - while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) - { - i++; - } - nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; - - break; - } + nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; } var eos = DirectionFromLevel(Math.Max(nextLevel, level)); @@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // PDI and concatenate that run to this one var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; - if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) + if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -855,87 +856,73 @@ namespace Avalonia.Media.TextFormatting.Unicode private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) { // Create mappings onto the underlying data - _runResolvedClasses = new MappedArraySlice(_workingClasses, _isolatedRunMapping.AsSlice()); - _runOriginalClasses = new MappedArraySlice(_originalClasses, _isolatedRunMapping.AsSlice()); - _runLevels = new MappedArraySlice(_resolvedLevels, _isolatedRunMapping.AsSlice()); + var isolatedRunMapping = _isolatedRunMapping.AsSlice(); + _runResolvedClasses = new MappedArraySlice(_workingClasses, isolatedRunMapping); + _runOriginalClasses = new MappedArraySlice(_originalClasses, isolatedRunMapping); + _runLevels = new MappedArraySlice(_resolvedLevels, isolatedRunMapping); if (_hasBrackets) { - _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); - _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice()); + _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, isolatedRunMapping); + _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, isolatedRunMapping); } _runLevel = runLevel; _runDirection = DirectionFromLevel(runLevel); _runLength = _runResolvedClasses.Length; - // By tracking the types of characters known to be in the current run, we can - // skip some of the rules that we know won't apply. The flags will be - // initialized while we're processing rule W1 below. - var hasEN = false; - var hasAL = false; - var hasES = false; - var hasCS = false; - var hasAN = false; - var hasET = false; - // Rule W1 // Also, set hasXX flags int i; var previousClass = sos; + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + const uint wRulesMask = + (1U << (int)BidiClass.EuropeanNumber) | + (1U << (int)BidiClass.ArabicLetter) | + (1U << (int)BidiClass.EuropeanSeparator) | + (1U << (int)BidiClass.CommonSeparator) | + (1U << (int)BidiClass.ArabicNumber) | + (1U << (int)BidiClass.EuropeanTerminator); + + uint wRules = 0; + for (i = 0; i < _runLength; i++) { var resolvedClass = _runResolvedClasses[i]; - - switch (resolvedClass) - { - case BidiClass.NonspacingMark: - _runResolvedClasses[i] = previousClass; - break; - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: + if (resolvedClass == BidiClass.NonspacingMark) + { + _runResolvedClasses[i] = previousClass; + } + else + { + var classBit = 1U << (int)resolvedClass; + if ((classBit & isolateMask) != 0U) + { previousClass = BidiClass.OtherNeutral; - break; - - case BidiClass.EuropeanNumber: - hasEN = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicLetter: - hasAL = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanSeparator: - hasES = true; - previousClass = resolvedClass; - break; - - case BidiClass.CommonSeparator: - hasCS = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicNumber: - hasAN = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanTerminator: - hasET = true; - previousClass = resolvedClass; - break; - - default: + } + else + { + wRules |= classBit & wRulesMask; previousClass = resolvedClass; - break; + } } } + // By tracking the types of characters known to be in the current run, we can + // skip some of the rules that we know won't apply. + var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U; + var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U; + var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U; + var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U; + var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U; + var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U; + // Rule W2 if (hasEN) { @@ -1547,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhitespace(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - case BidiClass.BoundaryNeutral: - case BidiClass.WhiteSpace: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate) | + (1U << (int)BidiClass.BoundaryNeutral) | + (1U << (int)BidiClass.WhiteSpace); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1584,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsRemovedByX9(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.BoundaryNeutral: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.BoundaryNeutral); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1604,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsNeutralClass(BidiClass direction) { - switch (direction) - { - case BidiClass.ParagraphSeparator: - case BidiClass.SegmentSeparator: - case BidiClass.WhiteSpace: - case BidiClass.OtherNeutral: - case BidiClass.RightToLeftIsolate: - case BidiClass.LeftToRightIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.ParagraphSeparator) | + (1U << (int)BidiClass.SegmentSeparator) | + (1U << (int)BidiClass.WhiteSpace) | + (1U << (int)BidiClass.OtherNeutral) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + return ((1U << (int)direction) & mask) != 0U; } /// @@ -1642,6 +1620,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// @@ -1717,13 +1736,5 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } - - public void Dispose() - { - _workingClassesBuffer.Dispose(); - _resolvedLevelsBuffer.Dispose(); - _x9Map.Dispose(); - _isolatedRunMapping.Dispose(); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0f0b3235e1..b8094056f2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,8 +11,10 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal struct BidiData : IDisposable + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -20,12 +22,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private ArrayBuilder _savedPairedBracketTypes; private ArrayBuilder _tempLevelBuffer; - public BidiData(sbyte paragraphEmbeddingLevel) - { - ParagraphEmbeddingLevel = paragraphEmbeddingLevel; - } - - public sbyte ParagraphEmbeddingLevel { get; private set; } + public sbyte ParagraphEmbeddingLevel { get; set; } public bool HasBrackets { get; private set; } @@ -36,7 +33,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the length of the data held by the BidiData /// - public int Length{get; private set; } + public int Length { get; private set; } /// /// Gets the bidi character type of each code point @@ -66,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -74,39 +73,32 @@ namespace Avalonia.Media.TextFormatting.Unicode // bracket values for all code points int i = Length; + + const uint embeddingMask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat); + + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); var codePointEnumerator = new CodepointEnumerator(text); - while (codePointEnumerator.MoveNext()) + while (codePointEnumerator.MoveNext(out var codepoint)) { - var codepoint = codePointEnumerator.Current; - // Look up BiDiClass var dir = codepoint.BiDiClass; _classes[i] = dir; - switch (dir) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftEmbedding: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - { - HasEmbeddings = true; - break; - } - - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - { - HasIsolates = true; - break; - } - } + var dirBit = 1U << (int)dir; + HasEmbeddings = (dirBit & embeddingMask) != 0U; + HasIsolates = (dirBit & isolateMask) != 0U; // Lookup paired bracket types var pbt = codepoint.PairedBracketType; @@ -151,6 +143,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void SaveTypes() { + _hasCleanState = false; + // Capture the types data _savedClasses.Clear(); _savedClasses.Add(_classes.AsSlice()); @@ -163,6 +157,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void RestoreTypes() { + _hasCleanState = false; + _classes.Clear(); _classes.Add(_savedClasses.AsSlice()); _pairedBracketTypes.Clear(); @@ -182,14 +178,34 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } - public void Dispose() + /// + /// Resets the bidi data to a clean state. + /// + public void Reset() { - _classes.Dispose(); - _pairedBracketTypes.Dispose(); - _pairedBracketValues.Dispose(); - _savedClasses.Dispose(); - _savedPairedBracketTypes.Dispose(); - _tempLevelBuffer.Dispose(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); + + ParagraphEmbeddingLevel = 0; + HasBrackets = false; + HasEmbeddings = false; + HasIsolates = false; + Length = 0; + + Classes = default; + PairedBracketTypes = default; + PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 22f7b50fd4..23a1e4a275 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode @@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The replacement codepoint that is used for non supported values. /// - public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); - - public Codepoint(uint value) + public static Codepoint ReplacementCodepoint { - _value = value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new('\uFFFD'); } + /// + /// Creates a new instance of with the specified value. + /// + /// The codepoint value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Codepoint(uint value) => _value = value; + /// /// Get the codepoint's value. /// @@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public bool IsWhiteSpace { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - switch (GeneralCategory) - { - case GeneralCategory.Control: - case GeneralCategory.NonspacingMark: - case GeneralCategory.Format: - case GeneralCategory.SpaceSeparator: - case GeneralCategory.SpacingMark: - return true; - } - - return false; + const ulong whiteSpaceMask = + (1UL << (int)GeneralCategory.Control) | + (1UL << (int)GeneralCategory.NonspacingMark) | + (1UL << (int)GeneralCategory.Format) | + (1UL << (int)GeneralCategory.SpaceSeparator) | + (1UL << (int)GeneralCategory.SpacingMark); + + return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL; } } @@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) { + // Perf note: this method is performance critical for text layout, modify with care! + count = 1; - if (index >= text.Length) + // Perf note: uint check allows the JIT to ellide the next bound check + if ((uint)index >= (uint)text.Length) { return ReplacementCodepoint; } - var code = text[index]; - - ushort hi, low; + uint code = text[index]; - //# High surrogate - if (0xD800 <= code && code <= 0xDBFF) + //# Surrogate + if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU)) { - hi = code; - - if (index + 1 == text.Length) - { - return ReplacementCodepoint; - } - - low = text[index + 1]; - - if (0xDC00 <= low && low <= 0xDFFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } + uint hi, low; - //# Low surrogate - if (0xDC00 <= code && code <= 0xDFFF) - { - if (index == 0) + //# High surrogate + if (code <= 0xDBFF) { - return ReplacementCodepoint; + if ((uint)(index + 1) < (uint)text.Length) + { + hi = code; + low = text[index + 1]; + + if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU)) + { + count = 2; + // Perf note: the code is written as below to become just two instructions: shl, lea. + // See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38 + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } - hi = text[index - 1]; - - low = code; - - if (0xD800 <= hi && hi <= 0xDBFF) + //# Low surrogate + else { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); + if (index > 0) + { + low = code; + hi = text[index - 1]; + + if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU)) + { + count = 2; + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } return ReplacementCodepoint; @@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode return new Codepoint(code); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => value - lowerBound <= upperBound - lowerBound; + /// /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound) - => (cp._value - lowerBound) <= (upperBound - lowerBound); + => IsInRangeInclusive(cp._value, lowerBound, upperBound); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index d21f30ab7e..47a2b7d46a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _offset; public CodepointEnumerator(ReadOnlySpan text) - { - _text = text; - Current = Codepoint.ReplacementCodepoint; - } - - /// - /// Gets the current . - /// - public Codepoint Current { get; private set; } + => _text = text; /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Codepoint codepoint) { - if (_text.IsEmpty) + if ((uint)_offset >= (uint)_text.Length) { - Current = Codepoint.ReplacementCodepoint; - + codepoint = Codepoint.ReplacementCodepoint; return false; } - Current = Codepoint.ReadAt(_text, 0, out var count); + codepoint = Codepoint.ReadAt(_text, _offset, out var count); - _text = _text.Slice(count); + _offset += count; return true; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index fa8e8ac976..fcc12d3526 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,16 +1,15 @@ -using System; - -namespace Avalonia.Media.TextFormatting.Unicode +namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) + public Grapheme(Codepoint firstCodepoint, int offset, int length) { FirstCodepoint = firstCodepoint; - Text = text; + Offset = offset; + Length = length; } /// @@ -19,8 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The text of the grapheme cluster + /// Gets the starting code unit offset of this grapheme inside its containing text. + /// + public int Offset { get; } + + /// + /// Gets the length of this grapheme, in code units. /// - public ReadOnlySpan Text { get; } + public int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 812bb99d99..dd01662155 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -4,57 +4,79 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _currentCodeUnitOffset; + private int _codeUnitLengthOfCurrentCodepoint; + private Codepoint _currentCodepoint; + + /// + /// Will be if invalid data or EOF reached. + /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. + /// + private GraphemeBreakClass _currentType; public GraphemeEnumerator(ReadOnlySpan text) { _text = text; - Current = default; + _currentCodeUnitOffset = 0; + _codeUnitLengthOfCurrentCodepoint = 0; + _currentCodepoint = Codepoint.ReplacementCodepoint; + _currentType = GraphemeBreakClass.Other; } - /// - /// Gets the current . - /// - public Grapheme Current { get; private set; } - /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Grapheme grapheme) { - if (_text.IsEmpty) + var startOffset = _currentCodeUnitOffset; + + if ((uint)startOffset >= (uint)_text.Length) { + grapheme = default; return false; } // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. - var processor = new Processor(_text); - - processor.MoveNext(); + if (startOffset == 0) + { + ReadNextCodepoint(); + } - var firstCodepoint = processor.CurrentCodepoint; + var firstCodepoint = _currentCodepoint; // First, consume as many Prepend scalars as we can (rule GB9b). - while (processor.CurrentType == GraphemeBreakClass.Prepend) + if (_currentType == GraphemeBreakClass.Prepend) { - processor.MoveNext(); + do + { + ReadNextCodepoint(); + } while (_currentType == GraphemeBreakClass.Prepend); + + // There were only Prepend scalars in the text + if ((uint)_currentCodeUnitOffset >= (uint)_text.Length) + { + goto Return; + } } // Next, make sure we're not about to violate control character restrictions. // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). - if (processor.CurrentCodeUnitOffset > 0) + if (_currentCodeUnitOffset > startOffset) { - if (processor.CurrentType == GraphemeBreakClass.Control - || processor.CurrentType == GraphemeBreakClass.CR - || processor.CurrentType == GraphemeBreakClass.LF) + const uint controlCrLfMask = + (1U << (int)GraphemeBreakClass.Control) | + (1U << (int)GraphemeBreakClass.CR) | + (1U << (int)GraphemeBreakClass.LF); + + if (((1U << (int)_currentType) & controlCrLfMask) != 0U) { goto Return; } @@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode // Now begin the main state machine. - var previousClusterBreakType = processor.CurrentType; + var previousClusterBreakType = _currentType; - processor.MoveNext(); + ReadNextCodepoint(); switch (previousClusterBreakType) { case GraphemeBreakClass.CR: - if (processor.CurrentType != GraphemeBreakClass.LF) + if (_currentType != GraphemeBreakClass.LF) { goto Return; // rules GB3 & GB4 (only can follow ) } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.LF; case GraphemeBreakClass.Control: @@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode goto Return; // rule GB4 (no data after Control | LF) case GraphemeBreakClass.L: - if (processor.CurrentType == GraphemeBreakClass.L) + { + if (_currentType == GraphemeBreakClass.L) { - processor.MoveNext(); // rule GB6 (L x L) + ReadNextCodepoint(); // rule GB6 (L x L) goto case GraphemeBreakClass.L; } - else if (processor.CurrentType == GraphemeBreakClass.V) + else if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB6 (L x V) + ReadNextCodepoint(); // rule GB6 (L x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.LV) + else if (_currentType == GraphemeBreakClass.LV) { - processor.MoveNext(); // rule GB6 (L x LV) + ReadNextCodepoint(); // rule GB6 (L x LV) goto case GraphemeBreakClass.LV; } - else if (processor.CurrentType == GraphemeBreakClass.LVT) + else if (_currentType == GraphemeBreakClass.LVT) { - processor.MoveNext(); // rule GB6 (L x LVT) + ReadNextCodepoint(); // rule GB6 (L x LVT) goto case GraphemeBreakClass.LVT; } else { break; } + } case GraphemeBreakClass.LV: case GraphemeBreakClass.V: - if (processor.CurrentType == GraphemeBreakClass.V) + { + if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB7 (LV | V x V) + ReadNextCodepoint(); // rule GB7 (LV | V x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.T) + else if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB7 (LV | V x T) + ReadNextCodepoint(); // rule GB7 (LV | V x T) goto case GraphemeBreakClass.T; } else { break; } + } case GraphemeBreakClass.LVT: case GraphemeBreakClass.T: - if (processor.CurrentType == GraphemeBreakClass.T) + if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB8 (LVT | T x T) + ReadNextCodepoint(); // rule GB8 (LVT | T x T) goto case GraphemeBreakClass.T; } else @@ -139,123 +165,79 @@ namespace Avalonia.Media.TextFormatting.Unicode case GraphemeBreakClass.ExtendedPictographic: // Attempt processing extended pictographic (rules GB11, GB9). // First, drain any Extend scalars that might exist - while (processor.CurrentType == GraphemeBreakClass.Extend) + while (_currentType == GraphemeBreakClass.Extend) { - processor.MoveNext(); + ReadNextCodepoint(); } // Now see if there's a ZWJ + extended pictograph again. - if (processor.CurrentType != GraphemeBreakClass.ZWJ) + if (_currentType != GraphemeBreakClass.ZWJ) { break; } - processor.MoveNext(); - if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) + ReadNextCodepoint(); + if (_currentType != GraphemeBreakClass.ExtendedPictographic) { break; } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.ExtendedPictographic; case GraphemeBreakClass.RegionalIndicator: // We've consumed a single RI scalar. Try to consume another (to make it a pair). - if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) + if (_currentType == GraphemeBreakClass.RegionalIndicator) { - processor.MoveNext(); + ReadNextCodepoint(); } // Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers. break; // nothing but trailers after the final RI - - default: - break; } + const uint gb9Mask = + (1U << (int)GraphemeBreakClass.Extend) | + (1U << (int)GraphemeBreakClass.ZWJ) | + (1U << (int)GraphemeBreakClass.SpacingMark); + // rules GB9, GB9a - while (processor.CurrentType == GraphemeBreakClass.Extend - || processor.CurrentType == GraphemeBreakClass.ZWJ - || processor.CurrentType == GraphemeBreakClass.SpacingMark) + while (((1U << (int)_currentType) & gb9Mask) != 0U) { - processor.MoveNext(); + ReadNextCodepoint(); } Return: - Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); - - _text = _text.Slice(processor.CurrentCodeUnitOffset); + var graphemeLength = _currentCodeUnitOffset - startOffset; + grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength); return true; // rules GB2, GB999 } - [StructLayout(LayoutKind.Auto)] - private ref struct Processor + private void ReadNextCodepoint() { - private readonly ReadOnlySpan _buffer; - private int _codeUnitLengthOfCurrentScalar; - - internal Processor(ReadOnlySpan buffer) - { - _buffer = buffer; - _codeUnitLengthOfCurrentScalar = 0; - CurrentCodepoint = Codepoint.ReplacementCodepoint; - CurrentType = GraphemeBreakClass.Other; - CurrentCodeUnitOffset = 0; - } - - public int CurrentCodeUnitOffset { get; private set; } - - /// - /// Will be if invalid data or EOF reached. - /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. - /// - public GraphemeBreakClass CurrentType { get; private set; } - - /// - /// Get the currently processed . - /// - public Codepoint CurrentCodepoint { get; private set; } - - public void MoveNext() - { - // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on - // the decoder's default behavior of interpreting these ill-formed subsequences as - // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property - // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. - // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications - // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file - // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt - // has the line "D800..DFFF ; Control # Cs [2048] ..", - // but starting with Unicode 12.0 that line has been removed. - // - // If a later version of the Unicode Standard further modifies this guidance we should reflect - // that here. - - if (CurrentCodeUnitOffset == _buffer.Length) - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - else - { - CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar; - - if (CurrentCodeUnitOffset < _buffer.Length) - { - CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset, - out _codeUnitLengthOfCurrentScalar); - } - else - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - } - - CurrentType = CurrentCodepoint.GraphemeBreakClass; - } + // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on + // the decoder's default behavior of interpreting these ill-formed subsequences as + // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property + // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. + // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications + // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file + // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt + // has the line "D800..DFFF ; Control # Cs [2048] ..", + // but starting with Unicode 12.0 that line has been removed. + // + // If a later version of the Unicode Standard further modifies this guidance we should reflect + // that here. + + _currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint; + + _currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset, + out _codeUnitLengthOfCurrentCodepoint); + + _currentType = _currentCodepoint.GraphemeBreakClass; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 877ab76ce5..5e12b7458e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode { @@ -46,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30 = false; _lb30a = 0; } - - public LineBreak Current { get; private set; } - - public bool MoveNext() + + public bool MoveNext(out LineBreak lineBreak) { // Get the first char if we're at the beginning of the string. if (_first) @@ -75,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: { _currentClass = MapFirst(_nextClass); - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); return true; } } @@ -87,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (shouldBreak) { - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; } } @@ -108,23 +107,23 @@ namespace Avalonia.Media.TextFormatting.Unicode break; } - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); return true; } } - Current = default; - + lineBreak = default; return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapClass(Codepoint cp) { if (cp.Value == 327685) { return LineBreakClass.Alphabetic; } - + // LB 1 // ========================================== // Resolved Original General_Category @@ -133,26 +132,38 @@ namespace Avalonia.Media.TextFormatting.Unicode // CM SA Only Mn or Mc // AL SA Any except Mn and Mc // NS CJ Any - switch (cp.LineBreakClass) - { - case LineBreakClass.Ambiguous: - case LineBreakClass.Surrogate: - case LineBreakClass.Unknown: - return LineBreakClass.Alphabetic; - - case LineBreakClass.ComplexContext: - return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark - ? LineBreakClass.CombiningMark - : LineBreakClass.Alphabetic; + var cls = cp.LineBreakClass; - case LineBreakClass.ConditionalJapaneseStarter: - return LineBreakClass.Nonstarter; + const ulong specialMask = + (1UL << (int)LineBreakClass.Ambiguous) | + (1UL << (int)LineBreakClass.Surrogate) | + (1UL << (int)LineBreakClass.Unknown) | + (1UL << (int)LineBreakClass.ComplexContext) | + (1UL << (int)LineBreakClass.ConditionalJapaneseStarter); - default: - return cp.LineBreakClass; + if (((1UL << (int)cls) & specialMask) != 0UL) + { + switch (cls) + { + case LineBreakClass.Ambiguous: + case LineBreakClass.Surrogate: + case LineBreakClass.Unknown: + return LineBreakClass.Alphabetic; + + case LineBreakClass.ComplexContext: + return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark + ? LineBreakClass.CombiningMark + : LineBreakClass.Alphabetic; + + case LineBreakClass.ConditionalJapaneseStarter: + return LineBreakClass.Nonstarter; + } } + + return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapFirst(LineBreakClass c) { switch (c) @@ -169,10 +180,80 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAlphaNumeric(LineBreakClass cls) - => cls == LineBreakClass.Alphabetic - || cls == LineBreakClass.HebrewLetter - || cls == LineBreakClass.Numeric; + { + const ulong mask = + (1UL << (int)LineBreakClass.Alphabetic) | + (1UL << (int)LineBreakClass.HebrewLetter) | + (1UL << (int)LineBreakClass.Numeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric) | + (1UL << (int)LineBreakClass.Space); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumeric(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.CloseParenthesis); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.InfixNumeric) | + (1UL << (int)LineBreakClass.BreakSymbols); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.WordJoiner) | + (1UL << (int)LineBreakClass.Alphabetic); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.CarriageReturn); + + return ((1UL << (int)cls) & mask) != 0UL; + } private LineBreakClass PeekNextCharClass() { @@ -198,83 +279,77 @@ namespace Avalonia.Media.TextFormatting.Unicode // Track combining mark exceptions. LB22 if (cls == LineBreakClass.CombiningMark) { - switch (_currentClass) + const ulong lb22ExMask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn); + + if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - _lb22ex = true; - break; + _lb22ex = true; } - } - // Track combining mark exceptions. LB31 - if (_first && cls == LineBreakClass.CombiningMark) - { - _lb31 = true; + const ulong lb31Mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn) | + (1UL << (int)LineBreakClass.ZWJ); + + // Track combining mark exceptions. LB31 + if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL) + { + _lb31 = true; + } } - if (cls == LineBreakClass.CombiningMark) + if (_first) { - switch (_currentClass) + // Rule LB24 + if (IsClosePunctuationOrParenthesis(cls)) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - case LineBreakClass.ZWJ: - _lb31 = true; - break; + _lb24ex = true; } - } - if (_first - && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) - { - _lb31 = true; + // Rule LB25 + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls)) + { + _lb25ex = true; + } + + if (IsPrefixPostfixNumericOrSpace(cls)) + { + _lb31 = true; + } } - if (_currentClass == LineBreakClass.Alphabetic && - (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) + if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls)) { _lb31 = true; } // Reset LB31 if next is U+0028 (Left Opening Parenthesis) if (_lb31 - && _currentClass != LineBreakClass.PostfixNumeric - && _currentClass != LineBreakClass.PrefixNumeric - && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028) + && !IsPrefixPostfixNumeric(_currentClass) + && cls == LineBreakClass.OpenPunctuation + && cp.Value == 0x0028) { _lb31 = false; } - // Rule LB24 - if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis)) - { - _lb24ex = true; - } - - // Rule LB25 - if (_first - && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols)) - { - _lb25ex = true; - } - - if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic) + if (IsSpaceOrWordJoinerOrAlphabetic(cls)) { var next = PeekNextCharClass(); - if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols) + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next)) { _lb25ex = true; } @@ -295,6 +370,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool? GetSimpleBreak() { // handle classes not handled by the pair table @@ -317,6 +393,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage private bool GetPairTableBreak(LineBreakClass lastClass) { // If not handled already, use the pair table @@ -477,8 +554,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var cls = cp.LineBreakClass; - if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || - cls == LineBreakClass.CarriageReturn) + if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls)) { from -= count; } diff --git a/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs new file mode 100644 index 0000000000..dacff9e589 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// Represents a line break that occurred due to wrapping. + internal sealed class WrappingTextLineBreak : TextLineBreak + { + private List? _remainingRuns; + + public WrappingTextLineBreak(TextEndOfLine? textEndOfLine, FlowDirection flowDirection, + List remainingRuns) + : base(textEndOfLine, flowDirection, isSplit: true) + { + Debug.Assert(remainingRuns.Count > 0); + _remainingRuns = remainingRuns; + } + + /// + /// Gets the remaining runs from this line break, and clears them from this line break. + /// + /// A list of text runs. + public List? AcquireRemainingRuns() + { + var remainingRuns = _remainingRuns; + _remainingRuns = null; + return remainingRuns; + } + } +} diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 1261d233ac..2be3e9a94e 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,5 +1,4 @@ using Avalonia.Media.Immutable; -using Avalonia.VisualTree; namespace Avalonia.Media { @@ -11,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty VisualProperty = - AvaloniaProperty.Register(nameof(Visual)); + public static readonly StyledProperty VisualProperty = + AvaloniaProperty.Register(nameof(Visual)); static VisualBrush() { @@ -38,7 +37,7 @@ namespace Avalonia.Media /// /// Gets or sets the visual to draw. /// - public Visual Visual + public Visual? Visual { get { return GetValue(VisualProperty); } set { SetValue(VisualProperty, value); } diff --git a/src/Avalonia.Base/Metadata/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs index 85ca6c4ec9..1c85a67641 100644 --- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs +++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs @@ -3,10 +3,10 @@ using System; namespace Avalonia.Metadata { /// - /// Defines the ambient class/property + /// Defines the ambient class/property /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)] - public class AmbientAttribute : Attribute + public sealed class AmbientAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs index a0b2fa0e1d..f32c8e78f6 100644 --- a/src/Avalonia.Base/Metadata/ContentAttribute.cs +++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class ContentAttribute : Attribute + public sealed class ContentAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs index ac46a0d30a..dd9603b4a9 100644 --- a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs +++ b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template. /// [AttributeUsage(AttributeTargets.Property)] -public class DataTypeAttribute : Attribute +public sealed class DataTypeAttribute : Attribute { - + } diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index caee71ebfd..ca58a91eb9 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that the property depends on the value of another property in markup. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class DependsOnAttribute : Attribute + public sealed class DependsOnAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs new file mode 100644 index 0000000000..fac8cd8737 --- /dev/null +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Instructs the compiler to resolve the compiled bindings data type for the item-specific properties of collection-like controls. +/// +/// +/// A typical usage example is a ListBox control, where is defined on the ItemTemplate property, +/// allowing the template to inherit the data type from the Items collection binding. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class InheritDataTypeFromItemsAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the property whose item type should be used on the target property. + public InheritDataTypeFromItemsAttribute(string ancestorItemsProperty) + { + AncestorItemsProperty = ancestorItemsProperty; + } + + /// + /// The name of the property whose item type should be used on the target property. + /// + public string AncestorItemsProperty { get; } + + /// + /// The ancestor type to be used in a lookup for the . + /// If null, the declaring type of the target property is used. + /// + public Type? AncestorType { get; set; } +} diff --git a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs index 348c983c03..75fe7b8031 100644 --- a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs +++ b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs @@ -11,7 +11,7 @@ namespace Avalonia.Metadata /// may be added to its API. /// [AttributeUsage(AttributeTargets.Interface)] - public class NotClientImplementableAttribute : Attribute + public sealed class NotClientImplementableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index 258154aba4..78bcc2ff29 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class TemplateContentAttribute : Attribute + public sealed class TemplateContentAttribute : Attribute { public Type? TemplateResultType { get; set; } } diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs index c46891b3ad..a644c9afe6 100644 --- a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs +++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class TrimSurroundingWhitespaceAttribute : Attribute + public sealed class TrimSurroundingWhitespaceAttribute : Attribute { } diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 3b6fa5168a..361f6d30fd 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -6,7 +6,8 @@ namespace Avalonia.Metadata /// This API is unstable and is not covered by API compatibility guarantees between minor and /// patch releases. /// - public class UnstableAttribute : Attribute + [AttributeUsage(AttributeTargets.All)] + public sealed class UnstableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs index 753a96b9ce..d2d163b368 100644 --- a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs +++ b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs @@ -3,8 +3,8 @@ using System; namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class)] - public class UsableDuringInitializationAttribute : Attribute + public sealed class UsableDuringInitializationAttribute : Attribute { - + } } diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs index aeaa38dad9..2fd2b1da3b 100644 --- a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class WhitespaceSignificantCollectionAttribute : Attribute + public sealed class WhitespaceSignificantCollectionAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs index d43fa55f5c..c6b79ba987 100644 --- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Maps an XML namespace to a CLR namespace for use in XAML. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class XmlnsDefinitionAttribute : Attribute + public sealed class XmlnsDefinitionAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 469f33e7fd..ef207a3dae 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -351,7 +351,7 @@ namespace Avalonia /// The new . public PixelRect WithHeight(int height) { - return new PixelRect(X, Y, Width, Height); + return new PixelRect(X, Y, Width, height); } /// diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index b5e7298b7e..08fcdb50aa 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -37,7 +37,7 @@ namespace Avalonia.Platform }; } - public event EventHandler? ColorValuesChanged; + public virtual event EventHandler? ColorValuesChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 6aa5eeea3d..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -49,7 +49,7 @@ namespace Avalonia.Platform /// The stroke pen. /// The first point of the line. /// The second point of the line. - void DrawLine(IPen pen, Point p1, Point p2); + void DrawLine(IPen? pen, Point p1, Point p2); /// /// Draws a geometry. @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); + void DrawGlyphRun(IBrush? foreground, IRef glyphRun); /// /// Creates a new that can be used as a render layer @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..5b06651b5f --- /dev/null +++ b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Platform; + +[Unstable] +public interface IExternalObjectsRenderInterfaceContextFeature +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceUuid { get; } + public byte[]? DeviceLuid { get; } +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedObject : IDisposable +{ + +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedImage : IPlatformRenderInterfaceImportedObject +{ + IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex); + + IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore); + + IBitmapImpl SnapshotWithAutomaticSync(); +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedSemaphore : IPlatformRenderInterfaceImportedObject +{ + +} diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 5826cfb2ff..d1964bf07e 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Metadata; @@ -47,7 +48,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(IPen pen, Point point); + bool StrokeContains(IPen? pen, Point point); /// /// Makes a clone of the geometry with the specified transform. @@ -87,6 +88,7 @@ namespace Avalonia.Platform /// If ture, the resulting snipped path will start with a BeginFigure call. /// The resulting snipped path. /// If the snipping operation is successful. - bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry); } } diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 6a8ae4d954..46b065b04e 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,6 +10,23 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - IReadOnlyList GetIntersections(float lowerBound, float upperBound); + + /// + /// Gets the conservative bounding box of the glyph run./>. + /// + Size Size { get; } + + /// + /// Gets the baseline origin of the glyph run./>. + /// + Point BaselineOrigin { get; } + + /// + /// Gets the intersections of specified upper and lower limit. + /// + /// Upper limit. + /// Lower limit. + /// + IReadOnlyList GetIntersections(float lowerLimit, float upperLimit); } } diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs index b6464dea58..1778031e5b 100644 --- a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Avalonia.Platform; @@ -15,4 +16,11 @@ public static class OptionalFeatureProviderExtensions { public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => (T?)provider.TryGetFeature(typeof(T)); -} \ No newline at end of file + + public static bool TryGetFeature(this IOptionalFeatureProvider provider, [MaybeNullWhen(false)] out T rv) + where T : class + { + rv = provider.TryGetFeature(); + return rv != null; + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs b/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs new file mode 100644 index 0000000000..227e65c08d --- /dev/null +++ b/src/Avalonia.Base/Platform/IPlatformBehaviorInhibition.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Avalonia.Platform +{ + /// + /// Allows to inhibit platform specific behavior. + /// + public interface IPlatformBehaviorInhibition + { + Task SetInhibitAppSleep(bool inhibitAppSleep, string reason); + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..cfc7fac3ea 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -166,11 +167,10 @@ namespace Avalonia.Platform /// /// The glyph typeface. /// The font rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. - /// - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// The list of glyphs. + /// The baseline origin of the run. Can be null. + /// An . + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); /// /// Creates a backend-specific object using a low-level API graphics context @@ -197,6 +197,8 @@ namespace Avalonia.Platform /// Default used on this platform. /// public PixelFormat DefaultPixelFormat { get; } + + bool IsSupportedBitmapPixelFormat(PixelFormat format); } [Unstable] @@ -210,5 +212,10 @@ namespace Avalonia.Platform /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Indicates that the context is no longer usable. This method should be thread-safe + /// + bool IsLost { get; } } } diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index bf18a7da5b..3dbc7c1bb2 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -26,6 +26,6 @@ namespace Avalonia.Platform bool CurrentThreadIsLoopThread { get; } - event Action Signaled; + event Action? Signaled; } } diff --git a/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs new file mode 100644 index 0000000000..acf1801e0a --- /dev/null +++ b/src/Avalonia.Base/Platform/IReadableBitmapImpl.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Platform; + +public interface IReadableBitmapImpl +{ + PixelFormat? Format { get; } + ILockedFramebuffer Lock(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs index fa1e1862b7..3284d34a0a 100644 --- a/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs @@ -6,8 +6,7 @@ namespace Avalonia.Platform /// Defines the platform-specific interface for a . /// [Unstable] - public interface IWriteableBitmapImpl : IBitmapImpl + public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapImpl { - ILockedFramebuffer Lock(); } } diff --git a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs index 467cd530fc..6a577c204c 100644 --- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -18,26 +19,21 @@ internal class AssemblyDescriptor : IAssemblyDescriptor { public AssemblyDescriptor(Assembly assembly) { - Assembly = assembly; + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + Resources = assembly.GetManifestResourceNames() + .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); + Name = assembly.GetName().Name; - if (assembly != null) + using var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName); + if (resources != null) { - Resources = assembly.GetManifestResourceNames() - .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); - Name = assembly.GetName().Name; - using (var resources = assembly.GetManifestResourceStream(Constants.AvaloniaResourceName)) - { - if (resources != null) - { - Resources.Remove(Constants.AvaloniaResourceName); + Resources.Remove(Constants.AvaloniaResourceName); - var indexLength = new BinaryReader(resources).ReadInt32(); - var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); - var baseOffset = indexLength + 4; - AvaloniaResources = index.ToDictionary(r => GetPathRooted(r), r => (IAssetDescriptor) - new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); - } - } + var indexLength = new BinaryReader(resources).ReadInt32(); + var index = AvaloniaResourcesIndexReaderWriter.ReadIndex(new SlicedStream(resources, 4, indexLength)); + var baseOffset = indexLength + 4; + AvaloniaResources = index.ToDictionary(GetPathRooted, r => (IAssetDescriptor) + new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); } } @@ -45,6 +41,7 @@ internal class AssemblyDescriptor : IAssemblyDescriptor public Dictionary? Resources { get; } public Dictionary? AvaloniaResources { get; } public string? Name { get; } + private static string GetPathRooted(AvaloniaResourcesIndexEntry r) => r.Path![0] == '/' ? r.Path : '/' + r.Path; } diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 526303ebb1..99fe17055d 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -1,9 +1,74 @@ -namespace Avalonia.Platform +using System; + +namespace Avalonia.Platform { - public enum PixelFormat + internal enum PixelFormatEnum { Rgb565, Rgba8888, - Bgra8888 + Bgra8888, + BlackWhite, + Gray2, + Gray4, + Gray8, + Gray16, + Gray32Float, + Rgba64 + } + + public record struct PixelFormat + { + internal PixelFormatEnum FormatEnum; + + public int BitsPerPixel + { + get + { + if (FormatEnum == PixelFormatEnum.BlackWhite) + return 1; + else if (FormatEnum == PixelFormatEnum.Gray2) + return 2; + else if (FormatEnum == PixelFormatEnum.Gray4) + return 4; + else if (FormatEnum == PixelFormatEnum.Gray8) + return 8; + else if (FormatEnum == PixelFormatEnum.Rgb565 + || FormatEnum == PixelFormatEnum.Gray16) + return 16; + else if (FormatEnum == PixelFormatEnum.Rgba64) + return 64; + + return 32; + } + } + + internal bool HasAlpha => FormatEnum == PixelFormatEnum.Rgba8888 + || FormatEnum == PixelFormatEnum.Bgra8888 + || FormatEnum == PixelFormatEnum.Rgba64; + + internal PixelFormat(PixelFormatEnum format) + { + FormatEnum = format; + } + + public static PixelFormat Rgb565 => PixelFormats.Rgb565; + public static PixelFormat Rgba8888 => PixelFormats.Rgba8888; + public static PixelFormat Bgra8888 => PixelFormats.Bgra8888; + + public override string ToString() => FormatEnum.ToString(); + } + + public static class PixelFormats + { + public static PixelFormat Rgb565 { get; } = new PixelFormat(PixelFormatEnum.Rgb565); + public static PixelFormat Rgba8888 { get; } = new PixelFormat(PixelFormatEnum.Rgba8888); + public static PixelFormat Rgba64 { get; } = new PixelFormat(PixelFormatEnum.Rgba64); + public static PixelFormat Bgra8888 { get; } = new PixelFormat(PixelFormatEnum.Bgra8888); + public static PixelFormat BlackWhite { get; } = new PixelFormat(PixelFormatEnum.BlackWhite); + public static PixelFormat Gray2 { get; } = new PixelFormat(PixelFormatEnum.Gray2); + public static PixelFormat Gray4 { get; } = new PixelFormat(PixelFormatEnum.Gray4); + public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); + public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); + public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); } } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs new file mode 100644 index 0000000000..4b47c93eb5 --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -0,0 +1,60 @@ +namespace Avalonia.Platform; + +public record struct PlatformGraphicsExternalImageProperties +{ + public int Width { get; set; } + public int Height { get; set; } + public PlatformGraphicsExternalImageFormat Format { get; set; } + public ulong MemorySize { get; set; } + public ulong MemoryOffset { get; set; } + public bool TopLeftOrigin { get; set; } +} + +public enum PlatformGraphicsExternalImageFormat +{ + R8G8B8A8UNorm, + B8G8R8A8UNorm +} + +/// +/// Describes various GPU memory handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalImageHandleTypes +{ + /// + /// An DXGI global shared handle returned by IDXGIResource::GetSharedHandle D3D11_RESOURCE_MISC_SHARED or D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX flag. + /// The handle does not own the reference to the underlying video memory, so the provider should make sure that the resource is valid until + /// the handle has been successfully imported + /// + public const string D3D11TextureGlobalSharedHandle = nameof(D3D11TextureGlobalSharedHandle); + /// + /// A DXGI NT handle returned by IDXGIResource1::CreateSharedHandle for a texture created with D3D11_RESOURCE_MISC_SHARED_NTHANDLE or flag + /// + public const string D3D11TextureNtHandle = nameof(D3D11TextureNtHandle); + /// + /// A POSIX file descriptor that's exported by Vulkan using VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); +} + +/// +/// Describes various GPU semaphore handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalSemaphoreHandleTypes +{ + /// + /// A POSIX file descriptor that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); + + /// + /// A NT handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT or in a compatible way + /// + public const string VulkanOpaqueNtHandle = nameof(VulkanOpaqueNtHandle); + + // A global shared handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT or in a compatible way + public const string VulkanOpaqueKmtHandle = nameof(VulkanOpaqueKmtHandle); + + /// A DXGI NT handle returned by ID3D12Device::CreateSharedHandle or ID3D11Fence::CreateSharedHandle + public const string Direct3D12FenceNtHandle = nameof(Direct3D12FenceNtHandle); +} diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index cec20678cf..5bf9ff9d9a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -1,50 +1,61 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Security; using System.Threading.Tasks; -using Avalonia.Metadata; namespace Avalonia.Platform.Storage.FileIO; -[Unstable] -public class BclStorageFile : IStorageBookmarkFile +internal class BclStorageFile : IStorageBookmarkFile { - private readonly FileInfo _fileInfo; - public BclStorageFile(string fileName) { - _fileInfo = new FileInfo(fileName); + FileInfo = new FileInfo(fileName); } public BclStorageFile(FileInfo fileInfo) { - _fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); + FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); } - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - - public string Name => _fileInfo.Name; + public FileInfo FileInfo { get; } + + public string Name => FileInfo.Name; public virtual bool CanBookmark => true; + public Uri Path + { + get + { + try + { + if (FileInfo.Directory is not null) + { + return StorageProviderHelpers.FilePathToUri(FileInfo.FullName); + } + } + catch (SecurityException) + { + } + return new Uri(FileInfo.Name, UriKind.Relative); + } + } + public Task GetBasicPropertiesAsync() { - if (_fileInfo.Exists) + if (FileInfo.Exists) { return Task.FromResult(new StorageItemProperties( - (ulong)_fileInfo.Length, - _fileInfo.CreationTimeUtc, - _fileInfo.LastAccessTimeUtc)); + (ulong)FileInfo.Length, + FileInfo.CreationTimeUtc, + FileInfo.LastAccessTimeUtc)); } return Task.FromResult(new StorageItemProperties()); } public Task GetParentAsync() { - if (_fileInfo.Directory is { } directory) + if (FileInfo.Directory is { } directory) { return Task.FromResult(new BclStorageFolder(directory)); } @@ -53,17 +64,17 @@ public class BclStorageFile : IStorageBookmarkFile public Task OpenReadAsync() { - return Task.FromResult(_fileInfo.OpenRead()); + return Task.FromResult(FileInfo.OpenRead()); } public Task OpenWriteAsync() { - return Task.FromResult(_fileInfo.OpenWrite()); + return Task.FromResult(FileInfo.OpenWrite()); } public virtual Task SaveBookmarkAsync() { - return Task.FromResult(_fileInfo.FullName); + return Task.FromResult(FileInfo.FullName); } public Task ReleaseBookmarkAsync() @@ -72,28 +83,6 @@ public class BclStorageFile : IStorageBookmarkFile return Task.CompletedTask; } - public bool TryGetUri([NotNullWhen(true)] out Uri? uri) - { - try - { - if (_fileInfo.Directory is not null) - { - uri = Path.IsPathRooted(_fileInfo.FullName) ? - new Uri(new Uri("file://"), _fileInfo.FullName) : - new Uri(_fileInfo.FullName, UriKind.Relative); - return true; - } - - uri = null; - return false; - } - catch (SecurityException) - { - uri = null; - return false; - } - } - protected virtual void Dispose(bool disposing) { } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index b91e910777..1e21c197bb 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -1,23 +1,18 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security; using System.Threading.Tasks; -using Avalonia.Metadata; namespace Avalonia.Platform.Storage.FileIO; -[Unstable] -public class BclStorageFolder : IStorageBookmarkFolder +internal class BclStorageFolder : IStorageBookmarkFolder { - private readonly DirectoryInfo _directoryInfo; - public BclStorageFolder(string path) { - _directoryInfo = new DirectoryInfo(path); - if (!_directoryInfo.Exists) + DirectoryInfo = new DirectoryInfo(path); + if (!DirectoryInfo.Exists) { throw new ArgumentException("Directory must exist"); } @@ -25,29 +20,46 @@ public class BclStorageFolder : IStorageBookmarkFolder public BclStorageFolder(DirectoryInfo directoryInfo) { - _directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); - if (!_directoryInfo.Exists) + DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); + if (!DirectoryInfo.Exists) { throw new ArgumentException("Directory must exist", nameof(directoryInfo)); } } - public string Name => _directoryInfo.Name; + public string Name => DirectoryInfo.Name; + + public DirectoryInfo DirectoryInfo { get; } public bool CanBookmark => true; + public Uri Path + { + get + { + try + { + return StorageProviderHelpers.FilePathToUri(DirectoryInfo.FullName); + } + catch (SecurityException) + { + return new Uri(DirectoryInfo.Name, UriKind.Relative); + } + } + } + public Task GetBasicPropertiesAsync() { var props = new StorageItemProperties( null, - _directoryInfo.CreationTimeUtc, - _directoryInfo.LastAccessTimeUtc); + DirectoryInfo.CreationTimeUtc, + DirectoryInfo.LastAccessTimeUtc); return Task.FromResult(props); } public Task GetParentAsync() { - if (_directoryInfo.Parent is { } directory) + if (DirectoryInfo.Parent is { } directory) { return Task.FromResult(new BclStorageFolder(directory)); } @@ -56,9 +68,9 @@ public class BclStorageFolder : IStorageBookmarkFolder public Task> GetItemsAsync() { - var items = _directoryInfo.GetDirectories() + var items = DirectoryInfo.GetDirectories() .Select(d => (IStorageItem)new BclStorageFolder(d)) - .Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) + .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f))) .ToArray(); return Task.FromResult>(items); @@ -66,7 +78,7 @@ public class BclStorageFolder : IStorageBookmarkFolder public virtual Task SaveBookmarkAsync() { - return Task.FromResult(_directoryInfo.FullName); + return Task.FromResult(DirectoryInfo.FullName); } public Task ReleaseBookmarkAsync() @@ -75,23 +87,6 @@ public class BclStorageFolder : IStorageBookmarkFolder return Task.CompletedTask; } - public bool TryGetUri([NotNullWhen(true)] out Uri? uri) - { - try - { - uri = Path.IsPathRooted(_directoryInfo.FullName) ? - new Uri(new Uri("file://"), _directoryInfo.FullName) : - new Uri(_directoryInfo.FullName, UriKind.Relative); - - return true; - } - catch (SecurityException) - { - uri = null; - return false; - } - } - protected virtual void Dispose(bool disposing) { } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index 469388021e..34409f5fda 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.Metadata; +using Avalonia.Compatibility; namespace Avalonia.Platform.Storage.FileIO; -[Unstable] -public abstract class BclStorageProvider : IStorageProvider +internal abstract class BclStorageProvider : IStorageProvider { public abstract bool CanOpen { get; } public abstract Task> OpenFilePickerAsync(FilePickerOpenOptions options); @@ -32,4 +33,98 @@ public abstract class BclStorageProvider : IStorageProvider ? Task.FromResult(new BclStorageFolder(folder)) : Task.FromResult(null); } + + public virtual Task TryGetFileFromPathAsync(Uri filePath) + { + if (filePath.IsAbsoluteUri) + { + var file = new FileInfo(filePath.LocalPath); + if (file.Exists) + { + return Task.FromResult(new BclStorageFile(file)); + } + } + + return Task.FromResult(null); + } + + public virtual Task TryGetFolderFromPathAsync(Uri folderPath) + { + if (folderPath.IsAbsoluteUri) + { + var directory = new DirectoryInfo(folderPath.LocalPath); + if (directory.Exists) + { + return Task.FromResult(new BclStorageFolder(directory)); + } + } + + return Task.FromResult(null); + } + + public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) + { + // Note, this BCL API returns different values depending on the .NET version. + // We should also document it. + // https://github.com/dotnet/docs/issues/31423 + // For pre-breaking change values see table: + // https://johnkoerner.com/csharp/special-folder-values-on-windows-versus-mac/ + var folderPath = wellKnownFolder switch + { + WellKnownFolder.Desktop => GetFromSpecialFolder(Environment.SpecialFolder.Desktop), + WellKnownFolder.Documents => GetFromSpecialFolder(Environment.SpecialFolder.MyDocuments), + WellKnownFolder.Downloads => GetDownloadsWellKnownFolder(), + WellKnownFolder.Music => GetFromSpecialFolder(Environment.SpecialFolder.MyMusic), + WellKnownFolder.Pictures => GetFromSpecialFolder(Environment.SpecialFolder.MyPictures), + WellKnownFolder.Videos => GetFromSpecialFolder(Environment.SpecialFolder.MyVideos), + _ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null) + }; + + if (folderPath is null) + { + return Task.FromResult(null); + } + + var directory = new DirectoryInfo(folderPath); + if (!directory.Exists) + { + return Task.FromResult(null); + } + + return Task.FromResult(new BclStorageFolder(directory)); + + string GetFromSpecialFolder(Environment.SpecialFolder folder) => + Environment.GetFolderPath(folder, Environment.SpecialFolderOption.Create); + } + + // TODO, replace with https://github.com/dotnet/runtime/issues/70484 when implemented. + // Normally we want to avoid platform specific code in the Avalonia.Base assembly. + protected static string? GetDownloadsWellKnownFolder() + { + if (OperatingSystemEx.IsWindows()) + { + return Environment.OSVersion.Version.Major < 6 ? null : + SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero); + } + + if (OperatingSystemEx.IsLinux()) + { + var envDir = Environment.GetEnvironmentVariable("XDG_DOWNLOAD_DIR"); + if (envDir != null && Directory.Exists(envDir)) + { + return envDir; + } + } + + if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS()) + { + return "~/Downloads"; + } + + return null; + } + + private static readonly Guid s_folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B"); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] + private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token); } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index f90d0a5a2f..55e84ee937 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -1,13 +1,23 @@ using System; using System.IO; using System.Linq; -using Avalonia.Metadata; +using System.Text; namespace Avalonia.Platform.Storage.FileIO; -[Unstable] -public static class StorageProviderHelpers +internal static class StorageProviderHelpers { + public static Uri FilePathToUri(string path) + { + var uriPath = new StringBuilder(path) + .Replace("%", $"%{(int)'%':X2}") + .Replace("[", $"%{(int)'[':X2}") + .Replace("]", $"%{(int)']':X2}") + .ToString(); + + return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri; + } + public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) { var name = Path.GetFileName(path); diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs index 75076e2bb8..7b0446e224 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Avalonia.Platform.Storage; @@ -7,9 +9,9 @@ namespace Avalonia.Platform.Storage; /// public sealed class FilePickerFileType { - public FilePickerFileType(string name) + public FilePickerFileType(string? name) { - Name = name; + Name = name ?? string.Empty; } /// @@ -21,7 +23,7 @@ public sealed class FilePickerFileType /// List of extensions in GLOB format. I.e. "*.png" or "*.*". /// /// - /// Used on Windows and Linux systems. + /// Used on Windows, Linux and Browser platforms. /// public IReadOnlyList? Patterns { get; set; } @@ -29,7 +31,7 @@ public sealed class FilePickerFileType /// List of extensions in MIME format. /// /// - /// Used on Android, Browser and Linux systems. + /// Used on Android, Linux and Browser platforms. /// public IReadOnlyList? MimeTypes { get; set; } @@ -41,4 +43,14 @@ public sealed class FilePickerFileType /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. /// public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } + + internal IReadOnlyList? TryGetExtensions() + { + // Converts random glob pattern to a simple extension name. + // GetExtension should be sufficient here. + // Only exception is "*.*proj" patterns that should be filtered as well. + return Patterns?.Select(Path.GetExtension) + .Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith(".")) + .ToArray()!; + } } diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 4aa84e3ec4..2a0ce15279 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFile : IStorageItem { - /// - /// Returns true, if file is readable. - /// - bool CanOpenRead { get; } - /// /// Opens a stream for read access. /// /// Task OpenReadAsync(); - - /// - /// Returns true, if file is writeable. - /// - bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// diff --git a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs index f5469d31c9..c2c93400b0 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageItem.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageItem.cs @@ -20,13 +20,13 @@ public interface IStorageItem : IDisposable string Name { get; } /// - /// Gets the full file-system path of the item, if the item has a path. + /// Gets the file-system path of the item. /// /// /// Android backend might return file path with "content:" scheme. /// Browser and iOS backends might return relative uris. /// - bool TryGetUri([NotNullWhen(true)] out Uri? uri); + Uri Path { get; } /// /// Gets the basic properties of the current item. diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 0f5cf931d9..9d3c961e51 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia.Metadata; @@ -53,4 +55,35 @@ public interface IStorageProvider /// Bookmark ID. /// Bookmarked folder or null if OS denied request. Task OpenFolderBookmarkAsync(string bookmark); + + /// + /// Attempts to read file from the file-system by its path. + /// + /// The path of the item to retrieve in Uri format. + /// + /// Uri path is usually expected to be an absolute path with "file" scheme. + /// But it can be an uri with "content" scheme on the Android. + /// It also might ask user for the permission, and throw an exception if it was denied. + /// + /// File or null if it doesn't exist. + Task TryGetFileFromPathAsync(Uri filePath); + + /// + /// Attempts to read folder from the file-system by its path. + /// + /// The path of the folder to retrieve in Uri format. + /// + /// Uri path is usually expected to be an absolute path with "file" scheme. + /// But it can be an uri with "content" scheme on the Android. + /// It also might ask user for the permission, and throw an exception if it was denied. + /// + /// Folder or null if it doesn't exist. + Task TryGetFolderFromPathAsync(Uri folderPath); + + /// + /// Attempts to read folder from the file-system by its path + /// + /// Well known folder identifier. + /// Folder or null if it doesn't exist. + Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder); } diff --git a/src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs b/src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs new file mode 100644 index 0000000000..4a0d0c634f --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Platform.Storage; + +public class NameCollisionOption +{ + +} diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs new file mode 100644 index 0000000000..6f8b945cd6 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +/// +/// Group of public extensions for class. +/// +public static class StorageProviderExtensions +{ + /// + public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) + { + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); + } + + /// + public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) + { + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); + } + + /// + /// Gets the local file system path of the item as a string. + /// + /// Storage folder or file. + /// Full local path to the folder or file if possible, otherwise null. + /// + /// Android platform usually uses "content:" virtual file paths + /// and Browser platform has isolated access without full paths, + /// so on these platforms this method will return null. + /// + public static string? TryGetLocalPath(this IStorageItem item) + { + // We can avoid double escaping of the path by checking for BclStorageFolder. + // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. + if (item is BclStorageFolder storageFolder) + { + return storageFolder.DirectoryInfo.FullName; + } + if (item is BclStorageFile storageFile) + { + return storageFile.FileInfo.FullName; + } + + if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) + { + return absolutePath.LocalPath; + } + + // android "content:", browser and ios relative links go here. + return null; + } +} diff --git a/src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs b/src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs new file mode 100644 index 0000000000..fcc933bc53 --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs @@ -0,0 +1,37 @@ +namespace Avalonia.Platform.Storage; + +/// +/// Specifies enumerated constants used to retrieve directory paths to system well known folders. +/// +public enum WellKnownFolder +{ + /// + /// Current user desktop folder. + /// + Desktop, + + /// + /// Current user documents folder. + /// + Documents, + + /// + /// Current user downloads folder. + /// + Downloads, + + /// + /// Current user music folder + /// + Music, + + /// + /// Current user pictures folder + /// + Pictures, + + /// + /// Current user videos folder + /// + Videos, +} diff --git a/src/Avalonia.Base/Platform/SystemNavigationManager.cs b/src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs similarity index 50% rename from src/Avalonia.Base/Platform/SystemNavigationManager.cs rename to src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs index 6165b2bb77..080cd8083e 100644 --- a/src/Avalonia.Base/Platform/SystemNavigationManager.cs +++ b/src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs @@ -5,13 +5,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { [Unstable] - public interface ITopLevelWithSystemNavigationManager - { - ISystemNavigationManager SystemNavigationManager { get; } - } - - [Unstable] - public interface ISystemNavigationManager + public interface ISystemNavigationManagerImpl { public event EventHandler? BackRequested; } diff --git a/src/Avalonia.Base/Point.cs b/src/Avalonia.Base/Point.cs index 0c789ff20f..86f7adf0d1 100644 --- a/src/Avalonia.Base/Point.cs +++ b/src/Avalonia.Base/Point.cs @@ -251,6 +251,12 @@ namespace Avalonia /// The transform. /// The transformed point. public Point Transform(Matrix transform) => transform.Transform(this); + + internal Point Transform(Matrix4x4 matrix) + { + var vec = Vector2.Transform(new Vector2((float)X, (float)Y), matrix); + return new Point(vec.X, vec.Y); + } /// /// Returns a new point with the specified X coordinate. diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index 11dc80ef8f..e1ff0970c2 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,6 +16,8 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; + private TValue? _defaultValue; + private bool _isDefaultValueInitialized; protected BindingEntryBase( ValueFrame frame, @@ -89,6 +91,7 @@ namespace Avalonia.PropertyStore protected abstract BindingValue ConvertAndValidate(TSource value); protected abstract BindingValue ConvertAndValidate(BindingValue value); + protected abstract TValue GetDefaultValue(Type ownerType); protected virtual void Start(bool produceValue) { @@ -104,17 +107,6 @@ namespace Avalonia.PropertyStore }; } - private void ClearValue() - { - if (_hasValue) - { - _hasValue = false; - _value = default; - if (_subscription is not null) - Frame.Owner?.OnBindingValueCleared(Property, Frame.Priority); - } - } - private void SetValue(BindingValue value) { static void Execute(BindingEntryBase instance, BindingValue value) @@ -124,24 +116,20 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); - if (value.HasValue) - { - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value)) - { - instance._value = value.Value; - instance._hasValue = true; - if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); - } - } - else if (value.Type != BindingValueType.DoNothing) + var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + + if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) { - instance.ClearValue(); + instance._value = effectiveValue; + instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) - instance.Frame.Owner?.OnBindingValueCleared(instance.Property, instance.Frame.Priority); + instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); } } + if (value.Type == BindingValueType.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -161,5 +149,16 @@ namespace Avalonia.PropertyStore _subscription = null; Frame.OnBindingCompleted(this); } + + private TValue GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 04d3c805c2..78f0ad46b7 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -29,6 +29,12 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets a value indicating whether the was overridden by a call to + /// . + /// + public bool IsOverridenCurrentValue { get; set; } + /// /// Begins a reevaluation pass on the effective value. /// diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 93fffb3755..c469034f9b 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -19,13 +19,16 @@ namespace Avalonia.PropertyStore private T? _baseValue; private UncommonFields? _uncommon; - public EffectiveValue(AvaloniaObject owner, StyledPropertyBase property) + public EffectiveValue( + AvaloniaObject owner, + StyledProperty property, + EffectiveValue? inherited) { Priority = BindingPriority.Unset; BasePriority = BindingPriority.Unset; _metadata = property.GetMetadata(owner.GetType()); - var value = _metadata.DefaultValue; + var value = inherited is null ? _metadata.DefaultValue : inherited.Value; if (property.HasCoercion && _metadata.CoerceValue is { } coerce) { @@ -57,15 +60,24 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledPropertyBase)value.Property, GetValue(value), priority); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); } public void SetLocalValueAndRaise( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, + T value) + { + SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false); + } + + public void SetCurrentValueAndRaise( + ValueStore owner, + StyledProperty property, T value) { - SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); + IsOverridenCurrentValue = true; + SetAndRaiseCore(owner, property, value, Priority, true); } public bool TryGetBaseValue([MaybeNullWhen(false)] out T value) @@ -82,7 +94,7 @@ namespace Avalonia.PropertyStore { Debug.Assert(oldValue is not null || newValue is not null); - var p = (StyledPropertyBase)property; + var p = (StyledProperty)property; var o = oldValue is not null ? ((EffectiveValue)oldValue).Value : _metadata.DefaultValue; var n = newValue is not null ? ((EffectiveValue)newValue).Value : _metadata.DefaultValue; var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; @@ -98,7 +110,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledPropertyBase)property, _baseValue!, BasePriority); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -107,7 +119,7 @@ namespace Avalonia.PropertyStore return; SetAndRaiseCore( owner, - (StyledPropertyBase)property, + (StyledProperty)property, _uncommon._uncoercedValue!, Priority, _uncommon._uncoercedBaseValue!, @@ -117,10 +129,10 @@ namespace Avalonia.PropertyStore public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) { UnsubscribeValueEntries(); - DisposeAndRaiseUnset(owner, (StyledPropertyBase)property); + DisposeAndRaiseUnset(owner, (StyledProperty)property); } - public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase property) + public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property) { BindingPriority priority; T oldValue; @@ -156,17 +168,18 @@ namespace Avalonia.PropertyStore private void SetAndRaiseCore( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, T value, - BindingPriority priority) + BindingPriority priority, + bool isOverriddenCurrentValue) { - Debug.Assert(priority < BindingPriority.Inherited); - var oldValue = Value; var valueChanged = false; var baseValueChanged = false; var v = value; + IsOverridenCurrentValue = isOverriddenCurrentValue; + if (_uncommon?._coerce is { } coerce) v = coerce(owner.Owner, value); @@ -203,13 +216,12 @@ namespace Avalonia.PropertyStore private void SetAndRaiseCore( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, T value, BindingPriority priority, T baseValue, BindingPriority basePriority) { - Debug.Assert(priority < BindingPriority.Inherited); Debug.Assert(basePriority > BindingPriority.Animation); Debug.Assert(priority <= basePriority); @@ -225,7 +237,7 @@ namespace Avalonia.PropertyStore bv = coerce(owner.Owner, baseValue); } - if (priority != BindingPriority.Unset && !EqualityComparer.Default.Equals(Value, v)) + if (!EqualityComparer.Default.Equals(Value, v)) { Value = v; valueChanged = true; @@ -233,9 +245,7 @@ namespace Avalonia.PropertyStore _uncommon._uncoercedValue = value; } - if (priority != BindingPriority.Unset && - (BasePriority == BindingPriority.Unset || - !EqualityComparer.Default.Equals(_baseValue, bv))) + if (!EqualityComparer.Default.Equals(_baseValue, bv)) { _baseValue = v; baseValueChanged = true; diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs index 364b4e1225..d8a353dc70 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -9,7 +9,7 @@ namespace Avalonia.PropertyStore public ImmediateValueEntry( ImmediateValueFrame owner, - StyledPropertyBase property, + StyledProperty property, T value) { _owner = owner; @@ -17,7 +17,7 @@ namespace Avalonia.PropertyStore Property = property; } - public StyledPropertyBase Property { get; } + public StyledProperty Property { get; } public bool HasValue => true; AvaloniaProperty IValueEntry.Property => Property; diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 50d5333b9f..7e9f3ab312 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -15,7 +15,7 @@ namespace Avalonia.PropertyStore } public TypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable> source) { var e = new TypedBindingEntry(this, property, source); @@ -24,7 +24,7 @@ namespace Avalonia.PropertyStore } public TypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source) { var e = new TypedBindingEntry(this, property, source); @@ -33,7 +33,7 @@ namespace Avalonia.PropertyStore } public SourceUntypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source) { var e = new SourceUntypedBindingEntry(this, property, source); @@ -41,7 +41,7 @@ namespace Avalonia.PropertyStore return e; } - public ImmediateValueEntry AddValue(StyledPropertyBase property, T value) + public ImmediateValueEntry AddValue(StyledProperty property, T value) { var e = new ImmediateValueEntry(this, property, value); Add(e); diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 8acb885604..5908d9e535 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -10,14 +10,16 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; - public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase property) + public LocalValueBindingObserver(ValueStore owner, StyledProperty property) { _owner = owner; Property = property; } - public StyledPropertyBase Property { get;} + public StyledProperty Property { get;} public void Start(IObservable source) { @@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - static void Execute(ValueStore owner, StyledPropertyBase property, T value) + static void Execute(LocalValueBindingObserver instance, T value) { - if (property.ValidateValue?.Invoke(value) != false) - owner.SetValue(property, value, BindingPriority.LocalValue); - else - owner.ClearLocalValue(property); + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetValue(property, value, BindingPriority.LocalValue); } if (Dispatcher.UIThread.CheckAccess()) { - Execute(_owner, Property, value); + Execute(this, value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. - var instance = _owner; - var property = Property; + var instance = this; var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, property, newValue)); + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } @@ -74,11 +78,21 @@ namespace Avalonia.PropertyStore LoggingUtils.LogIfNecessary(owner.Owner, property, value); if (value.HasValue) - owner.SetValue(property, value.Value, BindingPriority.LocalValue); - else if (value.Type != BindingValueType.DataValidationError) - owner.ClearLocalValue(property); + { + var effectiveValue = value.Value; + if (property.ValidateValue?.Invoke(effectiveValue) == false) + effectiveValue = instance.GetCachedDefaultValue(); + owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); + } + else + { + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); + } } + if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -92,5 +106,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 7c529591b6..46e6ed810a 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography; using Avalonia.Data; using Avalonia.Threading; @@ -10,14 +9,16 @@ namespace Avalonia.PropertyStore { private readonly ValueStore _owner; private IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; - public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase property) + public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) { _owner = owner; Property = property; } - public StyledPropertyBase Property { get; } + public StyledProperty Property { get; } public void Start(IObservable source) { @@ -49,11 +50,7 @@ namespace Avalonia.PropertyStore if (value == AvaloniaProperty.UnsetValue) { - owner.ClearLocalValue(property); - } - else if (value == BindingOperations.DoNothing) - { - // Do nothing! + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); } else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) { @@ -61,11 +58,14 @@ namespace Avalonia.PropertyStore } else { - owner.ClearLocalValue(property); + owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); } } + if (value == BindingOperations.DoNothing) + return; + if (Dispatcher.UIThread.CheckAccess()) { Execute(this, value); @@ -79,5 +79,16 @@ namespace Avalonia.PropertyStore Dispatcher.UIThread.Post(() => Execute(instance, newValue)); } } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } } } diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b4ac06d2bf..b82714817b 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -13,14 +13,14 @@ namespace Avalonia.PropertyStore public SourceUntypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable source) : base(frame, property, source) { _validate = property.ValidateValue; } - public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + public new StyledProperty Property => (StyledProperty)base.Property; protected override BindingValue ConvertAndValidate(object? value) { @@ -31,5 +31,7 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override TTarget GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 2276991a18..550f5c0001 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -11,7 +11,7 @@ namespace Avalonia.PropertyStore { public TypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable source) : base(frame, property, source) { @@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore public TypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable> source) : base(frame, property, source) { } - public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + public new StyledProperty Property => (StyledProperty)base.Property; protected override BindingValue ConvertAndValidate(T value) { @@ -48,5 +48,7 @@ namespace Avalonia.PropertyStore return value; } + + protected override T GetDefaultValue(Type ownerType) => Property.GetDefaultValue(ownerType); } } diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs index f8becb2e06..a77d7fddb6 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -29,5 +29,10 @@ namespace Avalonia.PropertyStore { throw new NotSupportedException(); } + + protected override object? GetDefaultValue(Type ownerType) + { + return ((IStyledPropertyMetadata)Property.GetMetadata(ownerType)).DefaultValue; + } } } diff --git a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs index 5c5591dcb5..372a808fb2 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs @@ -26,7 +26,7 @@ namespace Avalonia.PropertyStore [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] public static bool TryConvertAndValidate( - StyledPropertyBase property, + StyledProperty property, object? value, [MaybeNullWhen(false)] out T result) { diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 92e5288255..ec6ed392c1 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -7,7 +7,6 @@ using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Styling; using Avalonia.Utilities; -using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -43,7 +42,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable> source, BindingPriority priority) { @@ -71,7 +70,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority) { @@ -99,7 +98,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority) { @@ -156,16 +155,17 @@ namespace Avalonia.PropertyStore return observer; } - public void ClearLocalValue(AvaloniaProperty property) + public void ClearValue(AvaloniaProperty property) { if (TryGetEffectiveValue(property, out var effective) && - effective.Priority == BindingPriority.LocalValue) + (effective.Priority == BindingPriority.LocalValue || effective.IsOverridenCurrentValue)) { + effective.IsOverridenCurrentValue = false; ReevaluateEffectiveValue(property, effective, ignoreLocalValue: true); } } - public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) + public IDisposable? SetValue(StyledProperty property, T value, BindingPriority priority) { if (property.ValidateValue?.Invoke(value) == false) { @@ -184,7 +184,7 @@ namespace Avalonia.PropertyStore } else { - var effectiveValue = new EffectiveValue(Owner, property); + var effectiveValue = CreateEffectiveValue(property); AddEffectiveValue(property, effectiveValue); effectiveValue.SetAndRaise(this, result, priority); } @@ -200,7 +200,7 @@ namespace Avalonia.PropertyStore } else { - var effectiveValue = new EffectiveValue(Owner, property); + var effectiveValue = CreateEffectiveValue(property); AddEffectiveValue(property, effectiveValue); effectiveValue.SetLocalValueAndRaise(this, property, value); } @@ -209,6 +209,20 @@ namespace Avalonia.PropertyStore } } + public void SetCurrentValue(StyledProperty property, T value) + { + if (TryGetEffectiveValue(property, out var v)) + { + ((EffectiveValue)v).SetCurrentValueAndRaise(this, property, value); + } + else + { + var effectiveValue = CreateEffectiveValue(property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetCurrentValueAndRaise(this, property, value); + } + } + public object? GetValue(AvaloniaProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) @@ -219,7 +233,7 @@ namespace Avalonia.PropertyStore return GetDefaultValue(property); } - public T GetValue(StyledPropertyBase property) + public T GetValue(StyledProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) return ((EffectiveValue)v).Value; @@ -235,12 +249,7 @@ namespace Avalonia.PropertyStore return false; } - public bool IsSet(AvaloniaProperty property) - { - if (_effectiveValues.TryGetValue(property, out var v)) - return v.Priority < BindingPriority.Inherited; - return false; - } + public bool IsSet(AvaloniaProperty property) => _effectiveValues.TryGetValue(property, out _); public void CoerceValue(AvaloniaProperty property) { @@ -248,7 +257,7 @@ namespace Avalonia.PropertyStore v.CoerceValue(this, property); } - public Optional GetBaseValue(StyledPropertyBase property) + public Optional GetBaseValue(StyledProperty property) { if (TryGetEffectiveValue(property, out var v) && ((EffectiveValue)v).TryGetBaseValue(out var baseValue)) @@ -278,6 +287,16 @@ namespace Avalonia.PropertyStore return false; } + public EffectiveValue CreateEffectiveValue(StyledProperty property) + { + EffectiveValue? inherited = null; + + if (property.Inherits && TryGetInheritedValue(property, out var v)) + inherited = (EffectiveValue)v; + + return new EffectiveValue(Owner, property, inherited); + } + public void SetInheritanceParent(AvaloniaObject? newParent) { var values = AvaloniaPropertyDictionaryPool.Get(); @@ -380,23 +399,6 @@ namespace Avalonia.PropertyStore } } - /// - /// Called by non-LocalValue binding entries to re-evaluate the effective value when the - /// binding produces an unset value. - /// - /// The bound property. - /// The priority of binding which produced a new value. - public void OnBindingValueCleared(AvaloniaProperty property, BindingPriority priority) - { - Debug.Assert(priority != BindingPriority.LocalValue); - - if (TryGetEffectiveValue(property, out var existing)) - { - if (priority <= existing.Priority) - ReevaluateEffectiveValue(property, existing); - } - } - /// /// Called by a when its /// state changes. @@ -450,7 +452,7 @@ namespace Avalonia.PropertyStore /// The old value of the property. /// The effective value instance. public void OnInheritedEffectiveValueChanged( - StyledPropertyBase property, + StyledProperty property, T oldValue, EffectiveValue value) { @@ -475,7 +477,7 @@ namespace Avalonia.PropertyStore /// /// The property whose value changed. /// The old value of the property. - public void OnInheritedEffectiveValueDisposed(StyledPropertyBase property, T oldValue) + public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue) { Debug.Assert(property.Inherits); @@ -507,7 +509,7 @@ namespace Avalonia.PropertyStore if (existing == observer) { _localValueBindings?.Remove(property.Id); - ClearLocalValue(property); + ClearValue(property); } } } @@ -520,7 +522,7 @@ namespace Avalonia.PropertyStore /// The old value of the property. /// The new value of the property. public void OnAncestorInheritedValueChanged( - StyledPropertyBase property, + StyledProperty property, T oldValue, T newValue) { @@ -633,11 +635,13 @@ namespace Avalonia.PropertyStore { object? value; BindingPriority priority; + bool overridden = false; if (_effectiveValues.TryGetValue(property, out var v)) { value = v.Value; priority = v.Priority; + overridden = v.IsOverridenCurrentValue; } else if (property.Inherits && TryGetInheritedValue(property, out v)) { @@ -654,7 +658,8 @@ namespace Avalonia.PropertyStore property, value, priority, - null); + null, + overridden); } private int InsertFrame(ValueFrame frame) @@ -804,7 +809,7 @@ namespace Avalonia.PropertyStore // - The value is a non-animation value and its priority is higher than the current // effective value's base priority var isRelevantPriority = current is null || - priority < current.Priority || + (priority < current.Priority && priority < current.BasePriority) || (priority > BindingPriority.Animation && priority < current.BasePriority); if (foundEntry && isRelevantPriority && entry!.HasValue) diff --git a/src/Avalonia.Base/Reactive/AnonymousObserver.cs b/src/Avalonia.Base/Reactive/AnonymousObserver.cs index c2e02ae879..6c458713dc 100644 --- a/src/Avalonia.Base/Reactive/AnonymousObserver.cs +++ b/src/Avalonia.Base/Reactive/AnonymousObserver.cs @@ -1,9 +1,14 @@ using System; +using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Avalonia.Reactive; -internal class AnonymousObserver : IObserver +/// +/// Class to create an instance from delegate-based implementations of the On* methods. +/// +/// The type of the elements in the sequence. +public class AnonymousObserver : IObserver { private static readonly Action ThrowsOnError = ex => throw ex; private static readonly Action NoOpCompleted = () => { }; diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index 263109972f..cf20f20172 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -14,7 +14,7 @@ namespace Avalonia.Reactive /// usage. This class provides a more lightweight base for some internal observable types /// in the Avalonia framework. /// - public abstract class LightweightObservableBase : IObservable + internal abstract class LightweightObservableBase : IObservable { private Exception? _error; private List>? _observers = new List>(); diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index 831ab28adc..49dc087933 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Numerics; using Avalonia.Animation.Animators; using Avalonia.Utilities; @@ -441,6 +442,32 @@ namespace Avalonia return new Rect(new Point(left, top), new Point(right, bottom)); } + + internal Rect TransformToAABB(Matrix4x4 matrix) + { + ReadOnlySpan points = stackalloc Point[4] + { + TopLeft.Transform(matrix), + TopRight.Transform(matrix), + BottomRight.Transform(matrix), + BottomLeft.Transform(matrix) + }; + + var left = double.MaxValue; + var right = double.MinValue; + var top = double.MaxValue; + var bottom = double.MinValue; + + foreach (var p in points) + { + if (p.X < left) left = p.X; + if (p.X > right) right = p.X; + if (p.Y < top) top = p.Y; + if (p.Y > bottom) bottom = p.Y; + } + + return new Rect(new Point(left, top), new Point(right, bottom)); + } /// /// Translates the rectangle by an offset. diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs index a6db4330a3..455e9ebb5f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimation.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations public abstract class CompositionAnimation : CompositionObject, ICompositionAnimationBase { private readonly CompositionPropertySet _propertySet; - internal CompositionAnimation(Compositor compositor) : base(compositor, null!) + internal CompositionAnimation(Compositor compositor) : base(compositor, null) { _propertySet = new CompositionPropertySet(compositor); } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs index bad3991f43..1500e88abe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/CompositionAnimationGroup.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.Composition.Animations public void Remove(CompositionAnimation value) => Animations.Remove(value); public void RemoveAll() => Animations.Clear(); - public CompositionAnimationGroup(Compositor compositor) : base(compositor, null!) + public CompositionAnimationGroup(Compositor compositor) : base(compositor, null) { } } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs index 72be4edd07..d9adf261f8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs +++ b/src/Avalonia.Base/Rendering/Composition/Animations/ImplicitAnimationCollection.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition.Animations { private Dictionary _inner = new Dictionary(); private IDictionary _innerface; - internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null!) + internal ImplicitAnimationCollection(Compositor compositor) : base(compositor, null) { _innerface = _inner; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b2080aeb87..7fa2d4955f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Media; -using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Threading; using Avalonia.VisualTree; // Special license applies License.md @@ -23,21 +20,37 @@ public class CompositingRenderer : IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; - CompositionDrawingContext _recorder = new(); - DrawingContext _recordingContext; - private HashSet _dirty = new(); - private HashSet _recalculateChildren = new(); + private readonly CompositionDrawingContext _recorder = new(); + private readonly DrawingContext _recordingContext; + private readonly HashSet _dirty = new(); + private readonly HashSet _recalculateChildren = new(); + private readonly Action _update; + private bool _queuedUpdate; - private Action _update; private bool _updating; + private bool _isDisposed; - internal CompositionTarget CompositionTarget; + internal CompositionTarget CompositionTarget { get; } /// /// Asks the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered. /// public bool RenderOnlyOnRenderThread { get; set; } = true; + /// + public RendererDiagnostics Diagnostics { get; } + + /// + public Compositor Compositor => _compositor; + + /// + /// Initializes a new instance of + /// + /// The render root using this renderer. + /// The associated compositors. + /// + /// A function returning the list of native platform's surfaces that can be consumed by rendering subsystems. + /// public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> surfaces) { _root = root; @@ -46,45 +59,56 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; + Diagnostics = new RendererDiagnostics(); + Diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged; } - /// - public bool DrawFps - { - get => CompositionTarget.DrawFps; - set => CompositionTarget.DrawFps = value; - } - - /// - public bool DrawDirtyRects + private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e) { - get => CompositionTarget.DrawDirtyRects; - set => CompositionTarget.DrawDirtyRects = value; + switch (e.PropertyName) + { + case nameof(RendererDiagnostics.DebugOverlays): + CompositionTarget.DebugOverlays = Diagnostics.DebugOverlays; + break; + case nameof(RendererDiagnostics.LastLayoutPassTiming): + CompositionTarget.LastLayoutPassTiming = Diagnostics.LastLayoutPassTiming; + break; + } } /// public event EventHandler? SceneInvalidated; - void QueueUpdate() + private void QueueUpdate() { if(_queuedUpdate) return; _queuedUpdate = true; - _compositor.InvokeBeforeNextCommit(_update); + _compositor.RequestCompositionUpdate(_update); } /// public void AddDirty(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _dirty.Add((Visual)visual); + _dirty.Add(visual); QueueUpdate(); } /// - public IEnumerable HitTest(Point p, Visual root, Func? filter) + public IEnumerable HitTest(Point p, Visual? root, Func? filter) { + CompositionVisual? rootVisual = null; + if (root != null) + { + if (root.CompositionVisual == null) + yield break; + rootVisual = root.CompositionVisual; + } + Func? f = null; if (filter != null) f = v => @@ -93,8 +117,8 @@ public class CompositingRenderer : IRendererWithCompositor return filter(dlv.Visual); return true; }; - - var res = CompositionTarget.TryHitTest(p, f); + + var res = CompositionTarget.TryHitTest(p, rootVisual, f); if(res == null) yield break; foreach(var v in res) @@ -117,9 +141,11 @@ public class CompositingRenderer : IRendererWithCompositor /// public void RecalculateChildren(Visual visual) { + if (_isDisposed) + return; if (_updating) throw new InvalidOperationException("Visual was invalidated during the render pass"); - _recalculateChildren.Add((Visual)visual); + _recalculateChildren.Add(visual); QueueUpdate(); } @@ -162,7 +188,7 @@ public class CompositingRenderer : IRendererWithCompositor if (sortedChildren != null) for (var c = 0; c < visualChildren.Count; c++) { - if (!ReferenceEquals(compositionChildren[c], ((Visual)sortedChildren[c].visual).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], sortedChildren[c].visual.CompositionVisual)) { mismatch = true; break; @@ -170,7 +196,7 @@ public class CompositingRenderer : IRendererWithCompositor } else for (var c = 0; c < visualChildren.Count; c++) - if (!ReferenceEquals(compositionChildren[c], ((Visual)visualChildren[c]).CompositionVisual)) + if (!ReferenceEquals(compositionChildren[c], visualChildren[c].CompositionVisual)) { mismatch = true; break; @@ -192,7 +218,7 @@ public class CompositingRenderer : IRendererWithCompositor { foreach (var ch in sortedChildren) { - var compositionChild = ((Visual)ch.visual).CompositionVisual; + var compositionChild = ch.visual.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -201,7 +227,7 @@ public class CompositingRenderer : IRendererWithCompositor else foreach (var ch in v.GetVisualChildren()) { - var compositionChild = ((Visual)ch).CompositionVisual; + var compositionChild = ch.CompositionVisual; if (compositionChild != null) compositionChildren.Add(compositionChild); } @@ -257,6 +283,7 @@ public class CompositingRenderer : IRendererWithCompositor _recalculateChildren.Clear(); CompositionTarget.Size = _root.ClientSize; CompositionTarget.Scaling = _root.RenderScaling; + TriggerSceneInvalidatedOnBatchCompletion(_compositor.RequestCommitAsync()); } private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion) @@ -265,7 +292,7 @@ public class CompositingRenderer : IRendererWithCompositor SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); } - private void Update(Task batchCompletion) + private void Update() { if(_updating) return; @@ -279,13 +306,18 @@ public class CompositingRenderer : IRendererWithCompositor _updating = false; } } - + + /// public void Resized(Size size) { } + /// public void Paint(Rect rect) { + if (_isDisposed) + return; + QueueUpdate(); CompositionTarget.RequestRedraw(); if(RenderOnlyOnRenderThread && Compositor.Loop.RunsInBackground) @@ -294,17 +326,34 @@ public class CompositingRenderer : IRendererWithCompositor CompositionTarget.ImmediateUIThreadRender(); } - public void Start() => CompositionTarget.IsEnabled = true; - - public void Stop() + /// + public void Start() { - CompositionTarget.IsEnabled = false; + if (_isDisposed) + return; + + CompositionTarget.IsEnabled = true; } - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + /// + public void Stop() + => CompositionTarget.IsEnabled = false; + + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + => Compositor.TryGetRenderInterfaceFeature(featureType); + /// public void Dispose() { + if (_isDisposed) + return; + + _isDisposed = true; + _dirty.Clear(); + _recalculateChildren.Clear(); + SceneInvalidated = null; + Stop(); CompositionTarget.Dispose(); @@ -313,9 +362,4 @@ public class CompositingRenderer : IRendererWithCompositor if (Compositor.Loop.RunsInBackground) _compositor.Commit().Wait(); } - - /// - /// The associated object - /// - public Compositor Compositor => _compositor; } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index b019d1792b..f1905fec08 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -61,9 +61,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual return false; if (custom != null) { - // Simulate the old behavior - // TODO: Change behavior once legacy renderers are removed - pt += new Point(Offset.X, Offset.Y); return custom.HitTest(pt); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs new file mode 100644 index 0000000000..bfe70d593d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition; + +public class CompositionDrawingSurface : CompositionSurface +{ + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!; + internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) + { + } + + /// + /// Updates the surface contents using an imported memory image using a keyed mutex as the means of synchronization + /// + /// GPU image with new surface contents + /// The mutex key to wait for before accessing the image + /// The mutex key to release for after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithKeyedMutexAsync(ICompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithKeyedMutex(img, acquireIndex, releaseIndex)); + } + + /// + /// Updates the surface contents using an imported memory image using a semaphore pair as the means of synchronization + /// + /// GPU image with new surface contents + /// The semaphore to wait for before accessing the image + /// The semaphore to signal after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithSemaphoresAsync(ICompositionImportedGpuImage image, + ICompositionImportedGpuSemaphore waitForSemaphore, + ICompositionImportedGpuSemaphore signalSemaphore) + { + var img = (CompositionImportedGpuImage)image; + var wait = (CompositionImportedGpuSemaphore)waitForSemaphore; + var signal = (CompositionImportedGpuSemaphore)signalSemaphore; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithSemaphores(img, wait, signal)); + } + + /// + /// Updates the surface contents using an unspecified automatic means of synchronization + /// provided by the underlying platform + /// + /// GPU image with new surface contents + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateAsync(ICompositionImportedGpuImage image) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithAutomaticSync(img)); + } + + ~CompositionDrawingSurface() + { + Dispatcher.UIThread.Post(Dispose); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs new file mode 100644 index 0000000000..ce728f86a2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; +public interface ICompositionGpuInterop +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + /// + /// Returns the supported ways to synchronize access to the imported GPU image + /// + /// + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// + ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + /// An image that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// + /// A semaphore that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image); + + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + public bool IsLost { get; } + + /// + /// The LUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceLuid { get; set; } + + /// + /// The UUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceUuid { get; set; } +} + +[Flags] +public enum CompositionGpuImportedImageSynchronizationCapabilities +{ + /// + /// Pre-render and after-render semaphores must be provided alongside with the image + /// + Semaphores = 1, + /// + /// Image must be created with D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX or in other compatible way + /// + KeyedMutex = 2, + /// + /// Synchronization and ordering is somehow handled by the underlying platform + /// + Automatic = 4 +} + +/// +/// An imported GPU object that's usable by composition APIs +/// +public interface ICompositionGpuImportedObject : IAsyncDisposable +{ + /// + /// Tracks the import status of the object. Once the task is completed, + /// the user code is allowed to free the resource owner in case when a non-owning + /// sharing handle was used + /// + Task ImportCompeted { get; } + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + bool IsLost { get; } +} + +/// +/// An imported GPU image object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuImage : ICompositionGpuImportedObject +{ + +} + +/// +/// An imported GPU semaphore object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuSemaphore : ICompositionGpuImportedObject +{ + +} + +/// +/// An GPU object descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextObject : IDisposable +{ +} + +/// +/// An GPU image descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextImage : IDisposable +{ +} + +/// +/// An GPU semaphore descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextSemaphore : IDisposable +{ +} + diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs new file mode 100644 index 0000000000..1643ec6e8d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionInterop : ICompositionGpuInterop +{ + private readonly Compositor _compositor; + private readonly IPlatformRenderInterfaceContext _context; + private readonly IExternalObjectsRenderInterfaceContextFeature _externalObjects; + + + public CompositionInterop( + Compositor compositor, + IExternalObjectsRenderInterfaceContextFeature externalObjects) + { + _compositor = compositor; + _context = compositor.Server.RenderInterface.Value; + DeviceLuid = externalObjects.DeviceLuid; + DeviceUuid = externalObjects.DeviceUuid; + _externalObjects = externalObjects; + } + + public IReadOnlyList SupportedImageHandleTypes => _externalObjects.SupportedImageHandleTypes; + public IReadOnlyList SupportedSemaphoreTypes => _externalObjects.SupportedSemaphoreTypes; + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _externalObjects.GetSynchronizationCapabilities(imageHandleType); + + public ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + => new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(handle, properties)); + + public ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + return new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(image)); + } + + public ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle) + => new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects); + + public ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image) + { + throw new System.NotSupportedException(); + } + + public bool IsLost { get; } + public byte[]? DeviceLuid { get; set; } + public byte[]? DeviceUuid { get; set; } +} + +abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject +{ + protected Compositor Compositor { get; } + public IPlatformRenderInterfaceContext Context { get; } + public IExternalObjectsRenderInterfaceContextFeature Feature { get; } + + public CompositionGpuImportedObjectBase(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) + { + Compositor = compositor; + Context = context; + Feature = feature; + + ImportCompeted = Compositor.InvokeServerJobAsync(Import); + } + + protected abstract void Import(); + public abstract void Dispose(); + + public Task ImportCompeted { get; } + public bool IsLost => Context.IsLost; + + public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() => + { + if (ImportCompeted.Status == TaskStatus.RanToCompletion) + Dispose(); + })); +} + +class CompositionImportedGpuImage : CompositionGpuImportedObjectBase, ICompositionImportedGpuImage +{ + private readonly Func _importer; + private IPlatformRenderInterfaceImportedImage? _image; + + public CompositionImportedGpuImage(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature, + Func importer): base(compositor, context, feature) + { + _importer = importer; + } + + protected override void Import() + { + using (Compositor.Server.RenderInterface.EnsureCurrent()) + { + // The original context was lost and the new one might have different capabilities + if (Context != Compositor.Server.RenderInterface.Value) + throw new PlatformGraphicsContextLostException(); + _image = _importer(); + } + } + + public IPlatformRenderInterfaceImportedImage Image => + _image ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuImage)); + + public bool IsUsable => _image != null && Compositor.Server.RenderInterface.Value == Context; + + public override void Dispose() + { + _image?.Dispose(); + _image = null!; + } +} + +class CompositionImportedGpuSemaphore : CompositionGpuImportedObjectBase, ICompositionImportedGpuSemaphore +{ + private readonly IPlatformHandle _handle; + private IPlatformRenderInterfaceImportedSemaphore? _semaphore; + + public CompositionImportedGpuSemaphore(IPlatformHandle handle, + Compositor compositor, IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature) + { + _handle = handle; + } + + public IPlatformRenderInterfaceImportedSemaphore Semaphore => + _semaphore ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuSemaphore)); + + + public bool IsUsable => _semaphore != null && Compositor.Server.RenderInterface.Value == Context; + + protected override void Import() + { + _semaphore = Feature.ImportSemaphore(_handle); + } + + public override void Dispose() + { + _semaphore?.Dispose(); + _semaphore = null; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 50332926ad..8c21b534db 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -22,7 +22,7 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection? ImplicitAnimations { get; set; } private protected InlineDictionary PendingAnimations; - internal CompositionObject(Compositor compositor, ServerObject server) + internal CompositionObject(Compositor compositor, ServerObject? server) { Compositor = compositor; Server = server; @@ -32,7 +32,7 @@ namespace Avalonia.Rendering.Composition /// The associated Compositor /// public Compositor Compositor { get; } - internal ServerObject Server { get; } + internal ServerObject? Server { get; } public bool IsDisposed { get; private set; } private bool _registeredForSerialization; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index 7d794af9a2..efd89951bb 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -23,7 +23,7 @@ namespace Avalonia.Rendering.Composition private readonly Dictionary _variants = new Dictionary(); private readonly Dictionary _objects = new Dictionary(); - internal CompositionPropertySet(Compositor compositor) : base(compositor, null!) + internal CompositionPropertySet(Compositor compositor) : base(compositor, null) { } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs new file mode 100644 index 0000000000..0edd8ac732 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs @@ -0,0 +1,10 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionSurface : CompositionObject +{ + internal CompositionSurface(Compositor compositor, ServerObject server) : base(compositor, server) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index 0c24d6cd44..f2fa846205 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -28,16 +28,15 @@ namespace Avalonia.Rendering.Composition /// /// Attempts to perform a hit-tst /// - /// - /// /// - public PooledList? TryHitTest(Point point, Func? filter) + public PooledList? TryHitTest(Point point, CompositionVisual? root, Func? filter) { Server.Readback.NextRead(); - if (Root == null) + root ??= Root; + if (root == null) return null; var res = new PooledList(); - HitTestCore(Root, point, res, filter); + HitTestCore(root, point, res, filter); return res; } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 2b1b3f461f..801dd32d59 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -15,7 +14,7 @@ public partial class Compositor /// public CompositionTarget CreateCompositionTarget(Func> surfaces) { - return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces)); + return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); } public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); @@ -35,4 +34,8 @@ public partial class Compositor new(this, new ServerCompositionSolidColorVisual(Server)); public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); -} \ No newline at end of file + + public CompositionSurfaceVisual CreateSurfaceVisual() => new(this, new ServerCompositionSurfaceVisual(_server)); + + public CompositionDrawingSurface CreateDrawingSurface() => new(this); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 7fc5487171..153b32c5f3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -30,14 +30,19 @@ namespace Avalonia.Rendering.Composition private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); - private Queue> _invokeBeforeCommit = new(); + private Queue _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); private List _pendingServerCompositorJobs = new(); + private DiagnosticTextRenderer? _diagnosticTextRenderer; internal IEasing DefaultEasing { get; } - + + private DiagnosticTextRenderer DiagnosticTextRenderer + => _diagnosticTextRenderer ??= new(Typeface.Default.GlyphTypeface, 12.0); + + internal event Action? AfterCommit; /// /// Creates a new compositor on a specified render loop that would use a particular GPU @@ -77,16 +82,31 @@ namespace Avalonia.Rendering.Composition return _nextCommit.Task; } - + internal Task Commit() + { + try + { + return CommitCore(); + } + finally + { + if (_invokeBeforeCommitWrite.Count > 0) + RequestCommitAsync(); + AfterCommit?.Invoke(); + } + } + + Task CommitCore() { Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); _nextCommit ??= new TaskCompletionSource(); - while (_invokeBeforeCommit.Count > 0) - _invokeBeforeCommit.Dequeue()(_nextCommit.Task); + (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); + while (_invokeBeforeCommitRead.Count > 0) + _invokeBeforeCommitRead.Dequeue()(); var batch = new Batch(_nextCommit); @@ -109,6 +129,7 @@ namespace Avalonia.Rendering.Composition writer.WriteObject(job); writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); } + _pendingServerCompositorJobs.Clear(); } batch.CommittedAt = Server.Clock.Elapsed; @@ -138,34 +159,73 @@ namespace Avalonia.Rendering.Composition RequestCommitAsync(); } - internal void InvokeBeforeNextCommit(Action action) + /// + /// Enqueues a callback to be called before the next scheduled commit. + /// If there is no scheduled commit it automatically schedules one + /// This is useful for updating your composition tree objects after binding + /// and layout passes have completed + /// + public void RequestCompositionUpdate(Action action) { Dispatcher.UIThread.VerifyAccess(); - _invokeBeforeCommit.Enqueue(action); + _invokeBeforeCommitWrite.Enqueue(action); RequestCommitAsync(); } - /// - /// Attempts to query for a feature from the platform render interface - /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) + internal void PostServerJob(Action job) + { + Dispatcher.UIThread.VerifyAccess(); + _pendingServerCompositorJobs.Add(job); + RequestCommitAsync(); + } + + internal Task InvokeServerJobAsync(Action job) => + InvokeServerJobAsync(() => + { + job(); + return null; + }); + + internal Task InvokeServerJobAsync(Func job) { - var tcs = new TaskCompletionSource(); - _pendingServerCompositorJobs.Add(() => + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + PostServerJob(() => { try { - using (Server.RenderInterface.EnsureCurrent()) - { - tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); - } + tcs.SetResult(job()); } catch (Exception e) { tcs.TrySetException(e); } }); - return new ValueTask(tcs.Task); + return tcs.Task; } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + return Server.RenderInterface.Value.TryGetFeature(featureType); + } + })); + + public ValueTask TryGetCompositionGpuInterop() => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + var feature = Server.RenderInterface.Value + .TryGetFeature(); + if (feature == null) + return null; + return new CompositionInterop(this, feature); + } + })); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index aae1fcb90e..6b380608fe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -88,8 +88,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { + if (pen is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) @@ -159,8 +164,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { + if (foreground is null) + { + return; + } + var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) @@ -303,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity, bounds)) { - Add(new OpacityNode(opacity)); + Add(new OpacityNode(opacity, bounds)); } else { diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index ff2069e71e..b15da5d05d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -39,7 +39,8 @@ namespace Avalonia.Rendering.Composition.Expressions } } - internal class PrettyPrintStringAttribute : Attribute + [AttributeUsage(AttributeTargets.Field)] + internal sealed class PrettyPrintStringAttribute : Attribute { public string Name { get; } @@ -164,8 +165,6 @@ namespace Avalonia.Rendering.Composition.Expressions public override ExpressionVariant Evaluate(ref ExpressionEvaluationContext context) { - if (context.ForeignFunctionInterface == null) - return default; var args = new List(); foreach (var expr in Parameters) args.Add(expr.Evaluate(ref context)); diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs index 9086c59aad..f268364b54 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/ExpressionEvaluationContext.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Rendering.Composition.Server; // Special license applies License.md diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs new file mode 100644 index 0000000000..b01fb46aa3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -0,0 +1,78 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server +{ + /// + /// A class used to render diagnostic strings (only!), with caching of ASCII glyph runs. + /// + internal sealed class DiagnosticTextRenderer + { + private const char FirstChar = (char)32; + private const char LastChar = (char)126; + + private readonly GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; + + public double GetMaxHeight() + { + var maxHeight = 0.0; + + for (var c = FirstChar; c <= LastChar; c++) + { + var height = _runs[c - FirstChar].Size.Height; + if (height > maxHeight) + { + maxHeight = height; + } + } + + return maxHeight; + } + + public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize) + { + var chars = new char[LastChar - FirstChar + 1]; + for (var c = FirstChar; c <= LastChar; c++) + { + var index = c - FirstChar; + chars[index] = c; + var glyph = typeface.GetGlyph(c); + _runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph }); + } + } + + public Size MeasureAsciiText(ReadOnlySpan text) + { + var width = 0.0; + var height = 0.0; + + foreach (var c in text) + { + var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; + var run = _runs[effectiveChar - FirstChar]; + width += run.Size.Width; + height = Math.Max(height, run.Size.Height); + } + + return new Size(width, height); + } + + public void DrawAsciiText(IDrawingContextImpl context, ReadOnlySpan text, IBrush foreground) + { + var offset = 0.0; + var originalTransform = context.Transform; + + foreach (var c in text) + { + var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; + var run = _runs[effectiveChar - FirstChar]; + context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); + context.DrawGlyphRun(foreground, run.PlatformImpl); + offset += run.Size.Width; + } + + context.Transform = originalTransform; + } + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index e6bbba6ec0..08e506536f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -66,7 +66,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { _impl.DrawLine(pen, p1, p2); } @@ -86,7 +86,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } @@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopClip(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { - _impl.PushOpacity(opacity); + _impl.PushOpacity(opacity, bounds); } public void PopOpacity() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 18cb7a6308..03bd965fa8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -1,11 +1,8 @@ using System; using System.Diagnostics; using System.Globalization; -using System.Linq; using Avalonia.Media; -using Avalonia.Media.TextFormatting; using Avalonia.Platform; -using Avalonia.Utilities; // Special license applies License.md @@ -17,26 +14,18 @@ namespace Avalonia.Rendering.Composition.Server; internal class FpsCounter { private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private readonly DiagnosticTextRenderer _textRenderer; + private int _framesThisSecond; private int _totalFrames; private int _fps; private TimeSpan _lastFpsUpdate; - const int FirstChar = 32; - const int LastChar = 126; - // ASCII chars - private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1]; - - public FpsCounter(IGlyphTypeface typeface) - { - for (var c = FirstChar; c <= LastChar; c++) - { - var s = new string((char)c, 1); - var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); - } - } - public void FpsTick() => _framesThisSecond++; + public FpsCounter(DiagnosticTextRenderer textRenderer) + => _textRenderer = textRenderer; + + public void FpsTick() + => _framesThisSecond++; public void RenderFps(IDrawingContextImpl context, string aux) { @@ -53,27 +42,24 @@ internal class FpsCounter _lastFpsUpdate = now; } - var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} ") + aux; - double width = 0; - double height = 0; - foreach (var ch in fpsLine) - { - var run = _runs[ch - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); - } +#if NET6_0_OR_GREATER + var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); +#else + var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}"); +#endif - var rect = new Rect(0, 0, width + 3, height + 3); + var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan()); + var rect = new Rect(0.0, 0.0, size.Width + 3.0, size.Height + 3.0); context.DrawRectangle(Brushes.Black, null, rect); - double offset = 0; - foreach (var ch in fpsLine) - { - var run = _runs[ch - FirstChar]; - context.Transform = Matrix.CreateTranslation(offset, 0); - context.DrawGlyphRun(Brushes.White, run); - offset += run.Size.Width; - } + _textRenderer.DrawAsciiText(context, fpsLine.AsSpan(), Brushes.White); + } + + public void Reset() + { + _framesThisSecond = 0; + _totalFrames = 0; + _fps = 0; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs new file mode 100644 index 0000000000..d103b068a6 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs @@ -0,0 +1,176 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +/// +/// Represents a simple time graph for diagnostics purpose, used to show layout and render times. +/// +internal sealed class FrameTimeGraph +{ + private const double HeaderPadding = 2.0; + + private readonly IPlatformRenderInterface _renderInterface; + private readonly ImmutableSolidColorBrush _borderBrush; + private readonly ImmutablePen _graphPen; + private readonly double[] _frameValues; + private readonly Size _size; + private readonly Size _headerSize; + private readonly Size _graphSize; + private readonly double _defaultMaxY; + private readonly string _title; + private readonly DiagnosticTextRenderer _textRenderer; + + private int _startFrameIndex; + private int _frameCount; + + public Size Size + => _size; + + public FrameTimeGraph(int maxFrames, Size size, double defaultMaxY, string title, + DiagnosticTextRenderer textRenderer) + { + Debug.Assert(maxFrames >= 1); + Debug.Assert(size.Width > 0.0); + Debug.Assert(size.Height > 0.0); + + _renderInterface = AvaloniaLocator.Current.GetRequiredService(); + _borderBrush = new ImmutableSolidColorBrush(0x80808080); + _graphPen = new ImmutablePen(Brushes.Blue); + _frameValues = new double[maxFrames]; + _size = size; + _headerSize = new Size(size.Width, textRenderer.GetMaxHeight() + HeaderPadding * 2.0); + _graphSize = new Size(size.Width, size.Height - _headerSize.Height); + _defaultMaxY = defaultMaxY; + _title = title; + _textRenderer = textRenderer; + } + + public void AddFrameValue(double value) + { + if (_frameCount < _frameValues.Length) + { + _frameValues[_startFrameIndex + _frameCount] = value; + ++_frameCount; + } + else + { + // overwrite oldest value + _frameValues[_startFrameIndex] = value; + if (++_startFrameIndex == _frameValues.Length) + { + _startFrameIndex = 0; + } + } + } + + public void Reset() + { + _startFrameIndex = 0; + _frameCount = 0; + } + + public void Render(IDrawingContextImpl context) + { + var originalTransform = context.Transform; + context.PushClip(new Rect(_size)); + + context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_size))); + context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_headerSize))); + + context.Transform = originalTransform * Matrix.CreateTranslation(HeaderPadding, HeaderPadding); + _textRenderer.DrawAsciiText(context, _title.AsSpan(), Brushes.Black); + + if (_frameCount > 0) + { + var (min, avg, max) = GetYValues(); + + DrawLabelledValue(context, "Min", min, originalTransform, _headerSize.Width * 0.19); + DrawLabelledValue(context, "Avg", avg, originalTransform, _headerSize.Width * 0.46); + DrawLabelledValue(context, "Max", max, originalTransform, _headerSize.Width * 0.73); + + context.Transform = originalTransform * Matrix.CreateTranslation(0.0, _headerSize.Height); + context.DrawGeometry(null, _graphPen, BuildGraphGeometry(Math.Max(max, _defaultMaxY))); + } + + context.Transform = originalTransform; + context.PopClip(); + } + + private void DrawLabelledValue(IDrawingContextImpl context, string label, double value, in Matrix originalTransform, + double left) + { + context.Transform = originalTransform * Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding); + + var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red; + +#if NET6_0_OR_GREATER + Span buffer = stackalloc char[24]; + buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten); + _textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush); +#else + var text = FormattableString.Invariant($"{label}: {value,5:F2}ms"); + _textRenderer.DrawAsciiText(context, text.AsSpan(), brush); +#endif + } + + private IStreamGeometryImpl BuildGraphGeometry(double maxY) + { + Debug.Assert(_frameCount > 0); + + var graphGeometry = _renderInterface.CreateStreamGeometry(); + using var geometryContext = graphGeometry.Open(); + + var xRatio = _graphSize.Width / _frameValues.Length; + var yRatio = _graphSize.Height / maxY; + + geometryContext.BeginFigure(new Point(0.0, _graphSize.Height - GetFrameValue(0) * yRatio), false); + + for (var i = 1; i < _frameCount; ++i) + { + var x = Math.Round(i * xRatio); + var y = _graphSize.Height - GetFrameValue(i) * yRatio; + geometryContext.LineTo(new Point(x, y)); + } + + geometryContext.EndFigure(false); + return graphGeometry; + } + + private (double Min, double Average, double Max) GetYValues() + { + Debug.Assert(_frameCount > 0); + + var min = double.MaxValue; + var max = double.MinValue; + var total = 0.0; + + for (var i = 0; i < _frameCount; ++i) + { + var y = GetFrameValue(i); + + total += y; + + if (y < min) + { + min = y; + } + + if (y > max) + { + max = y; + } + } + + return (min, total / _frameCount, max); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetFrameValue(int frameOffset) + => _frameValues[(_startFrameIndex + frameOffset) % _frameValues.Length]; +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs new file mode 100644 index 0000000000..9c6c78c1ad --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.ExceptionServices; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisposable +{ + private IRef? _bitmap; + private IPlatformRenderInterfaceContext? _createdWithContext; + public override IRef? Bitmap + { + get + { + // Failsafe to avoid consuming an image imported with a different context + if (Compositor.RenderInterface.Value != _createdWithContext) + return null; + return _bitmap; + } + } + + public ServerCompositionDrawingSurface(ServerCompositor compositor) : base(compositor) + { + } + + void PerformSanityChecks(CompositionImportedGpuImage image) + { + // Failsafe to avoid consuming an image imported with a different context + if (!image.IsUsable) + throw new PlatformGraphicsContextLostException(); + + // This should never happen, but check for it anyway to avoid a deadlock + if (!image.ImportCompeted.IsCompleted) + throw new InvalidOperationException("The import operation is not completed yet"); + + // Rethrow the import here exception + if (image.ImportCompeted.IsFaulted) + image.ImportCompeted.GetAwaiter().GetResult(); + } + + void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context) + { + _bitmap?.Dispose(); + _bitmap = RefCountable.Create(newImage); + _createdWithContext = context; + Changed?.Invoke(); + } + + public void UpdateWithAutomaticSync(CompositionImportedGpuImage image) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithAutomaticSync(), image.Context); + } + + public void UpdateWithKeyedMutex(CompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context); + } + + public void UpdateWithSemaphores(CompositionImportedGpuImage image, CompositionImportedGpuSemaphore wait, CompositionImportedGpuSemaphore signal) + { + PerformSanityChecks(image); + if (!wait.IsUsable || !signal.IsUsable) + throw new PlatformGraphicsContextLostException(); + Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context); + } + + public void Dispose() + { + _bitmap?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs index 32a99fa187..88f10ba642 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -1,11 +1,18 @@ // Special license applies License.md +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + namespace Avalonia.Rendering.Composition.Server { - internal abstract class ServerCompositionSurface : ServerObject + internal abstract partial class ServerCompositionSurface : ServerObject { protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) { } + + public abstract IRef? Bitmap { get; } + public Action? Changed { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs new file mode 100644 index 0000000000..c75ae8e631 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -0,0 +1,48 @@ +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSurfaceVisual +{ + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if (Surface == null) + return; + if (Surface.Bitmap == null) + return; + var bmp = Surface.Bitmap.Item; + + //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI + canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + new Size(Size.X, Size.Y))); + } + + + private void OnSurfaceInvalidated() => ValuesInvalidated(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + base.OnDetachedFromRoot(target); + } + + partial void OnSurfaceChanged() + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + } + + partial void OnSurfaceChanging() + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index b172430fbb..63ec8d756b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Numerics; +using System.Diagnostics; using System.Threading; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -21,11 +21,12 @@ namespace Avalonia.Rendering.Composition.Server { private readonly ServerCompositor _compositor; private readonly Func> _surfaces; + private readonly DiagnosticTextRenderer _diagnosticTextRenderer; private static long s_nextId = 1; - public long Id { get; } - public ulong Revision { get; private set; } private IRenderTarget? _renderTarget; - private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface); + private FpsCounter? _fpsCounter; + private FrameTimeGraph? _renderTimeGraph; + private FrameTimeGraph? _layoutTimeGraph; private Rect _dirtyRect; private Random _random = new(); private Size _layerSize; @@ -35,18 +36,34 @@ namespace Avalonia.Rendering.Composition.Server private HashSet _attachedVisuals = new(); private Queue _adornerUpdateQueue = new(); + public long Id { get; } + public ulong Revision { get; private set; } public ICompositionTargetDebugEvents? DebugEvents { get; set; } public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : - base(compositor) + private FpsCounter FpsCounter + => _fpsCounter ??= new FpsCounter(_diagnosticTextRenderer); + + private FrameTimeGraph LayoutTimeGraph + => _layoutTimeGraph ??= CreateTimeGraph("Layout"); + + private FrameTimeGraph RenderTimeGraph + => _renderTimeGraph ??= CreateTimeGraph("Render"); + + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces, + DiagnosticTextRenderer diagnosticTextRenderer) + : base(compositor) { _compositor = compositor; _surfaces = surfaces; + _diagnosticTextRenderer = diagnosticTextRenderer; Id = Interlocked.Increment(ref s_nextId); } + private FrameTimeGraph CreateTimeGraph(string title) + => new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer); + partial void OnIsEnabledChanged() { if (IsEnabled) @@ -62,7 +79,33 @@ namespace Avalonia.Rendering.Composition.Server v.Deactivate(); } } - + + partial void OnDebugOverlaysChanged() + { + if ((DebugOverlays & RendererDebugOverlays.Fps) == 0) + { + _fpsCounter?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0) + { + _layoutTimeGraph?.Reset(); + } + + if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0) + { + _renderTimeGraph?.Reset(); + } + } + + partial void OnLastLayoutPassTimingChanged() + { + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) + { + LayoutTimeGraph.AddFrameValue(LastLayoutPassTiming.Elapsed.TotalMilliseconds); + } + } + partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; @@ -92,7 +135,10 @@ namespace Avalonia.Rendering.Composition.Server return; Revision++; - + + var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; + var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L; + // Update happens in a separate phase to extend dirty rect if needed Root.Update(this); @@ -137,33 +183,69 @@ namespace Avalonia.Rendering.Composition.Server targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, new Rect(_layerSize), new Rect(Size), BitmapInterpolationMode.LowQuality); - - - if (DrawDirtyRects) - { - targetContext.DrawRectangle(new ImmutableSolidColorBrush( - new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), - (byte)_random.Next(255))) - , null, _dirtyRect); - } - if (DrawFps) + if (DebugOverlays != RendererDebugOverlays.None) { - var nativeMem = ByteSizeHelper.ToString((ulong)( - (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * - Compositor.BatchMemoryPool.BufferSize), false); - var managedMem = ByteSizeHelper.ToString((ulong)( - (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * - Compositor.BatchObjectPool.ArraySize * - IntPtr.Size), false); - _fpsCounter.RenderFps(targetContext, FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}")); + if (captureTiming) + { + var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); + RenderTimeGraph.AddFrameValue(elapsed.TotalMilliseconds); + } + + DrawOverlays(targetContext); } + RenderedVisuals = 0; _dirtyRect = default; } } + private void DrawOverlays(IDrawingContextImpl targetContext) + { + if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) + { + targetContext.DrawRectangle( + new ImmutableSolidColorBrush( + new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))), + null, + _dirtyRect); + } + + if ((DebugOverlays & RendererDebugOverlays.Fps) != 0) + { + var nativeMem = ByteSizeHelper.ToString((ulong) ( + (Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) * + Compositor.BatchMemoryPool.BufferSize), false); + var managedMem = ByteSizeHelper.ToString((ulong) ( + (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * + Compositor.BatchObjectPool.ArraySize * + IntPtr.Size), false); + FpsCounter.RenderFps(targetContext, + FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}")); + } + + var top = 0.0; + + void DrawTimeGraph(FrameTimeGraph graph) + { + top += 8.0; + targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top); + graph.Render(targetContext); + top += graph.Size.Height; + } + + if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) + { + DrawTimeGraph(LayoutTimeGraph); + } + + if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0) + { + DrawTimeGraph(RenderTimeGraph); + } + } + public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling); private static Rect SnapToDevicePixels(Rect rect, double scale) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,27 +41,29 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix @@ -74,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e7405995f5..0492997200 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -22,7 +22,8 @@ namespace Avalonia.Rendering.Composition.Server { private readonly IRenderLoop _renderLoop; - private readonly Queue _batches = new Queue(); + private readonly Queue _batches = new Queue(); + private readonly Queue _receivedJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -75,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadAndExecuteJobs(stream); + ReadServerJobs(stream); continue; } @@ -97,21 +98,24 @@ namespace Avalonia.Rendering.Composition.Server } } - void ReadAndExecuteJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader) { object? readObject; while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - { - var job = (Action)readObject!; + _receivedJobQueue.Enqueue((Action)readObject!); + } + + void ExecuteServerJobs() + { + while(_receivedJobQueue.Count > 0) try { - job(); + _receivedJobQueue.Dequeue()(); } catch { // Ignore } - } } void CompletePendingBatches() @@ -160,6 +164,7 @@ namespace Avalonia.Rendering.Composition.Server try { RenderInterface.EnsureValidBackendContext(); + ExecuteServerJobs(); foreach (var t in _activeTargets) t.Render(); } diff --git a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs index d0d3dd9715..7b0fecf675 100644 --- a/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/DefaultRenderTimer.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics; -using System.Threading.Tasks; using Avalonia.Platform; namespace Avalonia.Rendering @@ -59,7 +57,8 @@ namespace Avalonia.Rendering } } - public bool RunsInBackground => true; + /// + public virtual bool RunsInBackground => true; /// /// Starts the timer. diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs deleted file mode 100644 index f0b993a2b0..0000000000 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ /dev/null @@ -1,780 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Threading; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - /// - /// A renderer which renders the state of the visual tree to an intermediate scene graph - /// representation which is then rendered on a rendering thread. - /// - public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer - { - private readonly IDispatcher? _dispatcher; - private readonly IRenderLoop? _renderLoop; - private readonly Func? _renderTargetFactory; - private readonly PlatformRenderInterfaceContextManager? _renderInterface; - private readonly Visual _root; - private readonly ISceneBuilder _sceneBuilder; - - private bool _running; - private bool _disposed; - private volatile IRef? _scene; - private DirtyVisuals? _dirty; - private HashSet? _recalculateChildren; - private IRef? _overlay; - private int _lastSceneId = -1; - private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); - private IRef? _currentDraw; - private readonly IDeferredRendererLock _lock; - private readonly object _sceneLock = new object(); - private readonly object _startStopLock = new object(); - private readonly object _renderLoopIsRenderingLock = new object(); - private readonly Action _updateSceneIfNeededDelegate; - private List? _pendingRenderThreadJobs; - - /// - /// Initializes a new instance of the class. - /// - /// The control to render. - /// The render loop. - /// The target render factory. - /// The Platform Render Context. - /// The scene builder to use. Optional. - /// The dispatcher to use. Optional. - /// Lock object used before trying to access render target - public DeferredRenderer( - IRenderRoot root, - IRenderLoop renderLoop, - Func renderTargetFactory, - PlatformRenderInterfaceContextManager? renderInterface = null, - ISceneBuilder? sceneBuilder = null, - IDispatcher? dispatcher = null, - IDeferredRendererLock? rendererLock = null) : base(true) - { - _dispatcher = dispatcher ?? Dispatcher.UIThread; - _root = root as Visual ?? throw new ArgumentNullException(nameof(root)); - _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - Layers = new RenderLayers(); - _renderLoop = renderLoop; - _renderTargetFactory = renderTargetFactory; - _renderInterface = renderInterface; - _lock = rendererLock ?? new ManagedDeferredRendererLock(); - _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; - } - - /// - /// Initializes a new instance of the class. - /// - /// The control to render. - /// The render target. - /// The scene builder to use. Optional. - /// - /// This constructor is intended to be used for unit testing. - /// - public DeferredRenderer( - Visual root, - IRenderTarget renderTarget, - ISceneBuilder? sceneBuilder = null) : base(true) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - RenderTarget = renderTarget ?? throw new ArgumentNullException(nameof(renderTarget)); - _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - Layers = new RenderLayers(); - _lock = new ManagedDeferredRendererLock(); - _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; - } - - /// - public bool DrawFps { get; set; } - - /// - public bool DrawDirtyRects { get; set; } - - /// - /// Gets or sets a path to which rendered frame should be rendered for debugging. - /// - public string? DebugFramesPath { get; set; } - - /// - /// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered - /// - public bool RenderOnlyOnRenderThread { get; set; } - - /// - public event EventHandler? SceneInvalidated; - - /// - /// Gets the render layers. - /// - internal RenderLayers Layers { get; } - - /// - /// Gets the current render target. - /// - internal IRenderTarget? RenderTarget { get; private set; } - - /// - public void AddDirty(Visual visual) - { - _dirty?.Add(visual); - } - - /// - /// Disposes of the renderer and detaches from the render loop. - /// - public void Dispose() - { - lock (_sceneLock) - { - if (_disposed) - return; - _disposed = true; - var scene = _scene; - _scene = null; - scene?.Dispose(); - } - - Stop(); - // Wait for any in-progress rendering to complete - lock(_renderLoopIsRenderingLock){} - DisposeRenderTarget(); - } - - public void RecalculateChildren(Visual visual) => _recalculateChildren?.Add(visual); - - void DisposeRenderTarget() - { - using (var l = _lock.TryLock()) - { - if(l == null) - { - // We are still trying to render on the render thread, try again a bit later - DispatcherTimer.RunOnce(DisposeRenderTarget, TimeSpan.FromMilliseconds(50), - DispatcherPriority.Background); - return; - } - - Layers.Clear(); - RenderTarget?.Dispose(); - RenderTarget = null; - } - } - - /// - public IEnumerable HitTest(Point p, Visual root, Func? filter) - { - EnsureCanHitTest(); - - //It's safe to access _scene here without a lock since - //it's only changed from UI thread which we are currently on - return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty(); - } - - /// - public Visual? HitTestFirst(Point p, Visual root, Func? filter) - { - EnsureCanHitTest(); - - //It's safe to access _scene here without a lock since - //it's only changed from UI thread which we are currently on - return _scene?.Item.HitTestFirst(p, root, filter); - } - - /// - public void Paint(Rect rect) - { - if (RenderOnlyOnRenderThread) - { - // Renderer is stopped and doesn't tick on the render thread - // This indicates a bug somewhere in our code - // (currently happens when a window gets minimized with Show desktop on Windows 10) - if(!_running) - return; - - while (true) - { - Scene? scene; - bool? updated; - lock (_sceneLock) - { - updated = UpdateScene(); - scene = _scene?.Item; - } - - // Renderer is in invalid state, skip drawing - if(updated == null) - return; - - // Wait for the scene to be rendered or disposed - scene?.Rendered.Wait(); - - // That was an up-to-date scene, we can return immediately - if (updated == true) - return; - } - } - else - { - var t = (IRenderLoopTask)this; - if (t.NeedsUpdate) - UpdateScene(); - if (_scene?.Item != null) - Render(true); - } - } - - /// - public void Resized(Size size) - { - } - - /// - public void Start() - { - lock (_startStopLock) - { - if (!_running && _renderLoop != null) - { - _renderLoop.Add(this); - _running = true; - } - } - } - - /// - public void Stop() - { - lock (_startStopLock) - { - if (_running && _renderLoop != null) - { - _renderLoop.Remove(this); - _running = false; - } - } - } - - public ValueTask TryGetRenderInterfaceFeature(Type featureType) - { - if (_renderInterface == null) - return new((object?)null); - - var tcs = new TaskCompletionSource(); - _pendingRenderThreadJobs ??= new(); - _pendingRenderThreadJobs.Add(() => - { - try - { - using (_renderInterface.EnsureCurrent()) - { - tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); - } - } - catch (Exception e) - { - tcs.TrySetException(e); - } - }); - return new ValueTask(tcs.Task); - } - - bool NeedsUpdate => _dirty == null || _dirty.Count > 0; - bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; - - void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); - - void IRenderLoopTask.Render() - { - lock (_renderLoopIsRenderingLock) - { - lock(_startStopLock) - if(!_running) - return; - Render(false); - } - } - - static Scene? TryGetChildScene(IRef? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - return TryGetChildScene(_currentDraw)?.Size ?? default; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - var childScene = TryGetChildScene(_currentDraw); - - if (childScene != null) - { - Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size)); - } - } - - internal void UnitTestUpdateScene() => UpdateScene(); - - internal void UnitTestRender() => Render(false); - - internal Scene? UnitTestScene() => _scene?.Item; - - private void EnsureCanHitTest() - { - if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) - { - // When unit testing the renderLoop may be null, so update the scene manually. - UpdateScene(); - } - } - - internal void Render(bool forceComposite) - { - using (var l = _lock.TryLock()) - { - if (l == null) - return; - - IDrawingContextImpl? context = null; - try - { - try - { - var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); - if (updated) - FpsTick(); - using (scene) - { - if (scene?.Item != null) - { - try - { - var overlay = DrawDirtyRects || DrawFps; - if (DrawDirtyRects) - _dirtyRectsDisplay.Tick(); - if (overlay) - RenderOverlay(scene.Item, ref context); - if (updated || forceComposite || overlay) - RenderComposite(scene.Item, ref context); - } - finally - { - try - { - if(scene.Item.RenderThreadJobs!=null) - foreach (var job in scene.Item.RenderThreadJobs) - job(); - } - finally - { - scene.Item.MarkAsRendered(); - } - } - } - } - } - finally - { - context?.Dispose(); - } - } - catch (RenderTargetCorruptedException ex) - { - Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); - RenderTarget?.Dispose(); - RenderTarget = null; - } - } - } - - private (IRef? scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl? context, - bool recursiveCall = false) - { - IRef? sceneRef; - lock (_sceneLock) - sceneRef = _scene?.Clone(); - if (sceneRef == null) - return (null, false); - using (sceneRef) - { - var scene = sceneRef.Item; - if (scene.Generation != _lastSceneId) - { - EnsureDrawingContext(ref context); - - Layers.Update(scene, context); - - RenderToLayers(scene); - - if (DebugFramesPath != null) - { - SaveDebugFrames(scene.Generation); - } - - lock (_sceneLock) - _lastSceneId = scene.Generation; - - - var isUiThread = Dispatcher.UIThread.CheckAccess(); - // We have consumed the previously available scene, but there might be some dirty - // rects since the last update. *If* we are on UI thread, we can force immediate scene - // rebuild before rendering anything on-screen - // We are calling the same method recursively here - if (!recursiveCall && isUiThread && NeedsUpdate) - { - UpdateScene(); - var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); - return (rs, true); - } - - // We are rendering a new scene version, so it's highly likely - // that there is already a pending update for animations - // So we are scheduling an update call so UI thread could prepare a scene before - // the next render timer tick - if (!recursiveCall && !isUiThread) - Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render); - - // Indicate that we have updated the layers - return (sceneRef.Clone(), true); - } - - // Just return scene, layers weren't updated - return (sceneRef.Clone(), false); - } - - } - - - private void Render(IDrawingContextImpl context, VisualNode node, Visual? layer, Rect clipBounds) - { - if (layer == null || node.LayerRoot == layer) - { - clipBounds = node.ClipBounds.Intersect(clipBounds); - - if (!clipBounds.IsDefault && node.Opacity > 0) - { - var isLayerRoot = node.Visual == layer; - - node.BeginRender(context, isLayerRoot); - - var drawOperations = node.DrawOperations; - var drawOperationsCount = drawOperations.Count; - for (int i = 0; i < drawOperationsCount; i++) - { - var operation = drawOperations[i]; - _currentDraw = operation; - operation.Item.Render(context); - _currentDraw = null; - } - - var children = node.Children; - var childrenCount = children.Count; - for (int i = 0; i < childrenCount; i++) - { - var child = children[i]; - Render(context, (VisualNode)child, layer, clipBounds); - } - - node.EndRender(context, isLayerRoot); - } - } - } - - private void RenderToLayers(Scene scene) - { - foreach (var layer in scene.Layers) - { - var renderLayer = Layers[layer.LayerRoot]; - if (layer.Dirty.IsEmpty && !renderLayer.IsEmpty) - continue; - var renderTarget = renderLayer.Bitmap; - var node = (VisualNode?)scene.FindNode(layer.LayerRoot); - - if (node != null) - { - using (var context = renderTarget.Item.CreateDrawingContext(this)) - { - if (renderLayer.IsEmpty) - { - // Render entire layer root node - context.Clear(Colors.Transparent); - context.Transform = Matrix.Identity; - context.PushClip(node.ClipBounds); - Render(context, node, layer.LayerRoot, node.ClipBounds); - context.PopClip(); - if (DrawDirtyRects) - { - _dirtyRectsDisplay.Add(node.ClipBounds); - } - - renderLayer.IsEmpty = false; - } - else - { - var scale = scene.Scaling; - - foreach (var rect in layer.Dirty) - { - var snappedRect = SnapToDevicePixels(rect, scale); - context.Transform = Matrix.Identity; - context.PushClip(snappedRect); - context.Clear(Colors.Transparent); - Render(context, node, layer.LayerRoot, snappedRect); - context.PopClip(); - - if (DrawDirtyRects) - { - _dirtyRectsDisplay.Add(snappedRect); - } - } - } - } - } - } - } - - private static Rect SnapToDevicePixels(Rect rect, double scale) - { - return new Rect( - new Point( - Math.Floor(rect.X * scale) / scale, - Math.Floor(rect.Y * scale) / scale), - new Point( - Math.Ceiling(rect.Right * scale) / scale, - Math.Ceiling(rect.Bottom * scale) / scale)); - } - - private void RenderOverlay(Scene scene, ref IDrawingContextImpl? parentContent) - { - EnsureDrawingContext(ref parentContent); - - if (DrawDirtyRects) - { - var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); - - using (var context = overlay.Item.CreateDrawingContext(this)) - { - context.Clear(Colors.Transparent); - RenderDirtyRects(context); - } - } - else - { - _overlay?.Dispose(); - _overlay = null; - } - } - - private void RenderDirtyRects(IDrawingContextImpl context) - { - foreach (var r in _dirtyRectsDisplay) - { - var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); - context.DrawRectangle(brush,null, r.Rect); - } - } - - private void RenderComposite(Scene scene, ref IDrawingContextImpl? context) - { - EnsureDrawingContext(ref context); - - context.Clear(Colors.Transparent); - - var clientRect = new Rect(scene.Size); - - var firstLayer = true; - foreach (var layer in scene.Layers) - { - var bitmap = Layers[layer.LayerRoot].Bitmap; - var sourceRect = new Rect(0, 0, bitmap.Item.PixelSize.Width, bitmap.Item.PixelSize.Height); - - if (layer.GeometryClip != null) - { - context.PushGeometryClip(layer.GeometryClip); - } - - if (layer.OpacityMask == null) - { - if (firstLayer && bitmap.Item.CanBlit) - bitmap.Item.Blit(context); - else - context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); - } - else - { - context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); - } - - if (layer.GeometryClip != null) - { - context.PopGeometryClip(); - } - - firstLayer = false; - } - - if (_overlay != null) - { - var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); - context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); - } - - if (DrawFps) - { - using (var c = new DrawingContext(context, false)) - { - RenderFps(c, clientRect, scene.Layers.Count); - } - } - } - - private void EnsureDrawingContext([NotNull] ref IDrawingContextImpl? context) - { - if (context != null) - { - return; - } - - if (RenderTarget?.IsCorrupted == true) - { - RenderTarget!.Dispose(); - RenderTarget = null; - } - - if (RenderTarget == null) - { - RenderTarget = _renderTargetFactory!(); - } - - context = RenderTarget.CreateDrawingContext(this); - } - - private void UpdateSceneIfNeeded() - { - if(NeedsUpdate) - UpdateScene(); - } - - private bool? UpdateScene() - { - Dispatcher.UIThread.VerifyAccess(); - using var noPump = NonPumpingLockHelper.Use(); - lock (_sceneLock) - { - if (_disposed) - return null; - if (_scene?.Item.Generation > _lastSceneId) - return false; - } - if (_root.IsVisible) - { - var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root) - { - RenderThreadJobs = _pendingRenderThreadJobs - }); - _pendingRenderThreadJobs = null; - var scene = sceneRef.Item; - - if (_dirty == null) - { - _dirty = new DirtyVisuals(); - _recalculateChildren = new HashSet(); - _sceneBuilder.UpdateAll(scene); - } - else - { - foreach (var visual in _recalculateChildren!) - { - var node = scene.FindNode(visual); - ((VisualNode?)node)?.SortChildren(scene); - } - - _recalculateChildren.Clear(); - - foreach (var visual in _dirty) - { - _sceneBuilder.Update(scene, visual); - } - } - - lock (_sceneLock) - { - var oldScene = _scene; - _scene = sceneRef; - oldScene?.Dispose(); - } - - _dirty.Clear(); - - if (SceneInvalidated != null) - { - var rect = new Rect(); - - foreach (var layer in scene.Layers) - { - foreach (var dirty in layer.Dirty) - { - rect = rect.Union(dirty); - } - } - - SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); - } - - return true; - } - else - { - lock (_sceneLock) - { - var oldScene = _scene; - _scene = null; - oldScene?.Dispose(); - } - - return null; - } - } - - private IRef GetOverlay( - IDrawingContextImpl parentContext, - Size size, - double scaling) - { - var pixelSize = size * scaling; - - if (_overlay == null || - _overlay.Item.PixelSize.Width != pixelSize.Width || - _overlay.Item.PixelSize.Height != pixelSize.Height) - { - _overlay?.Dispose(); - _overlay = RefCountable.Create(parentContext.CreateLayer(size)); - } - - return _overlay; - } - - private void SaveDebugFrames(int id) - { - var index = 0; - - foreach (var layer in Layers) - { - var fileName = Path.Combine(DebugFramesPath ?? string.Empty, FormattableString.Invariant($"frame-{id}-layer-{index++}.png")); - layer.Bitmap.Item.Save(fileName); - } - } - } -} diff --git a/src/Avalonia.Base/Rendering/ICustomHitTest.cs b/src/Avalonia.Base/Rendering/ICustomHitTest.cs new file mode 100644 index 0000000000..65bb24b4b3 --- /dev/null +++ b/src/Avalonia.Base/Rendering/ICustomHitTest.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering +{ + + /// + /// Allows customization of hit-testing + /// + public interface ICustomHitTest + { + /// The point to hit test in global coordinate space. + bool HitTest(Point point); + } +} diff --git a/src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs b/src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs deleted file mode 100644 index fae595413f..0000000000 --- a/src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - /// - /// An interface to allow non-templated controls to customize their hit-testing - /// when using a renderer with a simple hit-testing algorithm without a scene graph, - /// such as - /// - public interface ICustomSimpleHitTest - { - /// The point to hit test in global coordinate space. - bool HitTest(Point point); - } - - /// - /// Allows customization of hit-testing for all renderers. - /// - public interface ICustomHitTest : ICustomSimpleHitTest - { - } - - public static class CustomSimpleHitTestExtensions - { - public static bool HitTestCustom(this Visual visual, Point point) - => (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; - - public static bool HitTestCustom(this IEnumerable children, Point point) - => children.Any(ctrl => ctrl.HitTestCustom(point)); - } -} diff --git a/src/Avalonia.Base/Rendering/IRenderLoop.cs b/src/Avalonia.Base/Rendering/IRenderLoop.cs index e500ecdf8b..ebe683949d 100644 --- a/src/Avalonia.Base/Rendering/IRenderLoop.cs +++ b/src/Avalonia.Base/Rendering/IRenderLoop.cs @@ -27,7 +27,10 @@ namespace Avalonia.Rendering /// /// The update task. void Remove(IRenderLoopTask i); - + + /// + /// Indicates if the rendering is done on a non-UI thread. + /// bool RunsInBackground { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index cabb1302cd..09df7b7830 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -1,6 +1,4 @@ using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.VisualTree; namespace Avalonia.Rendering { @@ -25,12 +23,6 @@ namespace Avalonia.Rendering /// double RenderScaling { get; } - /// - /// Adds a rectangle to the window's dirty region. - /// - /// The rectangle. - void Invalidate(Rect rect); - /// /// Converts a point from screen to client coordinates. /// diff --git a/src/Avalonia.Base/Rendering/IRenderTimer.cs b/src/Avalonia.Base/Rendering/IRenderTimer.cs index 07af7eeec8..14fcffd6a9 100644 --- a/src/Avalonia.Base/Rendering/IRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/IRenderTimer.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Avalonia.Metadata; namespace Avalonia.Rendering diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index f3f5b5e99b..7e32504e17 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.VisualTree; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Rendering.Composition; @@ -12,15 +11,9 @@ namespace Avalonia.Rendering public interface IRenderer : IDisposable { /// - /// Gets or sets a value indicating whether the renderer should draw an FPS counter. + /// Gets a value indicating whether the renderer should draw specific diagnostics. /// - bool DrawFps { get; set; } - - /// - /// Gets or sets a value indicating whether the renderer should draw a visual representation - /// of its dirty rectangles. - /// - bool DrawDirtyRects { get; set; } + RendererDiagnostics Diagnostics { get; } /// /// Raised when a portion of the scene has been invalidated. @@ -97,6 +90,9 @@ namespace Avalonia.Rendering public interface IRendererWithCompositor : IRenderer { + /// + /// The associated object + /// Compositor Compositor { get; } } } diff --git a/src/Avalonia.Base/Rendering/IRendererFactory.cs b/src/Avalonia.Base/Rendering/IRendererFactory.cs deleted file mode 100644 index 11d89ef94c..0000000000 --- a/src/Avalonia.Base/Rendering/IRendererFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ - -namespace Avalonia.Rendering -{ - /// - /// Defines the interface for a renderer factory. - /// - public interface IRendererFactory - { - /// - /// Creates a renderer. - /// - /// The root visual. - /// The render loop. - IRenderer Create(IRenderRoot root, IRenderLoop renderLoop); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 60b144e806..09d2d55ce3 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -10,102 +10,12 @@ using Avalonia.VisualTree; namespace Avalonia.Rendering { /// - /// A renderer which renders the state of the visual tree without an intermediate scene graph - /// representation. + /// This class is used to render the visual tree into a DrawingContext by doing + /// a simple tree traversal. + /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - /// - /// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is - /// not taken into account. - /// - public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer + internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer { - private readonly Visual _root; - private readonly Func _renderTargetFactory; - private readonly PlatformRenderInterfaceContextManager? _renderContext; - private readonly IRenderRoot? _renderRoot; - private bool _updateTransformedBounds = true; - private IRenderTarget? _renderTarget; - - /// - /// Initializes a new instance of the class. - /// - /// The control to render. - /// The target render factory. - /// The render contex. - public ImmediateRenderer(Visual root, Func renderTargetFactory, - PlatformRenderInterfaceContextManager? renderContext = null) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderTargetFactory = renderTargetFactory; - _renderContext = renderContext; - _renderRoot = root as IRenderRoot; - } - - private ImmediateRenderer(Visual root, Func renderTargetFactory, bool updateTransformedBounds) - { - _root = root ?? throw new ArgumentNullException(nameof(root)); - _renderTargetFactory = renderTargetFactory; - _renderRoot = root as IRenderRoot; - _updateTransformedBounds = updateTransformedBounds; - } - - /// - public bool DrawFps { get; set; } - - /// - public bool DrawDirtyRects { get; set; } - - /// - public event EventHandler? SceneInvalidated; - - /// - public void Paint(Rect rect) - { - if (_renderTarget == null) - { - _renderTarget = _renderTargetFactory(); - } - - try - { - using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this))) - { - context.PlatformImpl.Clear(Colors.Transparent); - - using (context.PushTransformContainer()) - { - Render(context, _root, _root.Bounds); - } - - if (DrawDirtyRects) - { - var color = (uint)new Random().Next(0xffffff) | 0x44000000; - context.FillRectangle( - new SolidColorBrush(color), - rect); - } - - if (DrawFps) - { - RenderFps(context, _root.Bounds, null); - } - } - } - catch (RenderTargetCorruptedException ex) - { - Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget.Dispose(); - _renderTarget = null; - } - - SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); - } - - /// - public void Resized(Size size) - { - } - /// /// Renders a visual to a render target. /// @@ -113,11 +23,8 @@ namespace Avalonia.Rendering /// The render target. public static void Render(Visual visual, IRenderTarget target) { - using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false)) - using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) - { - renderer.Render(context, visual, visual.Bounds); - } + using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); + Render(context, visual, visual.Bounds); } /// @@ -127,77 +34,9 @@ namespace Avalonia.Rendering /// The drawing context. public static void Render(Visual visual, DrawingContext context) { - using (var renderer = new ImmediateRenderer(visual, - () => throw new InvalidOperationException("This is not supposed to be called"), - updateTransformedBounds: false)) - { - renderer.Render(context, visual, visual.Bounds); - } + Render(context, visual, visual.Bounds); } - - /// - public void AddDirty(Visual visual) - { - if (!visual.Bounds.IsDefault) - { - var m = visual.TransformToVisual(_root); - - if (m.HasValue) - { - var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value); - - //use transformedbounds as previous render state of the visual bounds - //so we can invalidate old and new bounds of a control in case it moved/shrinked - if (visual.TransformedBounds.HasValue) - { - var trb = visual.TransformedBounds.Value; - var trBounds = trb.Bounds.TransformToAABB(trb.Transform); - - if (trBounds != bounds) - { - _renderRoot?.Invalidate(trBounds); - } - } - - _renderRoot?.Invalidate(bounds); - } - } - } - - /// - /// Ends the operation of the renderer. - /// - public void Dispose() - { - _renderTarget?.Dispose(); - } - - /// - public IEnumerable HitTest(Point p, Visual root, Func filter) - { - return HitTest(root, p, filter); - } - - public Visual? HitTestFirst(Point p, Visual root, Func filter) - { - return HitTest(root, p, filter).FirstOrDefault(); - } - - /// - public void RecalculateChildren(Visual visual) => AddDirty(visual); - - /// - public void Start() - { - } - - /// - public void Stop() - { - } - - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => - new(_renderContext?.Value?.TryGetFeature(featureType)); + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) @@ -209,24 +48,15 @@ namespace Avalonia.Rendering /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var visual = brush.Visual; - Render(new DrawingContext(context), visual, visual.Bounds); + if (brush.Visual is { } visual) + { + Render(new DrawingContext(context), visual, visual.Bounds); + } } internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) { - using var renderer = new ImmediateRenderer(visual, - () => throw new InvalidOperationException("This is not supposed to be called"), - updateTransformedBounds); - renderer.Render(context, visual, visual.Bounds); - } - - private static void ClearTransformedBounds(Visual visual) - { - foreach (var e in visual.GetSelfAndVisualDescendants()) - { - visual.SetTransformedBounds(null); - } + Render(context, visual, visual.Bounds); } private static Rect GetTransformedBounds(Visual visual) @@ -244,45 +74,8 @@ namespace Avalonia.Rendering } } - private static IEnumerable HitTest( - Visual visual, - Point p, - Func? filter) - { - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (filter?.Invoke(visual) != false) - { - bool containsPoint; - - if (visual is ICustomSimpleHitTest custom) - { - containsPoint = custom.HitTest(p); - } - else - { - containsPoint = visual.TransformedBounds?.Contains(p) == true; - } - - if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0) - { - foreach (var child in visual.VisualChildren.SortByZIndex()) - { - foreach (var result in HitTest(child, p, filter)) - { - yield return result; - } - } - } - - if (containsPoint) - { - yield return visual; - } - } - } - private void Render(DrawingContext context, Visual visual, Rect clipRect) + private static void Render(DrawingContext context, Visual visual, Rect clipRect) { var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; @@ -324,7 +117,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual @@ -338,15 +131,7 @@ namespace Avalonia.Rendering using (context.PushTransformContainer()) { visual.Render(context); - -#pragma warning disable 0618 - var transformed = - new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); -#pragma warning restore 0618 - - if (_updateTransformedBounds) - visual.SetTransformedBounds(transformed); - + var childrenEnumerable = visual.HasNonUniformZIndexChildren ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) : (IEnumerable)visual.VisualChildren; @@ -362,18 +147,9 @@ namespace Avalonia.Rendering : clipRect; Render(context, child, childClipRect); } - else if (_updateTransformedBounds) - { - ClearTransformedBounds(child); - } } } } - - if (!visual.IsVisible && _updateTransformedBounds) - { - ClearTransformedBounds(visual); - } } } } diff --git a/src/Avalonia.Base/Rendering/LayoutPassTiming.cs b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs new file mode 100644 index 0000000000..b4b6d1d4f1 --- /dev/null +++ b/src/Avalonia.Base/Rendering/LayoutPassTiming.cs @@ -0,0 +1,11 @@ +using System; + +namespace Avalonia.Rendering +{ + /// + /// Represents a single layout pass timing. + /// + /// The number of the layout pass. + /// The elapsed time during the layout pass. + internal readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed); +} diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 261a39fa09..82dcd8f184 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -6,9 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Rendering; -[Unstable] -// TODO: Make it internal once legacy renderers are removed -public class PlatformRenderInterfaceContextManager +internal class PlatformRenderInterfaceContextManager { private readonly IPlatformGraphics? _graphics; private IPlatformRenderInterfaceContext? _backend; @@ -50,6 +48,8 @@ public class PlatformRenderInterfaceContextManager } } + internal IPlatformGraphicsContext? GpuContext => _gpuContext?.Value; + public IDisposable EnsureCurrent() { EnsureValidBackendContext(); diff --git a/src/Avalonia.Base/Rendering/RenderLayer.cs b/src/Avalonia.Base/Rendering/RenderLayer.cs deleted file mode 100644 index d1e3fcafb1..0000000000 --- a/src/Avalonia.Base/Rendering/RenderLayer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - public class RenderLayer - { - public RenderLayer( - IDrawingContextImpl drawingContext, - Size size, - double scaling, - Visual layerRoot) - { - Bitmap = RefCountable.Create(drawingContext.CreateLayer(size)); - Size = size; - Scaling = scaling; - LayerRoot = layerRoot; - IsEmpty = true; - } - - public IRef Bitmap { get; private set; } - public bool IsEmpty { get; set; } - public double Scaling { get; private set; } - public Size Size { get; private set; } - public Visual LayerRoot { get; } - - public void RecreateBitmap(IDrawingContextImpl drawingContext, Size size, double scaling) - { - if (Size != size || Scaling != scaling) - { - var resized = RefCountable.Create(drawingContext.CreateLayer(size)); - - using (var context = resized.Item.CreateDrawingContext(null)) - { - Bitmap.Dispose(); - context.Clear(default); - - Bitmap = resized; - Scaling = scaling; - Size = size; - IsEmpty = true; - } - } - } - } -} diff --git a/src/Avalonia.Base/Rendering/RenderLayers.cs b/src/Avalonia.Base/Rendering/RenderLayers.cs deleted file mode 100644 index eff81e6bbf..0000000000 --- a/src/Avalonia.Base/Rendering/RenderLayers.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; - -namespace Avalonia.Rendering -{ - public class RenderLayers : IEnumerable - { - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); - - public int Count => _inner.Count; - public RenderLayer this[Visual layerRoot] => _index[layerRoot]; - - public void Update(Scene scene, IDrawingContextImpl context) - { - for (var i = scene.Layers.Count - 1; i >= 0; --i) - { - var src = scene.Layers[i]; - - if (!_index.TryGetValue(src.LayerRoot, out var layer)) - { - layer = new RenderLayer(context, scene.Size, scene.Scaling, src.LayerRoot); - _inner.Add(layer); - _index.Add(src.LayerRoot, layer); - } - else - { - layer.RecreateBitmap(context, scene.Size, scene.Scaling); - } - } - - for (var i = 0; i < _inner.Count;) - { - var layer = _inner[i]; - - if (!scene.Layers.Exists(layer.LayerRoot)) - { - layer.Bitmap.Dispose(); - _inner.RemoveAt(i); - _index.Remove(layer.LayerRoot); - } - else - i++; - } - } - - public void Clear() - { - foreach (var layer in _index.Values) - { - layer.Bitmap.Dispose(); - } - - _index.Clear(); - _inner.Clear(); - } - - public bool TryGetValue(Visual layerRoot, [NotNullWhen(true)] out RenderLayer? value) - { - return _index.TryGetValue(layerRoot, out value); - } - - public IEnumerator GetEnumerator() => _inner.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index 1f58ca3827..185f44d29a 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -87,6 +87,7 @@ namespace Avalonia.Rendering } } + /// public bool RunsInBackground => Timer.RunsInBackground; private void TimerTick(TimeSpan time) diff --git a/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs b/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs new file mode 100644 index 0000000000..85932f1568 --- /dev/null +++ b/src/Avalonia.Base/Rendering/RendererDebugOverlays.cs @@ -0,0 +1,35 @@ +using System; + +namespace Avalonia.Rendering; + +/// +/// Represents the various types of overlays that can be drawn by a renderer. +/// +[Flags] +public enum RendererDebugOverlays +{ + /// + /// Do not draw any overlay. + /// + None = 0, + + /// + /// Draw a FPS counter. + /// + Fps = 1 << 0, + + /// + /// Draw invalidated rectangles each frame. + /// + DirtyRects = 1 << 1, + + /// + /// Draw a graph of past layout times. + /// + LayoutTimeGraph = 1 << 2, + + /// + /// Draw a graph of past render times. + /// + RenderTimeGraph = 1 << 3 +} diff --git a/src/Avalonia.Base/Rendering/RendererDiagnostics.cs b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs new file mode 100644 index 0000000000..0897cac62e --- /dev/null +++ b/src/Avalonia.Base/Rendering/RendererDiagnostics.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; + +namespace Avalonia.Rendering +{ + /// + /// Manages configurable diagnostics that can be displayed by a renderer. + /// + public class RendererDiagnostics : INotifyPropertyChanged + { + private RendererDebugOverlays _debugOverlays; + private LayoutPassTiming _lastLayoutPassTiming; + private PropertyChangedEventArgs? _debugOverlaysChangedEventArgs; + private PropertyChangedEventArgs? _lastLayoutPassTimingChangedEventArgs; + + /// + /// Gets or sets which debug overlays are displayed by the renderer. + /// + public RendererDebugOverlays DebugOverlays + { + get => _debugOverlays; + set + { + if (_debugOverlays != value) + { + _debugOverlays = value; + OnPropertyChanged(_debugOverlaysChangedEventArgs ??= new(nameof(DebugOverlays))); + } + } + } + + /// + /// Gets or sets the last layout pass timing that the renderer may display. + /// + internal LayoutPassTiming LastLayoutPassTiming + { + get => _lastLayoutPassTiming; + set + { + if (!_lastLayoutPassTiming.Equals(value)) + { + _lastLayoutPassTiming = value; + OnPropertyChanged(_lastLayoutPassTimingChangedEventArgs ??= new(nameof(LastLayoutPassTiming))); + } + } + } + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Called when a property changes on the object. + /// + /// The property change details. + protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) + => PropertyChanged?.Invoke(this, args); + } +} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs deleted file mode 100644 index d6766fa9b8..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ /dev/null @@ -1,482 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.Media.Imaging; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A drawing context which builds a scene graph. - /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport - { - private readonly ISceneBuilder _sceneBuilder; - private VisualNode? _node; - private int _childIndex; - private int _drawOperationindex; - - /// - /// Initializes a new instance of the class. - /// - /// - /// A scene builder used for constructing child scenes for visual brushes. - /// - /// The scene layers. - public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) - { - _sceneBuilder = sceneBuilder; - Layers = layers; - } - - /// - public Matrix Transform { get; set; } = Matrix.Identity; - - /// - /// Gets the layers in the scene being built. - /// - public SceneLayers Layers { get; } - - /// - /// Informs the drawing context of the visual node that is about to be rendered. - /// - /// The visual node. - /// - /// An object which when disposed will commit the changes to visual node. - /// - public UpdateState BeginUpdate(VisualNode node) - { - _ = node ?? throw new ArgumentNullException(nameof(node)); - - if (_node != null) - { - if (_childIndex < _node.Children.Count) - { - _node.ReplaceChild(_childIndex, node); - } - else - { - _node.AddChild(node); - } - - ++_childIndex; - } - - var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); - _node = node; - _childIndex = _drawOperationindex = 0; - return state; - } - - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } - - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } - - /// - /// Removes any remaining drawing operations from the visual node. - /// - /// - /// Drawing operations are updated in place, overwriting existing drawing operations if - /// they are different. Once drawing has completed for the current visual node, it is - /// possible that there are stale drawing operations at the end of the list. This method - /// trims these stale drawing operations. - /// - public void TrimChildren() - { - _node!.TrimChildren(_childIndex); - } - - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) - { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) - { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen pen, Point p1, Point p2) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) - { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows = default) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) - { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - /// - 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 DrawEllipse(IBrush? brush, IPen? pen, Rect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) - { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); - } - else - { - ++_drawOperationindex; - } - } - - public void Custom(ICustomDrawOperation custom) - { - var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, custom)) - Add(new CustomDrawOperation(custom, Transform)); - else - ++_drawOperationindex; - } - - public object? GetFeature(Type t) => null; - - /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) - { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); - } - - else - { - ++_drawOperationindex; - } - } - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); - } - - /// - public void PopClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new ClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopGeometryClip() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new GeometryClipNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopBitmapBlendMode() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new BitmapBlendModeNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacity() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new OpacityNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PopOpacityMask() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null, null)) - { - Add(new OpacityMaskNode()); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(Rect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushClip(RoundedRect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushGeometryClip(IGeometryImpl? clip) - { - if (clip is null) - return; - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new GeometryClipNode(Transform, clip)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacity(double opacity) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(opacity)) - { - Add(new OpacityNode(opacity)); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushOpacityMask(IBrush mask, Rect bounds) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(mask, bounds)) - { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); - } - else - { - ++_drawOperationindex; - } - } - - /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(blendingMode)) - { - Add(new BitmapBlendModeNode(blendingMode)); - } - else - { - ++_drawOperationindex; - } - } - - public readonly struct UpdateState : IDisposable - { - public UpdateState( - DeferredDrawingContextImpl owner, - VisualNode? node, - int childIndex, - int drawOperationIndex) - { - Owner = owner; - Node = node; - ChildIndex = childIndex; - DrawOperationIndex = drawOperationIndex; - } - - public void Dispose() - { - Owner._node!.TrimDrawOperations(Owner._drawOperationindex); - - var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; - - var drawOperations = Owner._node.DrawOperations; - var drawOperationsCount = drawOperations.Count; - - for (var i = 0; i < drawOperationsCount; i++) - { - dirty.Add(drawOperations[i].Item.Bounds); - } - - Owner._node = Node; - Owner._childIndex = ChildIndex; - Owner._drawOperationindex = DrawOperationIndex; - } - - public DeferredDrawingContextImpl Owner { get; } - public VisualNode? Node { get; } - public int ChildIndex { get; } - public int DrawOperationIndex { get; } - } - - private void Add(T node) where T : class, IDrawOperation - { - using (var refCounted = RefCountable.Create(node)) - { - Add(refCounted); - } - } - - private void Add(IRef node) - { - if (_drawOperationindex < _node!.DrawOperations.Count) - { - _node.ReplaceDrawOperation(_drawOperationindex, node); - } - else - { - _node.AddDrawOperation(node); - } - - ++_drawOperationindex; - } - - private IRef? NextDrawAs() where T : class, IDrawOperation - { - return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef : null; - } - - private IDisposable? CreateChildScene(IBrush? brush) - { - var visualBrush = brush as VisualBrush; - - if (visualBrush != null) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - var scene = new Scene(visual); - _sceneBuilder.UpdateAll(scene); - return scene; - } - } - - return null; - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 12b67105e9..82f8fc2d56 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -80,11 +80,8 @@ namespace Avalonia.Rendering.SceneGraph { p *= Transform.Invert(); - if (Material != null) - { - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } + var rect = Rect.Rect; + return rect.ContainsExclusive(p); } return false; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1d85e95835..a2d914bdd7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Rendering.SceneGraph { @@ -19,13 +20,13 @@ namespace Avalonia.Rendering.SceneGraph public GlyphRunNode( Matrix transform, IBrush foreground, - GlyphRun glyphRun, + IRef glyphRun, IDisposable? aux = null) - : base(new Rect(glyphRun.Size), transform, aux) + : base(new Rect(glyphRun.Item.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); - GlyphRun = glyphRun; + GlyphRun = glyphRun.Clone(); } /// @@ -41,7 +42,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the glyph run to draw. /// - public GlyphRun GlyphRun { get; } + public IRef GlyphRun { get; } /// public override void Render(IDrawingContextImpl context) @@ -61,14 +62,19 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && Equals(foreground, Foreground) && - Equals(glyphRun, GlyphRun); + Equals(glyphRun.Item, GlyphRun.Item); } /// public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + + public override void Dispose() + { + GlyphRun?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 6d30358119..2bfd2080c3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to childs, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs deleted file mode 100644 index f469fdbfe8..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Builds a scene graph from a visual tree. - /// - public interface ISceneBuilder - { - /// - /// Builds the initial scene graph for a visual tree. - /// - /// The scene to build. - void UpdateAll(Scene scene); - - /// - /// Updates the visual (and potentially its children) in a scene. - /// - /// The scene. - /// The visual to update. - /// True if changes were made, otherwise false. - bool Update(Scene scene, Visual visual); - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs deleted file mode 100644 index 59a032748d..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Represents a node in the low-level scene graph representing a . - /// - public interface IVisualNode : IDisposable - { - /// - /// Gets the visual to which the node relates. - /// - Visual Visual { get; } - - /// - /// Gets the parent scene graph node. - /// - IVisualNode? Parent { get; } - - /// - /// Gets the transform for the node from global to control coordinates. - /// - Matrix Transform { get; } - - /// - /// Gets the corner radius of visual. Contents are clipped to this radius. - /// - CornerRadius ClipToBoundsRadius { get; } - - /// - /// Gets the bounds of the node's geometry in global coordinates. - /// - Rect Bounds { get; } - - /// - /// Gets the clip bounds for the node in global coordinates. - /// - Rect ClipBounds { get; } - - /// - /// Gets the layout bounds for the node in global coordinates. - /// - Rect LayoutBounds { get; } - - /// - /// Whether the node is clipped to . - /// - bool ClipToBounds { get; } - - /// - /// Gets the node's clip geometry, if any. - /// - IGeometryImpl? GeometryClip { get; set; } - - /// - /// Gets a value indicating whether one of the node's ancestors has a geometry clip. - /// - bool HasAncestorGeometryClip { get; } - - /// - /// Gets the child scene graph nodes. - /// - IReadOnlyList Children { get; } - - /// - /// Gets the drawing operations for the visual. - /// - IReadOnlyList> DrawOperations { get; } - - /// - /// Gets the opacity of the scene graph node. - /// - double Opacity { get; } - - /// - /// Sets up the drawing context for rendering the node's geometry. - /// - /// The drawing context. - /// Whether to skip pushing the control's opacity. - void BeginRender(IDrawingContextImpl context, bool skipOpacity); - - /// - /// Resets the drawing context after rendering the node's geometry. - /// - /// The drawing context. - /// Whether to skip popping the control's opacity. - void EndRender(IDrawingContextImpl context, bool skipOpacity); - - /// - /// Hit test the geometry in this node. - /// - /// The point in global coordinates. - /// True if the point hits the node's geometry; otherwise false. - /// - /// This method does not recurse to child s, if you want - /// to hit test children they must be hit tested manually. - /// - bool HitTest(Point p); - - bool Disposed { get; } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds 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(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs deleted file mode 100644 index 735eb3bb3f..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Avalonia.Collections.Pooled; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Represents a scene graph used by the . - /// - public class Scene : IDisposable - { - private readonly Dictionary _index; - private readonly TaskCompletionSource _rendered = new TaskCompletionSource(); - - /// - /// Initializes a new instance of the class. - /// - /// The root visual to draw. - public Scene(Visual rootVisual) - : this( - new VisualNode(rootVisual, null), - new Dictionary(), - new SceneLayers(rootVisual), - 0) - { - _index.Add(rootVisual, Root); - } - - private Scene(VisualNode root, Dictionary index, SceneLayers layers, int generation) - { - _ = root ?? throw new ArgumentNullException(nameof(root)); - - var renderRoot = root.Visual as IRenderRoot; - - _index = index; - Root = root; - Layers = layers; - Generation = generation; - root.LayerRoot = root.Visual; - } - - public Task Rendered => _rendered.Task; - - /// - /// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned. - /// - public int Generation { get; } - - /// - /// Gets the layers for the scene. - /// - public SceneLayers Layers { get; } - - /// - /// Gets the root node of the scene graph. - /// - public IVisualNode Root { get; } - - /// - /// Gets or sets the size of the scene in device independent pixels. - /// - public Size Size { get; set; } - - /// - /// Gets or sets the scene scaling. - /// - public double Scaling { get; set; } = 1; - - /// - /// Adds a node to the scene index. - /// - /// The node. - public void Add(IVisualNode node) - { - _ = node ?? throw new ArgumentNullException(nameof(node)); - - _index.Add(node.Visual, node); - } - - /// - /// Clones the scene. - /// - /// The cloned scene. - public Scene CloneScene() - { - var index = new Dictionary(_index.Count); - var root = Clone((VisualNode)Root, null, index); - - var result = new Scene(root, index, Layers.Clone(), Generation + 1) - { - Size = Size, - Scaling = Scaling, - }; - - return result; - } - - public void Dispose() - { - _rendered.TrySetResult(false); - foreach (var node in _index.Values) - { - node.Dispose(); - } - } - - /// - /// Tries to find a node in the scene graph representing the specified visual. - /// - /// The visual. - /// - /// The node representing the visual or null if it could not be found. - /// - public IVisualNode? FindNode(Visual visual) - { - _index.TryGetValue(visual, out var node); - return node; - } - - /// - /// Gets the visuals at a point in the scene. - /// - /// The point. - /// The root of the subtree to search. - /// A filter. May be null. - /// The visuals at the specified point. - public IEnumerable HitTest(Point p, Visual root, Func? filter) - { - var node = FindNode(root); - return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty(); - } - - /// - /// Gets the visual at a point in the scene. - /// - /// The point. - /// The root of the subtree to search. - /// A filter. May be null. - /// The visual at the specified point. - public Visual? HitTestFirst(Point p, Visual root, Func? filter) - { - var node = FindNode(root); - return (node != null) ? HitTestFirst(node, p, filter) : null; - } - - /// - /// Removes a node from the scene index. - /// - /// The node. - public void Remove(IVisualNode node) - { - _ = node ?? throw new ArgumentNullException(nameof(node)); - - _index.Remove(node.Visual); - - node.Dispose(); - } - - private VisualNode Clone(VisualNode source, IVisualNode? parent, Dictionary index) - { - var result = source.Clone(parent); - - index.Add(result.Visual, result); - - var children = source.Children; - var childrenCount = children.Count; - - if (childrenCount > 0) - { - result.TryPreallocateChildren(childrenCount); - - for (var i = 0; i < childrenCount; i++) - { - var child = children[i]; - - result.AddChild(Clone((VisualNode)child, result, index)); - } - } - - return result; - } - - private Visual HitTestFirst(IVisualNode root, Point p, Func? filter) - { - using var enumerator = new HitTestEnumerator(root, filter, p, Root); - - enumerator.MoveNext(); - - return enumerator.Current; - } - - private class HitTestEnumerable : IEnumerable - { - private readonly IVisualNode _root; - private readonly Func? _filter; - private readonly IVisualNode _sceneRoot; - private readonly Point _point; - - public HitTestEnumerable(IVisualNode root, Func? filter, Point point, IVisualNode sceneRoot) - { - _root = root; - _filter = filter; - _point = point; - _sceneRoot = sceneRoot; - } - - public IEnumerator GetEnumerator() - { - return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - private struct HitTestEnumerator : IEnumerator - { - private readonly PooledStack _nodeStack; - private readonly Func? _filter; - private readonly IVisualNode _sceneRoot; - private Visual? _current; - private readonly Point _point; - - public HitTestEnumerator(IVisualNode root, Func? filter, Point point, IVisualNode sceneRoot) - { - _nodeStack = new PooledStack(); - _nodeStack.Push(new Entry(root, false, null, true)); - - _filter = filter; - _point = point; - _sceneRoot = sceneRoot; - - _current = null; - } - - public bool MoveNext() - { - while (_nodeStack.Count > 0) - { - (var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); - - if (wasVisited && isRoot) - { - break; - } - - var children = node.Children; - int childCount = children.Count; - - if (childCount == 0 || wasVisited) - { - if ((wasVisited || FilterAndClip(node, ref clip)) && - (node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) - { - _current = node.Visual; - - return true; - } - } - else if (FilterAndClip(node, ref clip)) - { - _nodeStack.Push(new Entry(node, true, null)); - - for (var i = 0; i < childCount; i++) - { - _nodeStack.Push(new Entry(children[i], false, clip)); - } - } - } - - return false; - } - - public void Reset() - { - throw new NotSupportedException(); - } - - public Visual Current => _current!; - - object IEnumerator.Current => Current; - - public void Dispose() - { - _nodeStack.Dispose(); - } - - private bool FilterAndClip(IVisualNode node, ref Rect? clip) - { - if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) - { - var clipped = false; - - if (node.ClipToBounds) - { - clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); - clipped = !clip.Value.ContainsExclusive(_point); - } - - if (node.GeometryClip != null) - { - var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); - clipped = !node.GeometryClip.FillContains(controlPoint!.Value); - } - - if (!clipped && node.Visual is ICustomHitTest custom) - { - clipped = !custom.HitTest(_point); - } - - return !clipped; - } - - return false; - } - - private readonly struct Entry - { - public readonly bool WasVisited; - public readonly bool IsRoot; - public readonly IVisualNode Node; - public readonly Rect? Clip; - - public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) - { - Node = node; - WasVisited = wasVisited; - IsRoot = isRoot; - Clip = clip; - } - - public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) - { - wasVisited = WasVisited; - isRoot = IsRoot; - node = Node; - clip = Clip; - } - } - } - - public void MarkAsRendered() => _rendered.TrySetResult(true); - - public List? RenderThreadJobs { get; set; } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs deleted file mode 100644 index 55ff772772..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ /dev/null @@ -1,485 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Threading; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Builds a scene graph from a visual tree. - /// - public class SceneBuilder : ISceneBuilder - { - /// - public void UpdateAll(Scene scene) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - Dispatcher.UIThread.VerifyAccess(); - - UpdateSize(scene); - scene.Layers.GetOrAdd(scene.Root.Visual); - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - Update(context, scene, (VisualNode)scene.Root, clip, true); - } - } - - /// - public bool Update(Scene scene, Visual visual) - { - _ = scene ?? throw new ArgumentNullException(nameof(scene)); - _ = visual ?? throw new ArgumentNullException(nameof(visual)); - - Dispatcher.UIThread.VerifyAccess(); - - if (!scene.Root.Visual.IsVisible) - { - throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); - } - - var node = (VisualNode?)scene.FindNode(visual); - - if (visual == scene.Root.Visual) - { - UpdateSize(scene); - } - - if (visual.VisualRoot == scene.Root.Visual) - { - if (node?.Parent != null && - visual.VisualParent != null && - node.Parent.Visual != visual.VisualParent) - { - // The control has changed parents. Remove the node and recurse into the new parent node. - ((VisualNode)node.Parent).RemoveChild(node); - Deindex(scene, node); - node = (VisualNode?)scene.FindNode(visual.VisualParent); - } - - if (visual.IsVisible) - { - // If the node isn't yet part of the scene, find the nearest ancestor that is. - node = node ?? FindExistingAncestor(scene, visual); - - // We don't need to do anything if this part of the tree has already been fully - // updated. - if (node != null && !node.SubTreeUpdated) - { - // If the control we've been asked to update isn't part of the scene then - // we're carrying out an add operation, so recurse and add all the - // descendents too. - var recurse = node.Visual != visual; - - using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) - using (var context = new DrawingContext(impl)) - { - var clip = new Rect(scene.Root.Visual.Bounds.Size); - - if (node.Parent != null) - { - context.PushPostTransform(node.Parent.Transform); - clip = node.Parent.ClipBounds; - } - - using (context.PushTransformContainer()) - { - Update(context, scene, node, clip, recurse); - } - } - - return true; - } - } - else - { - if (node != null) - { - // The control has been hidden so remove it from its parent and deindex the - // node and its descendents. - ((VisualNode?)node.Parent)?.RemoveChild(node); - Deindex(scene, node); - return true; - } - } - } - else if (node != null) - { - // The control has been removed so remove it from its parent and deindex the - // node and its descendents. - var trim = FindFirstDeadAncestor(scene, node); - ((VisualNode)trim.Parent!).RemoveChild(trim); - Deindex(scene, trim); - return true; - } - - return false; - } - - private static VisualNode? FindExistingAncestor(Scene scene, Visual visual) - { - var node = scene.FindNode(visual); - - while (node == null && visual.IsVisible) - { - var parent = visual.VisualParent; - - if (parent is null) - return null; - - visual = parent; - node = scene.FindNode(visual); - } - - return visual.IsVisible ? (VisualNode?)node : null; - } - - private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) - { - var parent = node.Parent; - - while (parent!.Visual.VisualRoot == null) - { - node = parent; - parent = node.Parent; - } - - return (VisualNode)node; - } - - private static object GetOrCreateChildNode(Scene scene, Visual child, VisualNode parent) - { - var result = (VisualNode?)scene.FindNode(child); - - if (result != null && result.Parent != parent) - { - Deindex(scene, result); - ((VisualNode?)result.Parent)?.RemoveChild(result); - result = null; - } - - return result ?? CreateNode(scene, child, parent); - } - - private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) - { - var visual = node.Visual; - var opacity = visual.Opacity; - var clipToBounds = visual.ClipToBounds; -#pragma warning disable CS0618 // Type or member is obsolete - var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? - roundRectClip.ClipToBoundsRadius : - default; -#pragma warning restore CS0618 // Type or member is obsolete - - var bounds = new Rect(visual.Bounds.Size); - var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; - - contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); - - if (visual.IsVisible) - { - var m = node != scene.Root ? - Matrix.CreateTranslation(visual.Bounds.Position) : - Matrix.Identity; - - var renderTransform = Matrix.Identity; - - // this should be calculated BEFORE renderTransform - if (visual.HasMirrorTransform) - { - var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); - renderTransform *= mirrorMatrix; - } - - if (visual.RenderTransform != null) - { - var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); - var offset = Matrix.CreateTranslation(origin); - var finalTransform = (-offset) * visual.RenderTransform.Value * (offset); - renderTransform *= finalTransform; - } - - m = renderTransform * m; - - using (contextImpl.BeginUpdate(node)) - using (context.PushPostTransform(m)) - using (context.PushTransformContainer()) - { - var globalBounds = bounds.TransformToAABB(contextImpl.Transform); - var clipBounds = clipToBounds ? - globalBounds.Intersect(clip) : - clip; - - forceRecurse = forceRecurse || - node.ClipBounds != clipBounds || - node.Opacity != opacity || - node.Transform != contextImpl.Transform; - - node.Transform = contextImpl.Transform; - node.ClipBounds = clipBounds; - node.ClipToBounds = clipToBounds; - node.LayoutBounds = globalBounds; - node.ClipToBoundsRadius = clipToBoundsRadius; - node.GeometryClip = visual.Clip?.PlatformImpl; - node.Opacity = opacity; - - // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. - node.OpacityMask = visual.OpacityMask?.ToImmutable(); - - if (ShouldStartLayer(visual)) - { - if (node.LayerRoot != visual) - { - MakeLayer(scene, node); - } - else - { - UpdateLayer(node, scene.Layers[node.LayerRoot]); - } - } - else if (node.LayerRoot == node.Visual && node.Parent != null) - { - ClearLayer(scene, node); - } - - if (node.ClipToBounds) - { - clip = clip.Intersect(node.ClipBounds); - } - - try - { - visual.Render(context); - } - catch { } - - var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); - visual.SetTransformedBounds(transformed); - - if (forceRecurse) - { - var visualChildren = (IList) visual.VisualChildren; - - node.TryPreallocateChildren(visualChildren.Count); - - if (visualChildren.Count == 1) - { - var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - else if (visualChildren.Count > 1) - { - var count = visualChildren.Count; - - if (visual.HasNonUniformZIndexChildren) - { - var sortedChildren = new (Visual visual, int index)[count]; - - for (var i = 0; i < count; i++) - { - sortedChildren[i] = (visualChildren[i], i); - } - - // Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements. - Array.Sort(sortedChildren, (lhs, rhs) => - { - var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); - - return result == 0 ? lhs.index.CompareTo(rhs.index) : result; - }); - - foreach (var child in sortedChildren) - { - var childNode = GetOrCreateChildNode(scene, child.Item1, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - } - else - foreach (var child in visualChildren) - { - var childNode = GetOrCreateChildNode(scene, child, node); - Update(context, scene, (VisualNode)childNode, clip, forceRecurse); - } - } - - node.SubTreeUpdated = true; - contextImpl.TrimChildren(); - } - } - } - else - { - contextImpl.BeginUpdate(node).Dispose(); - } - } - - private static void UpdateSize(Scene scene) - { - var renderRoot = scene.Root.Visual as IRenderRoot; - var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; - - scene.Scaling = renderRoot?.RenderScaling ?? 1; - - if (scene.Size != newSize) - { - var oldSize = scene.Size; - - scene.Size = newSize; - - Rect horizontalDirtyRect = default; - Rect verticalDirtyRect = default; - - if (newSize.Width > oldSize.Width) - { - horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); - } - - if (newSize.Height > oldSize.Height) - { - verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); - } - - foreach (var layer in scene.Layers) - { - layer.Dirty.Add(horizontalDirtyRect); - layer.Dirty.Add(verticalDirtyRect); - } - } - } - - private static VisualNode CreateNode(Scene scene, Visual visual, VisualNode parent) - { - var node = new VisualNode(visual, parent); - node.LayerRoot = parent.LayerRoot; - scene.Add(node); - return node; - } - - private static void Deindex(Scene scene, VisualNode node) - { - var nodeChildren = node.Children; - var nodeChildrenCount = nodeChildren.Count; - - for (var i = 0; i < nodeChildrenCount; i++) - { - if (nodeChildren[i] is VisualNode visual) - { - Deindex(scene, visual); - } - } - - scene.Remove(node); - - node.SubTreeUpdated = true; - - scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); - - node.Visual.SetTransformedBounds(null); - - if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) - { - scene.Layers.Remove(node.LayerRoot); - } - } - - private static void ClearLayer(Scene scene, VisualNode node) - { - var parent = (VisualNode)node.Parent!; - var oldLayerRoot = node.LayerRoot; - var newLayerRoot = parent.LayerRoot!; - var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; - var newDirtyRects = scene.Layers[newLayerRoot].Dirty; - - existingDirtyRects.Coalesce(); - - foreach (var r in existingDirtyRects) - { - newDirtyRects.Add(r); - } - - var oldLayer = scene.Layers[oldLayerRoot!]; - PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); - scene.Layers.Remove(oldLayer); - } - - private static void MakeLayer(Scene scene, VisualNode node) - { - var oldLayerRoot = node.LayerRoot!; - var layer = scene.Layers.Add(node.Visual); - var oldLayer = scene.Layers[oldLayerRoot!]; - - UpdateLayer(node, layer); - PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); - } - - private static void UpdateLayer(VisualNode node, SceneLayer layer) - { - layer.Opacity = node.Visual.Opacity; - - if (node.Visual.OpacityMask != null) - { - layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); - layer.OpacityMaskRect = node.ClipBounds; - } - else - { - layer.OpacityMask = null; - layer.OpacityMaskRect = default; - } - - layer.GeometryClip = node.HasAncestorGeometryClip ? - CreateLayerGeometryClip(node) : - null; - } - - private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) - { - node.LayerRoot = layer.LayerRoot; - - layer.Dirty.Add(node.Bounds); - oldLayer.Dirty.Add(node.Bounds); - - foreach (VisualNode child in node.Children) - { - // If the child is not the start of a new layer, recurse. - if (child.LayerRoot != child.Visual) - { - PropagateLayer(child, layer, oldLayer); - } - } - } - - // HACK: Disabled layers because they're broken in current renderer. See #2244. - private static bool ShouldStartLayer(Visual visual) => false; - - private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) - { - IGeometryImpl? result = null; - VisualNode? n = node; - - for (;;) - { - n = (VisualNode?)n!.Parent; - - if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) - { - break; - } - - if (n?.GeometryClip != null) - { - var transformed = n.GeometryClip.WithTransform(n.Transform); - - result = result == null ? transformed : result.Intersect(transformed); - } - } - - return result; - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs deleted file mode 100644 index a5e3b88188..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Represents a layer in a . - /// - public class SceneLayer - { - /// - /// Initializes a new instance of the class. - /// - /// The visual at the root of the layer. - /// The distance from the scene root. - public SceneLayer(Visual layerRoot, int distanceFromRoot) - { - LayerRoot = layerRoot; - Dirty = new DirtyRects(); - DistanceFromRoot = distanceFromRoot; - } - - /// - /// Clones the layer. - /// - /// The cloned layer. - public SceneLayer Clone() - { - return new SceneLayer(LayerRoot, DistanceFromRoot) - { - Opacity = Opacity, - OpacityMask = OpacityMask, - OpacityMaskRect = OpacityMaskRect, - GeometryClip = GeometryClip, - }; - } - - /// - /// Gets the visual at the root of the layer. - /// - public Visual LayerRoot { get; } - - /// - /// Gets the distance of the layer root from the root of the scene. - /// - public int DistanceFromRoot { get; } - - /// - /// Gets or sets the opacity of the layer. - /// - public double Opacity { get; set; } = 1; - - /// - /// Gets or sets the opacity mask for the layer. - /// - public IBrush? OpacityMask { get; set; } - - /// - /// Gets or sets the target rectangle for the layer opacity mask. - /// - public Rect OpacityMaskRect { get; set; } - - /// - /// Gets the layer's geometry clip. - /// - public IGeometryImpl? GeometryClip { get; set; } - - /// - /// Gets the dirty rectangles for the layer. - /// - internal DirtyRects Dirty { get; } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs deleted file mode 100644 index 8a997c5ace..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Holds a collection of layers for a . - /// - public class SceneLayers : IEnumerable - { - private readonly Visual _root; - private readonly List _inner; - private readonly Dictionary _index; - - /// - /// Initializes a new instance of the class. - /// - /// The scene's root visual. - public SceneLayers(Visual root) : this(root, 0) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The scene's root visual. - /// Initial layer capacity. - public SceneLayers(Visual root, int capacity) - { - _root = root; - - _inner = new List(capacity); - _index = new Dictionary(capacity); - } - - /// - /// Gets the number of layers in the scene. - /// - public int Count => _inner.Count; - - /// - /// Gets a value indicating whether any of the layers have a dirty region. - /// - public bool HasDirty - { - get - { - foreach (var layer in _inner) - { - if (!layer.Dirty.IsEmpty) - { - return true; - } - } - - return false; - } - } - - /// - /// Gets a layer by index. - /// - /// The index of the layer. - /// The layer. - public SceneLayer this[int index] => _inner[index]; - - /// - /// Gets a layer by its root visual. - /// - /// The layer's root visual. - /// The layer. - public SceneLayer this[Visual visual] => _index[visual]; - - /// - /// Adds a layer to the scene. - /// - /// The root visual of the layer. - /// The created layer. - public SceneLayer Add(Visual layerRoot) - { - _ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); - - var distance = layerRoot.CalculateDistanceFromAncestor(_root); - var layer = new SceneLayer(layerRoot, distance); - var insert = FindInsertIndex(layer); - _index.Add(layerRoot, layer); - _inner.Insert(insert, layer); - return layer; - } - - /// - /// Makes a deep clone of the layers. - /// - /// The cloned layers. - public SceneLayers Clone() - { - var result = new SceneLayers(_root, Count); - - foreach (var src in _inner) - { - var dest = src.Clone(); - result._index.Add(dest.LayerRoot, dest); - result._inner.Add(dest); - } - - return result; - } - - /// - /// Tests whether a layer exists with the specified root visual. - /// - /// The root visual. - /// - /// True if a layer exists with the specified root visual, otherwise false. - /// - public bool Exists(Visual layerRoot) - { - _ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); - - return _index.ContainsKey(layerRoot); - } - - /// - /// Tries to find a layer with the specified root visual. - /// - /// The root visual. - /// The layer if found, otherwise null. - public SceneLayer? Find(Visual layerRoot) - { - _index.TryGetValue(layerRoot, out var result); - return result; - } - - /// - /// Gets an existing layer or creates a new one if no existing layer is found. - /// - /// The root visual. - /// The layer. - public SceneLayer GetOrAdd(Visual layerRoot) - { - _ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); - - if (!_index.TryGetValue(layerRoot, out var result)) - { - result = Add(layerRoot); - } - - return result; - } - - /// - /// Removes a layer from the scene. - /// - /// The root visual. - /// True if a matching layer was removed, otherwise false. - public bool Remove(Visual layerRoot) - { - _ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); - - if (_index.TryGetValue(layerRoot, out var layer)) - { - Remove(layer); - } - - return layer != null; - } - - /// - /// Removes a layer from the scene. - /// - /// The layer. - /// True if the layer was part of the scene, otherwise false. - public bool Remove(SceneLayer layer) - { - _ = layer ?? throw new ArgumentNullException(nameof(layer)); - - _index.Remove(layer.LayerRoot); - return _inner.Remove(layer); - } - - /// - public IEnumerator GetEnumerator() => _inner.GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private int FindInsertIndex(SceneLayer insert) - { - var index = 0; - - foreach (var layer in _inner) - { - if (layer.DistanceFromRoot > insert.DistanceFromRoot) - { - break; - } - - ++index; - } - - return index; - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs deleted file mode 100644 index b9491e6cbd..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs +++ /dev/null @@ -1,448 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Reactive; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the low-level scene graph representing an . - /// - internal class VisualNode : IVisualNode - { - private static readonly IReadOnlyList EmptyChildren = Array.Empty(); - private static readonly IReadOnlyList> EmptyDrawOperations = Array.Empty>(); - - private Rect? _bounds; - private double _opacity; - private List? _children; - private List>? _drawOperations; - private IRef? _drawOperationsRefCounter; - private bool _drawOperationsCloned; - private Matrix transformRestore; - - /// - /// Initializes a new instance of the class. - /// - /// The visual that this node represents. - /// The parent scene graph node, if any. - public VisualNode(Visual visual, IVisualNode? parent) - { - Visual = visual ?? throw new ArgumentNullException(nameof(visual)); - Parent = parent; - HasAncestorGeometryClip = parent != null && - (parent.HasAncestorGeometryClip || parent.GeometryClip != null); - } - - /// - public Visual Visual { get; } - - /// - public IVisualNode? Parent { get; } - - /// - public CornerRadius ClipToBoundsRadius { get; set; } - - /// - public Matrix Transform { get; set; } - - /// - public Rect Bounds => _bounds ?? CalculateBounds(); - - /// - public Rect ClipBounds { get; set; } - - /// - public Rect LayoutBounds { get; set; } - - /// - public bool ClipToBounds { get; set; } - - /// - public IGeometryImpl? GeometryClip { get; set; } - - /// - public bool HasAncestorGeometryClip { get; } - - /// - public double Opacity - { - get { return _opacity; } - set - { - if (_opacity != value) - { - _opacity = value; - OpacityChanged = true; - } - } - } - - /// - /// Gets or sets the opacity mask for the scene graph node. - /// - public IBrush? OpacityMask { get; set; } - - /// - /// Gets a value indicating whether this node in the scene graph has already - /// been updated in the current update pass. - /// - public bool SubTreeUpdated { get; set; } - - /// - /// Gets a value indicating whether the property has changed. - /// - public bool OpacityChanged { get; private set; } - - public Visual? LayerRoot { get; set; } - - /// - public IReadOnlyList Children => _children ?? EmptyChildren; - - /// - public IReadOnlyList> DrawOperations => _drawOperations ?? EmptyDrawOperations; - - /// - /// Adds a child to the collection. - /// - /// The child to add. - public void AddChild(IVisualNode child) - { - if (child.Disposed) - { - throw new ObjectDisposedException("Visual node for {node.Visual}"); - } - - if (child.Parent != this) - { - throw new AvaloniaInternalException("VisualNode added to wrong parent."); - } - - EnsureChildrenCreated(); - _children.Add(child); - } - - /// - /// Adds an operation to the collection. - /// - /// The operation to add. - public void AddDrawOperation(IRef operation) - { - EnsureDrawOperationsCreated(); - _drawOperations.Add(operation.Clone()); - } - - /// - /// Removes a child from the collection. - /// - /// The child to remove. - public void RemoveChild(IVisualNode child) - { - EnsureChildrenCreated(); - _children.Remove(child); - } - - /// - /// Replaces a child in the collection. - /// - /// The child to be replaced. - /// The child to add. - public void ReplaceChild(int index, IVisualNode node) - { - if (node.Disposed) - { - throw new ObjectDisposedException("Visual node for {node.Visual}"); - } - - if (node.Parent != this) - { - throw new AvaloniaInternalException("VisualNode added to wrong parent."); - } - - EnsureChildrenCreated(); - _children[index] = node; - } - - /// - /// Replaces an item in the collection. - /// - /// The operation to be replaced. - /// The operation to add. - public void ReplaceDrawOperation(int index, IRef operation) - { - EnsureDrawOperationsCreated(); - var old = _drawOperations[index]; - _drawOperations[index] = operation.Clone(); - old.Dispose(); - } - - /// - /// Sorts the collection according to the order of the visual's - /// children and their z-index. - /// - /// The scene that the node is a part of. - public void SortChildren(Scene scene) - { - if (_children == null || _children.Count <= 1) - { - return; - } - - var keys = new List(Visual.VisualChildren.Count); - - for (var i = 0; i < Visual.VisualChildren.Count; ++i) - { - var child = Visual.VisualChildren[i]; - var zIndex = child.ZIndex; - keys.Add(((long)zIndex << 32) + i); - } - - keys.Sort(); - _children.Clear(); - - foreach (var i in keys) - { - var child = Visual.VisualChildren[(int)(i & 0xffffffff)]; - var node = scene.FindNode(child); - - if (node != null) - { - _children.Add(node); - } - } - } - - /// - /// Removes items in the collection from the specified index - /// to the end. - /// - /// The index of the first child to be removed. - public void TrimChildren(int first) - { - if (first < _children?.Count) - { - EnsureChildrenCreated(); - for (int i = first; i < _children.Count; i++) - { - _children[i].Dispose(); - } - _children.RemoveRange(first, _children.Count - first); - } - } - - /// - /// Removes items in the collection from the specified index - /// to the end. - /// - /// The index of the first operation to be removed. - public void TrimDrawOperations(int first) - { - if (first < _drawOperations?.Count) - { - EnsureDrawOperationsCreated(); - for (int i = first; i < _drawOperations.Count; i++) - { - _drawOperations[i].Dispose(); - } - _drawOperations.RemoveRange(first, _drawOperations.Count - first); - } - } - - /// - /// Makes a copy of the node - /// - /// The new parent node. - /// A cloned node. - public VisualNode Clone(IVisualNode? parent) - { - return new VisualNode(Visual, parent) - { - Transform = Transform, - ClipBounds = ClipBounds, - ClipToBoundsRadius = ClipToBoundsRadius, - ClipToBounds = ClipToBounds, - LayoutBounds = LayoutBounds, - GeometryClip = GeometryClip, - _opacity = Opacity, - OpacityMask = OpacityMask, - _drawOperations = _drawOperations, - _drawOperationsRefCounter = _drawOperationsRefCounter?.Clone(), - _drawOperationsCloned = true, - LayerRoot= LayerRoot, - }; - } - - /// - public bool HitTest(Point p) - { - var drawOperations = DrawOperations; - var drawOperationsCount = drawOperations.Count; - - for (var i = 0; i < drawOperationsCount; i++) - { - var operation = drawOperations[i]; - - if (operation?.Item?.HitTest(p) == true) - { - return true; - } - } - - return false; - } - - /// - public void BeginRender(IDrawingContextImpl context, bool skipOpacity) - { - transformRestore = context.Transform; - - if (ClipToBounds) - { - context.Transform = Matrix.Identity; - if (ClipToBoundsRadius.IsDefault) - context.PushClip(ClipBounds); - else - context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); - } - - context.Transform = Transform; - - if (Opacity != 1 && !skipOpacity) - { - context.PushOpacity(Opacity); - } - - if (GeometryClip != null) - { - context.PushGeometryClip(GeometryClip); - } - - if (OpacityMask != null) - { - context.PushOpacityMask(OpacityMask, LayoutBounds); - } - } - - /// - public void EndRender(IDrawingContextImpl context, bool skipOpacity) - { - if (OpacityMask != null) - { - context.PopOpacityMask(); - } - - if (GeometryClip != null) - { - context.PopGeometryClip(); - } - - if (Opacity != 1 && !skipOpacity) - { - context.PopOpacity(); - } - - if (ClipToBounds) - { - context.Transform = Matrix.Identity; - context.PopClip(); - } - - context.Transform = transformRestore; - } - - internal void TryPreallocateChildren(int count) - { - if (count == 0) - { - return; - } - - EnsureChildrenCreated(count); - } - - private Rect CalculateBounds() - { - var result = new Rect(); - - if (_drawOperations != null) - { - foreach (var operation in _drawOperations) - { - result = result.Union(operation.Item.Bounds); - } - } - - _bounds = result; - return result; - } - - [MemberNotNull(nameof(_children))] - private void EnsureChildrenCreated(int capacity = 0) - { - if (_children == null) - { - _children = new List(capacity); - } - } - - /// - /// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations). - /// - [MemberNotNull(nameof(_drawOperations))] - private void EnsureDrawOperationsCreated() - { - if (_drawOperations == null) - { - _drawOperations = new List>(); - _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); - _drawOperationsCloned = false; - } - else if (_drawOperationsCloned) - { - var oldDrawOperations = _drawOperations; - - _drawOperations = new List>(oldDrawOperations.Count); - - foreach (var drawOperation in oldDrawOperations) - { - _drawOperations.Add(drawOperation.Clone()); - } - - _drawOperationsRefCounter?.Dispose(); - _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); - _drawOperationsCloned = false; - } - } - - /// - /// Creates disposable that will dispose all items in passed draw operations after being disposed. - /// It is crucial that we don't capture current instance - /// as draw operations can be cloned and may persist across subsequent scenes. - /// - /// Draw operations that need to be disposed. - /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(List> drawOperations) - { - return Disposable.Create(drawOperations, operations => - { - foreach (var operation in operations) - { - operation.Dispose(); - } - }); - } - - public bool Disposed { get; private set; } - - public void Dispose() - { - _drawOperationsRefCounter?.Dispose(); - - Disposed = true; - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs index 73840376fe..cac4d1693a 100644 --- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs +++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering /// /// The render root that has been updated. /// The updated area. - internal SceneInvalidatedEventArgs( + public SceneInvalidatedEventArgs( IRenderRoot root, Rect dirtyRect) { diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs new file mode 100644 index 0000000000..5d0bba2341 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Reactive; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Rendering; + +/// +/// A helper class for composition-backed swapchains, should not be a public API yet +/// +abstract class SwapchainBase : IAsyncDisposable where TImage : class, ISwapchainImage +{ + protected ICompositionGpuInterop Interop { get; } + protected CompositionDrawingSurface Target { get; } + private List _pendingImages = new(); + + public SwapchainBase(ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + Interop = interop; + Target = target; + } + + static bool IsBroken(TImage image) => image.LastPresent?.IsFaulted == true; + static bool IsReady(TImage image) => image.LastPresent == null || image.LastPresent.Status == TaskStatus.RanToCompletion; + + TImage? CleanupAndFindNextImage(PixelSize size) + { + TImage? firstFound = null; + var foundMultiple = false; + + for (var c = _pendingImages.Count - 1; c > -1; c--) + { + var image = _pendingImages[c]; + var ready = IsReady(image); + var matches = image.Size == size; + if (IsBroken(image) || (!matches && ready)) + { + image.DisposeAsync(); + _pendingImages.RemoveAt(c); + } + + if (matches && ready) + { + if (firstFound == null) + firstFound = image; + else + foundMultiple = true; + } + + } + + // We are making sure that there was at least one image of the same size in flight + // Otherwise we might encounter UI thread lockups + return foundMultiple ? firstFound : null; + } + + protected abstract TImage CreateImage(PixelSize size); + + protected IDisposable BeginDrawCore(PixelSize size, out TImage image) + { + var img = CleanupAndFindNextImage(size) ?? CreateImage(size); + + img.BeginDraw(); + _pendingImages.Remove(img); + image = img; + return Disposable.Create(() => + { + img.Present(); + _pendingImages.Add(img); + }); + } + + public async ValueTask DisposeAsync() + { + foreach (var img in _pendingImages) + await img.DisposeAsync(); + } +} + + +interface ISwapchainImage : IAsyncDisposable +{ + PixelSize Size { get; } + Task? LastPresent { get; } + void BeginDraw(); + void Present(); +} diff --git a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs index 1bbf804b5f..7f2eedc98c 100644 --- a/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs +++ b/src/Avalonia.Base/Rendering/UiThreadRenderTimer.cs @@ -8,13 +8,20 @@ namespace Avalonia.Rendering /// /// Render timer that ticks on UI thread. Useful for debugging or bootstrapping on new platforms /// - public class UiThreadRenderTimer : DefaultRenderTimer { + /// + /// Initializes a new instance of the class. + /// + /// The number of frames per second at which the loop should run. public UiThreadRenderTimer(int framesPerSecond) : base(framesPerSecond) { } + /// + public override bool RunsInBackground => false; + + /// protected override IDisposable StartCore(Action tick) { bool cancelled = false; diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 6043175eee..94af8385a8 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -28,7 +28,7 @@ namespace Avalonia public class StyledElement : Animatable, IDataContextProvider, ILogical, - IResourceHost, + IThemeVariantHost, IStyleHost, IStyleable, ISetLogicalParent, @@ -41,7 +41,11 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = AvaloniaProperty.Register( nameof(DataContext), + defaultValue: null, inherits: true, + defaultBindingMode: BindingMode.OneWay, + validate: null, + coerce: null, notifying: DataContextNotifying); /// @@ -139,6 +143,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -295,6 +302,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -439,11 +449,11 @@ namespace Avalonia void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - (_styles?.TryGetResource(key, out value) ?? false); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + (_styles?.TryGetResource(key, theme, out value) ?? false); } /// @@ -494,13 +504,7 @@ namespace Avalonia NotifyResourcesChanged(); } -#nullable disable - RaisePropertyChanged( - ParentProperty, - new Optional(old), - new BindingValue(Parent), - BindingPriority.LocalValue); -#nullable enable + RaisePropertyChanged(ParentProperty, old, Parent); } } @@ -620,7 +624,20 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); + else + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } } private protected virtual void OnControlThemeChanged() @@ -658,7 +675,7 @@ namespace Avalonia { var theme = Theme; - // Explitly set Theme property takes precedence. + // Explicitly set Theme property takes precedence. if (theme is not null) return theme; diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 019ed09c20..8695918c18 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -1,14 +1,18 @@ using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Avalonia.Utilities; namespace Avalonia { /// /// A styled avalonia property. /// - public class StyledProperty : StyledPropertyBase + public class StyledProperty : AvaloniaProperty, IStyledPropertyAccessor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the property. /// The type of the class that registers the property. @@ -23,20 +27,30 @@ namespace Avalonia bool inherits = false, Func? validate = null, Action? notifying = null) - : base(name, ownerType, metadata, inherits, validate, notifying) + : base(name, ownerType, metadata, notifying) { + Inherits = inherits; + ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// - /// Initializes a new instance of the class. + /// Gets the value validation callback for the property. /// - /// The property to add the owner to. - /// The type of the class that registers the property. - internal StyledProperty(StyledPropertyBase source, Type ownerType) - : base(source, ownerType) - { - } - + public Func? ValidateValue { get; } + + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + /// /// Registers the property on another type. /// @@ -47,5 +61,193 @@ namespace Avalonia AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); return this; } + + public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + + /// + /// Gets the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public TValue GetDefaultValue(Type type) + { + return GetMetadata(type).DefaultValue; + } + + /// + /// Gets the property metadata for the specified type. + /// + /// The type. + /// + /// The property metadata. + /// + public new StyledPropertyMetadata GetMetadata(Type type) + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + return (StyledPropertyMetadata)base.GetMetadata(type); + } + + /// + /// Overrides the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public void OverrideDefaultValue(TValue defaultValue) where T : AvaloniaObject + { + OverrideDefaultValue(typeof(T), defaultValue); + } + + /// + /// Overrides the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public void OverrideDefaultValue(Type type, TValue defaultValue) + { + OverrideMetadata(type, new StyledPropertyMetadata(defaultValue)); + } + + /// + /// Overrides the metadata for the property on the specified type. + /// + /// The type. + /// The metadata. + public void OverrideMetadata(StyledPropertyMetadata metadata) where T : AvaloniaObject + { + base.OverrideMetadata(typeof(T), metadata); + } + + /// + /// Overrides the metadata for the property on the specified type. + /// + /// The type. + /// The metadata. + public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) + { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + + HasCoercion |= metadata.CoerceValue != null; + + base.OverrideMetadata(type, metadata); + } + + /// + /// Gets the string representation of the property. + /// + /// The property's string representation. + public override string ToString() + { + return Name; + } + + /// + object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + bool IStyledPropertyAccessor.ValidateValue(object? value) + { + if (value is null && !typeof(TValue).IsValueType) + return ValidateValue?.Invoke(default!) ?? true; + if (value is TValue typed) + return ValidateValue?.Invoke(typed) ?? true; + return false; + } + + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) + { + return o.GetValueStore().CreateEffectiveValue(this); + } + + /// + internal override void RouteClearValue(AvaloniaObject o) + { + o.ClearValue(this); + } + + /// + internal override object? RouteGetValue(AvaloniaObject o) + { + return o.GetValue(this); + } + + /// + internal override object? RouteGetBaseValue(AvaloniaObject o) + { + var value = o.GetBaseValue(this); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + + /// + internal override IDisposable? RouteSetValue( + AvaloniaObject target, + object? value, + BindingPriority priority) + { + if (ShouldSetValue(target, value, out var converted)) + return target.SetValue(this, converted, priority); + return null; + } + + internal override void RouteSetCurrentValue(AvaloniaObject target, object? value) + { + if (ShouldSetValue(target, value, out var converted)) + target.SetCurrentValue(this, converted); + } + + internal override IDisposable RouteBind( + AvaloniaObject target, + IObservable source, + BindingPriority priority) + { + return target.Bind(this, source, priority); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + private bool ShouldSetValue(AvaloniaObject target, object? value, [NotNullWhen(true)] out TValue? converted) + { + if (value == BindingOperations.DoNothing) + { + converted = default; + return false; + } + if (value == UnsetValue) + { + target.ClearValue(this); + converted = default; + return false; + } + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var v)) + { + converted = (TValue)v!; + return true; + } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } + + private object? GetDefaultBoxedValue(Type type) + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + return GetMetadata(type).DefaultValue; + } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs deleted file mode 100644 index a281a7b7f6..0000000000 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ /dev/null @@ -1,250 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Avalonia.Data; -using Avalonia.PropertyStore; -using Avalonia.Reactive; -using Avalonia.Styling; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Base class for styled properties. - /// - public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor - { - private readonly bool _inherits; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the property. - /// The type of the class that registers the property. - /// The property metadata. - /// Whether the property inherits its value. - /// A value validation callback. - /// A callback. - protected StyledPropertyBase( - string name, - Type ownerType, - StyledPropertyMetadata metadata, - bool inherits = false, - Func? validate = null, - Action? notifying = null) - : base(name, ownerType, metadata, notifying) - { - _inherits = inherits; - ValidateValue = validate; - HasCoercion |= metadata.CoerceValue != null; - - if (validate?.Invoke(metadata.DefaultValue) == false) - { - throw new ArgumentException( - $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The property to add the owner to. - /// The type of the class that registers the property. - protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) - : base(source, ownerType, null) - { - _inherits = source.Inherits; - } - - /// - /// Gets a value indicating whether the property inherits its value. - /// - /// - /// A value indicating whether the property inherits its value. - /// - public override bool Inherits => _inherits; - - /// - /// Gets the value validation callback for the property. - /// - public Func? ValidateValue { get; } - - /// - /// Gets a value indicating whether this property has any value coercion callbacks defined - /// in its metadata. - /// - internal bool HasCoercion { get; private set; } - - public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) - { - var metadata = GetMetadata(instance.GetType()); - - if (metadata.CoerceValue != null) - { - return metadata.CoerceValue.Invoke(instance, baseValue); - } - - return baseValue; - } - - /// - /// Gets the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public TValue GetDefaultValue(Type type) - { - return GetMetadata(type).DefaultValue; - } - - /// - /// Gets the property metadata for the specified type. - /// - /// The type. - /// - /// The property metadata. - /// - public new StyledPropertyMetadata GetMetadata(Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - return (StyledPropertyMetadata)base.GetMetadata(type); - } - - /// - /// Overrides the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public void OverrideDefaultValue(TValue defaultValue) where T : AvaloniaObject - { - OverrideDefaultValue(typeof(T), defaultValue); - } - - /// - /// Overrides the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public void OverrideDefaultValue(Type type, TValue defaultValue) - { - OverrideMetadata(type, new StyledPropertyMetadata(defaultValue)); - } - - /// - /// Overrides the metadata for the property on the specified type. - /// - /// The type. - /// The metadata. - public void OverrideMetadata(StyledPropertyMetadata metadata) where T : AvaloniaObject - { - base.OverrideMetadata(typeof(T), metadata); - } - - /// - /// Overrides the metadata for the property on the specified type. - /// - /// The type. - /// The metadata. - public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) - { - if (ValidateValue != null) - { - if (!ValidateValue(metadata.DefaultValue)) - { - throw new ArgumentException( - $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); - } - } - - HasCoercion |= metadata.CoerceValue != null; - - base.OverrideMetadata(type, metadata); - } - - /// - /// Gets the string representation of the property. - /// - /// The property's string representation. - public override string ToString() - { - return Name; - } - - /// - object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - - bool IStyledPropertyAccessor.ValidateValue(object? value) - { - if (value is null && !typeof(TValue).IsValueType) - return ValidateValue?.Invoke(default!) ?? true; - if (value is TValue typed) - return ValidateValue?.Invoke(typed) ?? true; - return false; - } - - internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) - { - return new EffectiveValue(o, this); - } - - /// - internal override void RouteClearValue(AvaloniaObject o) - { - o.ClearValue(this); - } - - /// - internal override object? RouteGetValue(AvaloniaObject o) - { - return o.GetValue(this); - } - - /// - internal override object? RouteGetBaseValue(AvaloniaObject o) - { - var value = o.GetBaseValue(this); - return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; - } - - /// - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] - internal override IDisposable? RouteSetValue( - AvaloniaObject target, - object? value, - BindingPriority priority) - { - if (value == BindingOperations.DoNothing) - { - return null; - } - else if (value == UnsetValue) - { - target.ClearValue(this); - return null; - } - else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) - { - return target.SetValue(this, (TValue)converted!, priority); - } - else - { - var type = value?.GetType().FullName ?? "(null)"; - throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); - } - } - - internal override IDisposable RouteBind( - AvaloniaObject target, - IObservable source, - BindingPriority priority) - { - return target.Bind(this, source, priority); - } - - private object? GetDefaultBoxedValue(Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - return GetMetadata(type).DefaultValue; - } - } -} diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs new file mode 100644 index 0000000000..01583148a8 --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for the host element with a theme variant. +/// +[Unstable] +public interface IThemeVariantHost : IResourceHost +{ + /// + /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty. + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs index 68a9b8aafe..af5540ecf0 100644 --- a/src/Avalonia.Base/Styling/PropertySetterInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterInstance.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling ISetterInstance { private readonly StyledElement _target; - private readonly StyledPropertyBase? _styledProperty; + private readonly StyledProperty? _styledProperty; private readonly DirectPropertyBase? _directProperty; private readonly T _value; private IDisposable? _subscription; @@ -22,7 +22,7 @@ namespace Avalonia.Styling public PropertySetterInstance( StyledElement target, - StyledPropertyBase property, + StyledProperty property, T value) { _target = target; diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index b7b44a7dfe..093597c6a0 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); } throw new NotSupportedException(); diff --git a/src/Avalonia.Base/Styling/StyleBase.cs b/src/Avalonia.Base/Styling/StyleBase.cs index e8fc40ca4c..7dfa516bce 100644 --- a/src/Avalonia.Base/Styling/StyleBase.cs +++ b/src/Avalonia.Base/Styling/StyleBase.cs @@ -74,16 +74,16 @@ namespace Avalonia.Styling public event EventHandler? OwnerChanged; - public bool TryGetResource(object key, out object? result) + public bool TryGetResource(object key, ThemeVariant? themeVariant, out object? result) { - if (_resources is not null && _resources.TryGetResource(key, out result)) + if (_resources is not null && _resources.TryGetResource(key, themeVariant, out result)) return true; if (_children is not null) { for (var i = 0; i < _children.Count; ++i) { - if (_children[i].TryGetResource(key, out result)) + if (_children[i].TryGetResource(key, themeVariant, out result)) return true; } } diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 1b1886335f..5d5b1617aa 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -115,16 +115,16 @@ namespace Avalonia.Styling } /// - public bool TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { - if (_resources != null && _resources.TryGetResource(key, out value)) + if (_resources != null && _resources.TryGetResource(key, theme, out value)) { return true; } for (var i = Count - 1; i >= 0; --i) { - if (this[i].TryGetResource(key, out value)) + if (this[i].TryGetResource(key, theme, out value)) { return true; } diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs new file mode 100644 index 0000000000..389136b0f5 --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -0,0 +1,120 @@ +using System; +using System.ComponentModel; +using System.Text; +using Avalonia.Platform; + +namespace Avalonia.Styling; + +/// +/// Specifies a UI theme variant that should be used for the Control and Application types. +/// +[TypeConverter(typeof(ThemeVariantTypeConverter))] +public sealed record ThemeVariant +{ + /// + /// Defines the ActualThemeVariant property. + /// + internal static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + "ActualThemeVariant", + inherits: true); + + /// + /// Defines the RequestedThemeVariant property. + /// + internal static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + "RequestedThemeVariant", defaultValue: Default); + + /// + /// Creates a new instance of the + /// + /// Key of the theme variant by which variants are compared. + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// Thrown if inheritVariant is a reference to the which is ambiguous value to inherit. + /// Thrown if key is null. + public ThemeVariant(object key, ThemeVariant? inheritVariant) + { + Key = key ?? throw new ArgumentNullException(nameof(key)); + InheritVariant = inheritVariant; + + if (inheritVariant == Default) + { + throw new ArgumentException("Inheriting default theme variant is not supported.", nameof(inheritVariant)); + } + } + + private ThemeVariant(object key) + { + Key = key; + } + + /// + /// Key of the theme variant by which variants are compared. + /// + public object Key { get; } + + /// + /// Reference to a theme variant which should be used, if resource wasn't found for the requested variant. + /// + public ThemeVariant? InheritVariant { get; } + + /// + /// Inherit theme variant from the parent. If set on Application, system theme is inherited. + /// Using Default as the ResourceDictionary.Key marks this dictionary as a fallback in case the theme variant or resource key is not found in other theme dictionaries. + /// + public static ThemeVariant Default { get; } = new(nameof(Default)); + + /// + /// Use the Light theme variant. + /// + public static ThemeVariant Light { get; } = new(nameof(Light)); + + /// + /// Use the Dark theme variant. + /// + public static ThemeVariant Dark { get; } = new(nameof(Dark)); + + public override string ToString() + { + return Key.ToString() ?? $"ThemeVariant {{ Key = {Key} }}"; + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public bool Equals(ThemeVariant? other) + { + return Key == other?.Key; + } + + public static explicit operator ThemeVariant(PlatformThemeVariant themeVariant) + { + return themeVariant switch + { + PlatformThemeVariant.Light => Light, + PlatformThemeVariant.Dark => Dark, + _ => throw new ArgumentOutOfRangeException(nameof(themeVariant), themeVariant, null) + }; + } + + public static explicit operator PlatformThemeVariant?(ThemeVariant themeVariant) + { + if (themeVariant == Light) + { + return PlatformThemeVariant.Light; + } + else if (themeVariant == Dark) + { + return PlatformThemeVariant.Dark; + } + else if (themeVariant.InheritVariant is { } inheritVariant) + { + return (PlatformThemeVariant?)inheritVariant; + } + + return null; + } +} diff --git a/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs new file mode 100644 index 0000000000..acb2d7651b --- /dev/null +++ b/src/Avalonia.Base/Styling/ThemeVariantTypeConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Styling; + +public class ThemeVariantTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + return value switch + { + nameof(ThemeVariant.Default) => ThemeVariant.Default, + nameof(ThemeVariant.Light) => ThemeVariant.Light, + nameof(ThemeVariant.Dark) => ThemeVariant.Dark, + _ => throw new NotSupportedException("ThemeVariant type converter supports only build in variants. For custom variants please use x:Static markup extension.") + }; + } +} diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index e6b67bd383..bbbcc39ecc 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -12,14 +11,13 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder : IDisposable - where T : struct + internal struct ArrayBuilder { private const int DefaultCapacity = 4; private const int MaxCoreClrArrayLength = 0x7FeFFFFF; // Starts out null, initialized on first Add. - private T[] _data; + private T[]? _data; private int _size; /// @@ -49,6 +47,12 @@ namespace Avalonia.Utilities } } + /// + /// Gets the current capacity of the array. + /// + public int Capacity + => _data?.Length ?? 0; + /// /// Returns a reference to specified element of the array. /// @@ -116,14 +120,44 @@ namespace Avalonia.Utilities return slice; } + /// + /// Appends an item. + /// + /// The item to append. + public void AddItem(T value) + { + var index = Length++; + _data![index] = value; + } + /// /// Clears the array. /// Allocated memory is left intact for future usage. /// public void Clear() { - // No need to actually clear since we're not allowing reference types. +#if NET6_0_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ClearArray(); + } + else + { + _size = 0; + } +#else + ClearArray(); +#endif + } + + private void ClearArray() + { + var size = _size; _size = 0; + if (size > 0) + { + Array.Clear(_data!, 0, size); + } } private void EnsureCapacity(int min) @@ -136,7 +170,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : length * 2; + var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; if (newCapacity > MaxCoreClrArrayLength) { @@ -145,15 +179,14 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = min; + newCapacity = (uint)min; } - var array = ArrayPool.Shared.Rent(newCapacity); + var array = new T[newCapacity]; if (_size > 0) { Array.Copy(_data!, array, _size); - ArrayPool.Shared.Return(_data!); } _data = array; @@ -182,13 +215,12 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); - - public void Dispose() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } + + /// + /// Returns the current state of the array as a span. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AsSpan() => _data.AsSpan(0, _size); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index b70088a907..17e75f2f95 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -18,7 +17,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the slice. internal readonly struct ArraySlice : IReadOnlyList - where T : struct { /// /// Gets an empty @@ -186,13 +184,6 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; - - public void ReturnRent() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } + diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index b7060d2e21..defc9b1639 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -14,7 +14,6 @@ // under the License. // Copied from: https://github.com/toptensoftware/RichTextKit -using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -39,7 +38,7 @@ namespace Avalonia.Utilities /// The value to search for /// The comparer /// The index of the found item; otherwise the bitwise complement of the index of the next larger item - public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) where T : IComparable + public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) { return list.BinarySearch(0, list.Count, value, comparer); } diff --git a/src/Avalonia.Base/Utilities/StopwatchHelper.cs b/src/Avalonia.Base/Utilities/StopwatchHelper.cs new file mode 100644 index 0000000000..4719226ea4 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StopwatchHelper.cs @@ -0,0 +1,19 @@ +using System; +using System.Diagnostics; + +namespace Avalonia.Utilities; + +/// +/// Allows using as timestamps without allocating. +/// +/// Equivalent to Stopwatch.GetElapsedTime in .NET 7. +internal static class StopwatchHelper +{ + private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + + public static TimeSpan GetElapsedTime(long startingTimestamp) + => GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); + + public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + => new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks)); +} diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3c44dd63ce..fafafabd82 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -212,7 +212,7 @@ namespace Avalonia.Utilities var toTypeConverter = TypeDescriptor.GetConverter(toUnderl); - if (toTypeConverter.CanConvertFrom(from) == true) + if (toTypeConverter.CanConvertFrom(from)) { result = toTypeConverter.ConvertFrom(null, culture, value); return true; @@ -220,7 +220,7 @@ namespace Avalonia.Utilities var fromTypeConverter = TypeDescriptor.GetConverter(from); - if (fromTypeConverter.CanConvertTo(toUnderl) == true) + if (fromTypeConverter.CanConvertTo(toUnderl)) { result = fromTypeConverter.ConvertTo(null, culture, value, toUnderl); return true; @@ -329,7 +329,7 @@ namespace Avalonia.Utilities } [RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)] - public static T ConvertImplicit(object value) + public static T ConvertImplicit(object? value) { if (TryConvertImplicit(typeof(T), value, out var result)) { @@ -369,11 +369,6 @@ namespace Avalonia.Utilities /// public static bool IsNumeric(Type type) { - if (type == null) - { - return false; - } - var underlyingType = Nullable.GetUnderlyingType(type); if (underlyingType != null) diff --git a/src/Avalonia.Base/Utilities/WeakEvent.cs b/src/Avalonia.Base/Utilities/WeakEvent.cs index e72606bf70..237a491615 100644 --- a/src/Avalonia.Base/Utilities/WeakEvent.cs +++ b/src/Avalonia.Base/Utilities/WeakEvent.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Threading; @@ -15,7 +11,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event { private readonly Func, Action> _subscribe; - readonly ConditionalWeakTable _subscriptions = new(); + private readonly ConditionalWeakTable _subscriptions = new(); internal WeakEvent( Action> subscribe, @@ -51,56 +47,6 @@ public class WeakEvent : WeakEvent where TEventArgs : Event private readonly WeakEvent _ev; private readonly TSender _target; private readonly Action _compact; - - struct Entry - { - WeakReference>? _reference; - int _hashCode; - - public Entry(IWeakEventSubscriber r) - { - if (r == null) - { - _reference = null; - _hashCode = 0; - return; - } - - _hashCode = r.GetHashCode(); - _reference = new WeakReference>(r); - } - - public bool IsEmpty - { - get - { - if (_reference == null) - return true; - if (_reference.TryGetTarget(out _)) - return false; - _reference = null; - return true; - } - } - - public bool TryGetTarget([MaybeNullWhen(false)]out IWeakEventSubscriber target) - { - if (_reference == null) - { - target = null!; - return false; - } - return _reference.TryGetTarget(out target); - } - - public bool Equals(IWeakEventSubscriber r) - { - if (_reference == null || r.GetHashCode() != _hashCode) - return false; - return _reference.TryGetTarget(out var target) && target == r; - } - } - private readonly Action _unsubscribe; private readonly WeakHashList> _list = new(); private bool _compactScheduled; @@ -114,7 +60,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event _unsubscribe = ev._subscribe(target, OnEvent); } - void Destroy() + private void Destroy() { if(_destroyed) return; @@ -134,15 +80,15 @@ public class WeakEvent : WeakEvent where TEventArgs : Event ScheduleCompact(); } - void ScheduleCompact() + private void ScheduleCompact() { if(_compactScheduled || _destroyed) return; _compactScheduled = true; Dispatcher.UIThread.Post(_compact, DispatcherPriority.Background); } - - void Compact() + + private void Compact() { if(!_compactScheduled) return; @@ -152,7 +98,7 @@ public class WeakEvent : WeakEvent where TEventArgs : Event Destroy(); } - void OnEvent(object? sender, TEventArgs eventArgs) + private void OnEvent(object? sender, TEventArgs eventArgs) { var alive = _list.GetAlive(); if(alive == null) @@ -196,4 +142,4 @@ public class WeakEvent return () => unsubscribe(s, handler); }); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs index 020ba7a6d9..ef143144e6 100644 --- a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -60,8 +60,7 @@ namespace Avalonia.Utilities private static class SubscriptionTypeStorage where TArgs : EventArgs where TSubscriber : class { - public static readonly ConditionalWeakTable> Subscribers - = new ConditionalWeakTable>(); + public static readonly ConditionalWeakTable> Subscribers = new(); } private class SubscriptionDic : Dictionary> @@ -69,8 +68,7 @@ namespace Avalonia.Utilities { } - private static readonly Dictionary> Accessors - = new Dictionary>(); + private static readonly Dictionary> s_accessors = new(); private class Subscription where T : EventArgs where TSubscriber : class { @@ -81,18 +79,17 @@ namespace Avalonia.Utilities private readonly Delegate _delegate; private Descriptor[] _data = new Descriptor[2]; - private int _count = 0; + private int _count; - delegate void CallerDelegate(TSubscriber s, object sender, T args); - - struct Descriptor + private delegate void CallerDelegate(TSubscriber s, object? sender, T args); + + private struct Descriptor { - public WeakReference Subscriber; - public CallerDelegate Caller; + public WeakReference? Subscriber; + public CallerDelegate? Caller; } - private static Dictionary s_Callers = - new Dictionary(); + private static readonly Dictionary s_callers = new(); public Subscription(SubscriptionDic sdic, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents)] Type targetType, @@ -101,8 +98,8 @@ namespace Avalonia.Utilities _sdic = sdic; _target = target; _eventName = eventName; - if (!Accessors.TryGetValue(targetType, out var evDic)) - Accessors[targetType] = evDic = new Dictionary(); + if (!s_accessors.TryGetValue(targetType, out var evDic)) + s_accessors[targetType] = evDic = new Dictionary(); if (evDic.TryGetValue(eventName, out var info)) { @@ -123,12 +120,12 @@ namespace Avalonia.Utilities var del = new Action(OnEvent); _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType!, del.Target); - _info.AddMethod!.Invoke(target, new[] { _delegate }); + _info.AddMethod!.Invoke(target, new object?[] { _delegate }); } - void Destroy() + private void Destroy() { - _info.RemoveMethod!.Invoke(_target, new[] { _delegate }); + _info.RemoveMethod!.Invoke(_target, new object?[] { _delegate }); _sdic.Remove(_eventName); } @@ -146,8 +143,8 @@ namespace Avalonia.Utilities MethodInfo method = s.Method; var subscriber = (TSubscriber)s.Target!; - if (!s_Callers.TryGetValue(method, out var caller)) - s_Callers[method] = caller = + if (!s_callers.TryGetValue(method, out var caller)) + s_callers[method] = caller = (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, method); _data[_count] = new Descriptor { @@ -178,7 +175,7 @@ namespace Avalonia.Utilities } } - void Compact(bool preventDestroy = false) + private void Compact(bool preventDestroy = false) { int empty = -1; for (int c = 0; c < _count; c++) @@ -206,15 +203,15 @@ namespace Avalonia.Utilities Destroy(); } - void OnEvent(object sender, T eventArgs) + private void OnEvent(object? sender, T eventArgs) { var needCompact = false; - for(var c=0; c<_count; c++) + for (var c = 0; c < _count; c++) { - var r = _data[c].Subscriber; + var r = _data[c].Subscriber!; if (r.TryGetTarget(out var sub)) { - _data[c].Caller(sub, sender, eventArgs); + _data[c].Caller!(sub, sender, eventArgs); } else needCompact = true; diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs index 6da899bab2..2f62564e0e 100644 --- a/src/Avalonia.Base/Utilities/WeakEvents.cs +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Input; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -20,15 +21,30 @@ public class WeakEvents }); /// - /// Represents PropertyChanged event from + /// Represents PropertyChanged event from with auto-dispatching to the UI thread /// public static readonly WeakEvent - PropertyChanged = WeakEvent.Register( + ThreadSafePropertyChanged = WeakEvent.Register( (s, h) => { - PropertyChangedEventHandler handler = (_, e) => h(s, e); + bool unsubscribed = false; + PropertyChangedEventHandler handler = (_, e) => + { + if (Dispatcher.UIThread.CheckAccess()) + h(s, e); + else + Dispatcher.UIThread.Post(() => + { + if (!unsubscribed) + h(s, e); + }); + }; s.PropertyChanged += handler; - return () => s.PropertyChanged -= handler; + return () => + { + unsubscribed = true; + s.PropertyChanged -= handler; + }; }); diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7fcb53bcea..8b0cc06136 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -36,12 +36,7 @@ namespace Avalonia /// public static readonly DirectProperty BoundsProperty = AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); - - public static readonly DirectProperty TransformedBoundsProperty = - AvaloniaProperty.RegisterDirect( - nameof(TransformedBounds), - o => o.TransformedBounds); - + /// /// Defines the property. /// @@ -116,7 +111,6 @@ namespace Avalonia (s, h) => s.Invalidated -= h); private Rect _bounds; - private TransformedBounds? _transformedBounds; private IRenderRoot? _visualRoot; private Visual? _visualParent; private bool _hasMirrorTransform; @@ -172,11 +166,6 @@ namespace Avalonia protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } } - /// - /// Gets the bounds of the control relative to the window, accounting for rendering transforms. - /// - public TransformedBounds? TransformedBounds => _transformedBounds; - /// /// Gets or sets a value indicating whether the control should be clipped to its bounds. /// @@ -359,7 +348,7 @@ namespace Avalonia /// public void InvalidateVisual() { - VisualRoot?.Renderer?.AddDirty(this); + VisualRoot?.Renderer.AddDirty(this); } /// @@ -460,7 +449,7 @@ namespace Avalonia protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); - VisualRoot?.Renderer?.RecalculateChildren(this); + VisualRoot?.Renderer.RecalculateChildren(this); } /// @@ -488,23 +477,19 @@ namespace Avalonia OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - _visualRoot.Renderer?.RecalculateChildren(_visualParent!); + _visualRoot.Renderer.RecalculateChildren(_visualParent!); if (ZIndex != 0 && VisualParent is Visual parent) parent.HasNonUniformZIndexChildren = true; var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnAttachedToVisualTreeCore(e); - } + child.OnAttachedToVisualTreeCore(e); } } } @@ -523,11 +508,6 @@ namespace Avalonia return CompositionVisual; } - internal void SetTransformedBounds(TransformedBounds? value) - { - SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); - } - /// /// Calls the method /// for this control and all of its visual descendants. @@ -556,20 +536,16 @@ namespace Avalonia } DetachedFromVisualTree?.Invoke(this, e); - e.Root?.Renderer?.AddDirty(this); + e.Root.Renderer.AddDirty(this); var visualChildren = VisualChildren; + var visualChildrenCount = visualChildren.Count; - if (visualChildren != null) + for (var i = 0; i < visualChildrenCount; i++) { - var visualChildrenCount = visualChildren.Count; - - for (var i = 0; i < visualChildrenCount; i++) + if (visualChildren[i] is { } child) { - if (visualChildren[i] is Visual child) - { - child.OnDetachedFromVisualTreeCore(e); - } + child.OnDetachedFromVisualTreeCore(e); } } } @@ -597,7 +573,7 @@ namespace Avalonia /// The new visual parent. protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged(VisualParentProperty, oldParent, newParent); } internal override ParametrizedLogger? GetBindingWarningLogger( @@ -675,7 +651,7 @@ namespace Avalonia parentVisual.HasNonUniformZIndexChildren = true; sender?.InvalidateVisual(); - parent?.VisualRoot?.Renderer?.RecalculateChildren(parent); + parent?.VisualRoot?.Renderer.RecalculateChildren(parent); } /// diff --git a/src/Avalonia.Base/VisualTree/VisualExtensions.cs b/src/Avalonia.Base/VisualTree/VisualExtensions.cs index b58db3b276..9e38c6e7f2 100644 --- a/src/Avalonia.Base/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Base/VisualTree/VisualExtensions.cs @@ -46,7 +46,7 @@ namespace Avalonia.VisualTree Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); var result = 0; - v = v?.VisualParent; + v = v.VisualParent; while (v != null) { @@ -64,17 +64,13 @@ namespace Avalonia.VisualTree /// The first visual. /// The second visual. /// The common ancestor, or null if not found. - public static Visual? FindCommonVisualAncestor(this Visual visual, Visual target) + public static Visual? FindCommonVisualAncestor(this Visual? visual, Visual? target) { - Visual? v = visual ?? throw new ArgumentNullException(nameof(visual)); - - if (target is null) + if (visual is null || target is null) { return null; } - Visual? t = target; - void GoUpwards(ref Visual? node, int count) { for (int i = 0; i < count; ++i) @@ -83,6 +79,9 @@ namespace Avalonia.VisualTree } } + Visual? v = visual; + Visual? t = target; + // We want to find lowest node first, then make sure that both nodes are at the same height. // By doing that we can sometimes find out that other node is our lowest common ancestor. var firstHeight = CalculateDistanceFromRoot(v); @@ -144,7 +143,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First ancestor of given type. - public static T? FindAncestorOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindAncestorOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -173,7 +172,7 @@ namespace Avalonia.VisualTree /// The visual. /// If given visual should be included in search. /// First descendant of given type. - public static T? FindDescendantOfType(this Visual visual, bool includeSelf = false) where T : class + public static T? FindDescendantOfType(this Visual? visual, bool includeSelf = false) where T : class { if (visual is null) { @@ -392,7 +391,7 @@ namespace Avalonia.VisualTree /// True if is an ancestor of ; /// otherwise false. /// - public static bool IsVisualAncestorOf(this Visual visual, Visual target) + public static bool IsVisualAncestorOf(this Visual? visual, Visual? target) { Visual? current = target?.VisualParent; diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 6dfcb2e74d..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -7,6 +7,8 @@ + + @@ -24,18 +26,22 @@ + + + + - - + + @@ -46,4 +52,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index be320246b3..dd5e7d5b01 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -41,18 +41,6 @@ namespace Avalonia.Controls.Primitives { } - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - } - /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -123,28 +111,25 @@ namespace Avalonia.Controls.Primitives IsAlphaMaxForced, IsSaturationValueMaxForced); - if (bgraPixelData != null) + if (_backgroundBitmap != null) { - if (_backgroundBitmap != null) - { - // TODO: CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER - // - // Re-use the existing WriteableBitmap - // This assumes the height, width and byte counts are the same and must be set to null - // elsewhere if that assumption is ever not true. - // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); - - // TODO: ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES - //_backgroundBitmap?.Dispose(); - _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); - } - else - { - _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); - } - - Background = new ImageBrush(_backgroundBitmap); + // TODO: CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER + // + // Re-use the existing WriteableBitmap + // This assumes the height, width and byte counts are the same and must be set to null + // elsewhere if that assumption is ever not true. + // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + + // TODO: ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES + //_backgroundBitmap?.Dispose(); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } + else + { + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); + } + + Background = new ImageBrush(_backgroundBitmap); } } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 2ccf20d460..8f52b059f1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -245,11 +245,12 @@ VerticalAlignment="Center" /> + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 286ca71bc2..fab5415bfb 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -494,11 +494,12 @@ VerticalAlignment="Center" /> + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index b82d36a288..d44902dd70 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -245,11 +245,12 @@ VerticalAlignment="Center" /> + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 556099f56b..0ce6dd5e04 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -456,11 +456,12 @@ VerticalAlignment="Center" /> + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index f35124ee0a..91b65a1f72 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3979,7 +3979,7 @@ namespace Avalonia.Controls { if (focusedObject is Control element) { - parent = element.Parent; + parent = element.VisualParent; if (parent != null) { dataGridWillReceiveRoutedEvent = false; diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index e859a6e725..110590fef2 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -7,6 +7,8 @@ using Avalonia.Data; using System; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Metadata; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -23,6 +25,7 @@ namespace Avalonia.Controls /// //TODO Binding [AssignBinding] + [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] public virtual IBinding Binding { get @@ -111,9 +114,9 @@ namespace Avalonia.Controls if (result != null) { - if(result.Subject != null) + if(result.Source is IAvaloniaSubject subject) { - var bindingHelper = new CellEditBinding(result.Subject); + var bindingHelper = new CellEditBinding(subject); var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); BindingOperations.Apply(target, property, instanceBinding, null); diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 516e9cf6c2..00318e2dd8 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -24,6 +24,7 @@ namespace Avalonia.Controls (o, v) => o.CellTemplate = v); [Content] + [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] public IDataTemplate CellTemplate { get { return _cellTemplate; } @@ -50,6 +51,7 @@ namespace Avalonia.Controls /// /// If this property is the column is read-only. /// + [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] public IDataTemplate CellEditingTemplate { get => _cellEditingCellTemplate; diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index dd8575c989..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -1,6 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.6 0.8 @@ -9,19 +51,6 @@ M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z - - - - - - - - - - - @@ -29,23 +58,10 @@ - + - - - - - - - - - @@ -565,5 +581,6 @@ + diff --git a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs index f4ba644ae6..6aebf05d6b 100644 --- a/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs +++ b/src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Utils { if (child is Control childElement) { - parent = childElement.Parent; + parent = childElement.VisualParent; } } child = parent; diff --git a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj new file mode 100644 index 0000000000..1ec0ee33a7 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj @@ -0,0 +1,20 @@ + + + net6.0;netstandard2.0 + Avalonia.Controls.ItemsRepeater + Avalonia + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Repeater/ElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/IElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/IElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemTemplateWrapper.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs similarity index 91% rename from src/Avalonia.Controls/Repeater/ItemsRepeater.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index deeda75c8b..3d3d01e06e 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -11,6 +11,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; using Avalonia.LogicalTree; +using Avalonia.Metadata; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -43,8 +44,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty LayoutProperty = - AvaloniaProperty.Register(nameof(Layout), new StackLayout()); + public static readonly StyledProperty LayoutProperty = + AvaloniaProperty.Register(nameof(Layout), new StackLayout()); /// /// Defines the property. @@ -52,8 +53,8 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalCacheLengthProperty = AvaloniaProperty.Register(nameof(VerticalCacheLength), 2.0); - private static readonly StyledProperty VirtualizationInfoProperty = - AvaloniaProperty.RegisterAttached("VirtualizationInfo"); + private static readonly StyledProperty VirtualizationInfoProperty = + AvaloniaProperty.RegisterAttached("VirtualizationInfo"); internal static readonly Rect InvalidRect = new Rect(-1, -1, -1, -1); internal static readonly Point ClearedElementsArrangePosition = new Point(-10000.0, -10000.0); @@ -62,7 +63,7 @@ namespace Avalonia.Controls private readonly ViewportManager _viewportManager; private readonly TargetWeakEventSubscriber _layoutWeakSubscriber; private IEnumerable? _items; - private VirtualizingLayoutContext? _layoutContext; + private RepeaterLayoutContext? _layoutContext; private EventHandler? _childIndexChanged; private bool _isLayoutInProgress; private NotifyCollectionChangedEventArgs? _processingItemsSourceChange; @@ -103,7 +104,7 @@ namespace Avalonia.Controls /// The layout used to size and position elements. The default is a StackLayout with /// vertical orientation. /// - public AttachedLayout Layout + public AttachedLayout? Layout { get => GetValue(LayoutProperty); set => SetValue(LayoutProperty, value); @@ -121,6 +122,7 @@ namespace Avalonia.Controls /// /// Gets or sets the template used to display each item. /// + [InheritDataTypeFromItems(nameof(Items))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); @@ -162,18 +164,7 @@ namespace Avalonia.Controls private bool IsProcessingCollectionChange => _processingItemsSourceChange != null; - private LayoutContext LayoutContext - { - get - { - if (_layoutContext == null) - { - _layoutContext = new RepeaterLayoutContext(this); - } - - return _layoutContext; - } - } + private RepeaterLayoutContext LayoutContext => _layoutContext ??= new RepeaterLayoutContext(this); event EventHandler? IChildIndexProvider.ChildIndexChanged { @@ -267,39 +258,22 @@ namespace Avalonia.Controls internal void UnpinElement(Control element) => _viewManager.UpdatePin(element, false); - internal static VirtualizationInfo? TryGetVirtualizationInfo(Control element) + internal static VirtualizationInfo? TryGetVirtualizationInfo(Control? element) { - return (element as AvaloniaObject)?.GetValue(VirtualizationInfoProperty); - } - - internal static VirtualizationInfo CreateAndInitializeVirtualizationInfo(Control element) - { - if (TryGetVirtualizationInfo(element) != null) - { - throw new InvalidOperationException("VirtualizationInfo already created."); - } - - var result = new VirtualizationInfo(); - element.SetValue(VirtualizationInfoProperty, result); - return result; + return element?.GetValue(VirtualizationInfoProperty); } internal static VirtualizationInfo GetVirtualizationInfo(Control element) { - if (element is AvaloniaObject ao) - { - var result = ao.GetValue(VirtualizationInfoProperty); - - if (result == null) - { - result = new VirtualizationInfo(); - ao.SetValue(VirtualizationInfoProperty, result); - } + var result = element.GetValue(VirtualizationInfoProperty); - return result; + if (result == null) + { + result = new VirtualizationInfo(); + element.SetValue(VirtualizationInfoProperty, result); } - throw new NotSupportedException("Custom implementations of AvaloniaObject not supported."); + return result; } private protected override void InvalidateMeasureOnChildrenChanged() @@ -307,6 +281,7 @@ namespace Avalonia.Controls // Don't invalidate measure when children change. } + /// protected override Size MeasureOverride(Size availableSize) { if (_isLayoutInProgress) @@ -332,7 +307,7 @@ namespace Avalonia.Controls if (layout != null) { - var layoutContext = GetLayoutContext(); + var layoutContext = LayoutContext; desiredSize = layout.Measure(layoutContext, availableSize); extent = new Rect(LayoutOrigin.X, LayoutOrigin.Y, desiredSize.Width, desiredSize.Height); @@ -362,6 +337,7 @@ namespace Avalonia.Controls } } + /// protected override Size ArrangeOverride(Size finalSize) { if (_isLayoutInProgress) @@ -378,7 +354,7 @@ namespace Avalonia.Controls try { - var arrangeSize = Layout?.Arrange(GetLayoutContext(), finalSize) ?? default; + var arrangeSize = Layout?.Arrange(LayoutContext, finalSize) ?? default; // The view manager might clear elements during this call. // That's why we call it before arranging cleared elements @@ -419,6 +395,7 @@ namespace Avalonia.Controls } } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); @@ -426,11 +403,13 @@ namespace Avalonia.Controls _viewportManager.ResetScrollers(); } + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _viewportManager.ResetScrollers(); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ItemsProperty) @@ -499,7 +478,7 @@ namespace Avalonia.Controls if (parent == this) { var virtInfo = TryGetVirtualizationInfo(element); - return _viewManager.GetElementIndex(virtInfo!); + return _viewManager.GetElementIndex(virtInfo); } return -1; @@ -527,7 +506,7 @@ namespace Avalonia.Controls { if (index >= 0 && index >= (ItemsSourceView?.Count ?? 0)) { - throw new ArgumentException("Argument index is invalid.", "index"); + throw new ArgumentException("Argument index is invalid.", nameof(index)); } if (_isLayoutInProgress) @@ -545,7 +524,7 @@ namespace Avalonia.Controls throw new InvalidOperationException("Cannot make an Anchor when there is no attached layout."); } - element = (Control)GetLayoutContext().GetOrCreateElementAt(index); + element = (Control)LayoutContext.GetOrCreateElementAt(index); element.Measure(Size.Infinity); } @@ -646,9 +625,9 @@ namespace Avalonia.Controls if (Layout is VirtualizingLayout virtualLayout) { - virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(LayoutContext, newValue, args); } - else if (Layout is NonVirtualizingLayout nonVirtualLayout) + else if (Layout is NonVirtualizingLayout) { // Walk through all the elements and make sure they are cleared for // non-virtualizing layouts. @@ -692,7 +671,7 @@ namespace Avalonia.Controls try { - virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + virtualLayout.OnItemsChanged(LayoutContext, newValue, args); } finally { @@ -759,7 +738,7 @@ namespace Avalonia.Controls AttachedLayout.ArrangeInvalidatedWeakEvent.Subscribe(newValue, _layoutWeakSubscriber); } - bool isVirtualizingLayout = newValue != null && newValue is VirtualizingLayout; + bool isVirtualizingLayout = newValue is VirtualizingLayout; _viewportManager.OnLayoutChanged(isVirtualizingLayout); InvalidateMeasure(); } @@ -787,7 +766,7 @@ namespace Avalonia.Controls { if (Layout is VirtualizingLayout virtualLayout) { - virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args); + virtualLayout.OnItemsChanged(LayoutContext, sender, args); } else { @@ -806,15 +785,5 @@ namespace Avalonia.Controls { _viewportManager.OnBringIntoViewRequested(e); } - - private VirtualizingLayoutContext GetLayoutContext() - { - if (_layoutContext == null) - { - _layoutContext = new RepeaterLayoutContext(this); - } - - return _layoutContext; - } } } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementClearingEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementClearingEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementIndexChangedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/ItemsRepeaterElementPreparedEventArgs.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeaterElementPreparedEventArgs.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclePool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclePool.cs diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RecyclingElementFactory.cs diff --git a/src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/RepeaterLayoutContext.cs diff --git a/src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/UniqueIdElementPool.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/UniqueIdElementPool.cs diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs similarity index 97% rename from src/Avalonia.Controls/Repeater/ViewManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs index 2dff18cd04..6b9d7934bf 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls } } } - if (element == null) { element = GetElementFromUniqueIdResetPool(index); }; + if (element == null) { element = GetElementFromUniqueIdResetPool(index); } if (element == null) { element = GetElementFromPinnedElements(index); } if (element == null) { element = GetElementFromElementFactory(index); } @@ -221,7 +221,7 @@ namespace Avalonia.Controls return nextElement; } - public int GetElementIndex(VirtualizationInfo virtInfo) + public int GetElementIndex(VirtualizationInfo? virtInfo) { if (virtInfo == null) { @@ -256,7 +256,7 @@ namespace Avalonia.Controls public void UpdatePin(Control element, bool addPin) { - var parent = element.VisualParent; + var parent = element.GetVisualParent(); var child = (Visual)element; while (parent != null) @@ -283,7 +283,7 @@ namespace Avalonia.Controls } child = parent; - parent = child.VisualParent; + parent = child.GetVisualParent(); } } @@ -627,11 +627,7 @@ namespace Avalonia.Controls var element = GetElement(); - var virtInfo = ItemsRepeater.TryGetVirtualizationInfo(element); - if (virtInfo == null) - { - virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); - } + var virtInfo = ItemsRepeater.GetVirtualizationInfo(element); // Clear flag virtInfo.MustClearDataContext = false; @@ -656,7 +652,7 @@ namespace Avalonia.Controls // that handlers can walk up the tree in case they want to find their IndexPath in the // nested case. var children = repeater.Children; - if (element.VisualParent != repeater) + if (element.GetVisualParent() != repeater) { children.Add(element); } @@ -701,7 +697,7 @@ namespace Avalonia.Controls if (FocusManager.Instance?.Current is Visual child) { - var parent = child.VisualParent; + var parent = child.GetVisualParent(); var owner = _owner; // Find out if the focused element belongs to one of our direct @@ -710,9 +706,8 @@ namespace Avalonia.Controls { if (parent is ItemsRepeater repeater) { - var element = child as Control; if (repeater == owner && - element is not null && + child is Control element && ItemsRepeater.GetVirtualizationInfo(element).IsRealized) { focusedElement = element; @@ -722,7 +717,7 @@ namespace Avalonia.Controls } child = parent; - parent = child?.VisualParent; + parent = child.GetVisualParent(); } } diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs similarity index 97% rename from src/Avalonia.Controls/Repeater/ViewportManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs index 56e0cda8fe..6ed817c238 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls // be a direct child of ours, or even an indirect child. We need to walk up the tree starting // from anchorElement to figure out what child of ours (if any) to use as the suggested element. var child = anchorElement; - var parent = child.VisualParent as Control; + var parent = child.GetVisualParent() as Control; while (parent != null) { @@ -78,7 +78,7 @@ namespace Avalonia.Controls } child = parent; - parent = parent.VisualParent as Control; + parent = parent.GetVisualParent() as Control; } } } @@ -166,7 +166,7 @@ namespace Avalonia.Controls if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})", - _owner.Layout.LayoutId, _expectedViewportShift); + _owner.Layout?.LayoutId, _expectedViewportShift); // There are cases where we might be expecting a shift but not get it. We will // be waiting for the effective viewport event but if the scroll viewer is not able @@ -287,7 +287,7 @@ namespace Avalonia.Controls if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure", - _owner.Layout.LayoutId, + _owner.Layout?.LayoutId, _pendingViewportShift); // Assume this is never going to come. @@ -369,11 +369,11 @@ namespace Avalonia.Controls private Control? GetImmediateChildOfRepeater(Control descendant) { var targetChild = descendant; - var parent = (Control?)descendant.VisualParent; + var parent = (Control?)descendant.GetVisualParent(); while (parent != null && parent != _owner) { targetChild = parent; - parent = (Control?)parent.VisualParent; + parent = (Control?)parent.GetVisualParent(); } if (parent == null) @@ -436,7 +436,7 @@ namespace Avalonia.Controls private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e) { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout?.LayoutId); UpdateViewport(e.EffectiveViewport); _pendingViewportShift = default; @@ -471,7 +471,7 @@ namespace Avalonia.Controls break; } - parent = parent.VisualParent; + parent = parent.GetVisualParent(); } if (!_managingViewportDisabled) @@ -490,14 +490,14 @@ namespace Avalonia.Controls var previousVisibleWindow = _visibleWindow; Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Effective Viewport: ({Before})->({After})", - _owner.Layout.LayoutId, + _owner.Layout?.LayoutId, previousVisibleWindow, viewport); if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) { - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout.LayoutId); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout?.LayoutId); // We got cleared. _visibleWindow = default; } @@ -509,7 +509,7 @@ namespace Avalonia.Controls if (_visibleWindow != previousVisibleWindow) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Used Viewport: ({Before})->({After})", - _owner.Layout.LayoutId, + _owner.Layout?.LayoutId, previousVisibleWindow, currentVisibleWindow); TryInvalidateMeasure(); @@ -532,7 +532,7 @@ namespace Avalonia.Controls // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to // avoid layout cycles. - Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout.LayoutId); + Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout?.LayoutId); _owner.InvalidateMeasure(); } } diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs similarity index 100% rename from src/Avalonia.Controls/Repeater/VirtualizationInfo.cs rename to src/Avalonia.Controls.ItemsRepeater/Controls/VirtualizationInfo.cs diff --git a/src/Avalonia.Base/Layout/AttachedLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/AttachedLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/AttachedLayout.cs diff --git a/src/Avalonia.Base/Layout/ElementManager.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs similarity index 100% rename from src/Avalonia.Base/Layout/ElementManager.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/ElementManager.cs diff --git a/src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs similarity index 100% rename from src/Avalonia.Base/Layout/FlowLayoutAlgorithm.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/FlowLayoutAlgorithm.cs diff --git a/src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs similarity index 100% rename from src/Avalonia.Base/Layout/IFlowLayoutAlgorithmDelegates.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/IFlowLayoutAlgorithmDelegates.cs diff --git a/src/Avalonia.Base/Layout/LayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContext.cs diff --git a/src/Avalonia.Base/Layout/LayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/LayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/LayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/NonVirtualizingStackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/NonVirtualizingStackLayout.cs diff --git a/src/Avalonia.Base/Layout/OrientationBasedMeasures.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs similarity index 100% rename from src/Avalonia.Base/Layout/OrientationBasedMeasures.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/OrientationBasedMeasures.cs diff --git a/src/Avalonia.Base/Layout/StackLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs similarity index 98% rename from src/Avalonia.Base/Layout/StackLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs index e9093cc146..5e2b2b8574 100644 --- a/src/Avalonia.Base/Layout/StackLayout.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Logging; @@ -25,13 +26,13 @@ namespace Avalonia.Layout /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); + StackPanel.OrientationProperty.AddOwner(); /// /// Defines the property. /// public static readonly StyledProperty SpacingProperty = - AvaloniaProperty.Register(nameof(Spacing)); + StackPanel.SpacingProperty.AddOwner(); private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures(); diff --git a/src/Avalonia.Base/Layout/StackLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/StackLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/StackLayoutState.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayout.cs diff --git a/src/Avalonia.Base/Layout/UniformGridLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/UniformGridLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UniformGridLayoutState.cs diff --git a/src/Avalonia.Base/Layout/Utils/ListUtils.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs similarity index 100% rename from src/Avalonia.Base/Layout/Utils/ListUtils.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/Utils/ListUtils.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvBounds.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvBounds.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/UvMeasure.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/UvMeasure.cs diff --git a/src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualLayoutContextAdapter.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualLayoutContextAdapter.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayout.cs diff --git a/src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs similarity index 100% rename from src/Avalonia.Base/Layout/VirtualizingLayoutContext.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/VirtualizingLayoutContext.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapItem.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapItem.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayout.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayout.cs diff --git a/src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs b/src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs similarity index 100% rename from src/Avalonia.Base/Layout/WrapLayout/WrapLayoutState.cs rename to src/Avalonia.Controls.ItemsRepeater/Layout/WrapLayoutState.cs diff --git a/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d8023b0853 --- /dev/null +++ b/src/Avalonia.Controls.ItemsRepeater/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 5b652cce19..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -4,6 +4,7 @@ using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Input.Raw; @@ -28,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -49,10 +50,22 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); + /// public event EventHandler? ResourcesChanged; - public event EventHandler? UrlsOpened; + /// + public event EventHandler? UrlsOpened; + + /// + public event EventHandler? ActualThemeVariantChanged; /// /// Creates an instance of the class. @@ -75,6 +88,16 @@ namespace Avalonia set { SetValue(DataContextProperty, value); } } + /// + public ThemeVariant? RequestedThemeVariant + { + get => GetValue(RequestedThemeVariantProperty); + set => SetValue(RequestedThemeVariantProperty, value); + } + + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); + /// /// Gets the current instance of the class. /// @@ -191,11 +214,11 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceNode.TryGetResource(object key, out object? value) + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) { value = null; - return (_resources?.TryGetResource(key, out value) ?? false) || - Styles.TryGetResource(key, out value); + return (_resources?.TryGetResource(key, theme, out value) ?? false) || + Styles.TryGetResource(key, theme, out value); } void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) @@ -222,10 +245,15 @@ namespace Avalonia FocusManager = new FocusManager(); InputManager = new InputManager(); + var settings = AvaloniaLocator.Current.GetRequiredService(); + settings.ColorValuesChanged += OnColorValuesChanged; + OnColorValuesChanged(settings, settings.GetColorValues()); + AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() @@ -290,5 +318,26 @@ namespace Avalonia set => SetAndRaise(NameProperty, ref _name, value); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ActualThemeVariantProperty, themeVariant); + else + ClearValue(ActualThemeVariantProperty); + } + else if (change.Property == ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } + } + + private void OnColorValuesChanged(object? sender, PlatformColorValues e) + { + SetValue(ActualThemeVariantProperty, (ThemeVariant)e.ThemeVariant, BindingPriority.Template); + } } } diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index fde401fb01..ada0b94124 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -17,32 +16,34 @@ namespace Avalonia.Controls.ApplicationLifetimes private int _exitCode; private CancellationTokenSource? _cts; private bool _isShuttingDown; - private HashSet _windows = new HashSet(); + private readonly HashSet _windows = new(); + + private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime; - private static ClassicDesktopStyleApplicationLifetime? _activeLifetime; static ClassicDesktopStyleApplicationLifetime() { Window.WindowOpenedEvent.AddClassHandler(typeof(Window), OnWindowOpened); - Window.WindowClosedEvent.AddClassHandler(typeof(Window), WindowClosedEvent); + Window.WindowClosedEvent.AddClassHandler(typeof(Window), OnWindowClosed); } - private static void WindowClosedEvent(object? sender, RoutedEventArgs e) + private static void OnWindowClosed(object? sender, RoutedEventArgs e) { - _activeLifetime?._windows.Remove((Window)sender!); - _activeLifetime?.HandleWindowClosed((Window)sender!); + var window = (Window)sender!; + s_activeLifetime?._windows.Remove(window); + s_activeLifetime?.HandleWindowClosed(window); } private static void OnWindowOpened(object? sender, RoutedEventArgs e) { - _activeLifetime?._windows.Add((Window)sender!); + s_activeLifetime?._windows.Add((Window)sender!); } public ClassicDesktopStyleApplicationLifetime() { - if (_activeLifetime != null) + if (s_activeLifetime != null) throw new InvalidOperationException( "Can not have multiple active ClassicDesktopStyleApplicationLifetime instances and the previously created one was not disposed"); - _activeLifetime = this; + s_activeLifetime = this; } /// @@ -65,9 +66,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// public Window? MainWindow { get; set; } + /// public IReadOnlyList Windows => _windows.ToArray(); - private void HandleWindowClosed(Window window) + private void HandleWindowClosed(Window? window) { if (window == null) return; @@ -130,8 +132,8 @@ namespace Avalonia.Controls.ApplicationLifetimes public void Dispose() { - if (_activeLifetime == this) - _activeLifetime = null; + if (s_activeLifetime == this) + s_activeLifetime = null; } private bool DoShutdown( diff --git a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs index 22b5f8236d..b9a372f935 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs @@ -40,7 +40,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// The main window. /// Window? MainWindow { get; set; } - + + /// + /// Gets the list of all open windows in the application. + /// IReadOnlyList Windows { get; } /// diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 98885e11ca..9a949e31d4 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -792,7 +792,7 @@ namespace Avalonia.Controls Control? element = focused as Control; if (element != null) { - parent = element.Parent; + parent = element.VisualParent; } } focused = parent; @@ -1711,7 +1711,7 @@ namespace Avalonia.Controls /// The predicate to use for the partial or /// exact match. /// Returns the object or null. - private object? TryGetMatch(string? searchText, AvaloniaList view, AutoCompleteFilterPredicate? predicate) + private object? TryGetMatch(string? searchText, AvaloniaList? view, AutoCompleteFilterPredicate? predicate) { if (predicate is null) return null; diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs index 35f94722ce..3ea9c170ff 100644 --- a/src/Avalonia.Controls/Automation/AutomationProperties.cs +++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs @@ -38,8 +38,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.AcceleratorKey attached property. /// - public static readonly AttachedProperty AcceleratorKeyProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AcceleratorKeyProperty = + AvaloniaProperty.RegisterAttached( "AcceleratorKey", typeof(AutomationProperties)); @@ -54,16 +54,16 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.AccessKey attached property /// - public static readonly AttachedProperty AccessKeyProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AccessKeyProperty = + AvaloniaProperty.RegisterAttached( "AccessKey", typeof(AutomationProperties)); /// /// Defines the AutomationProperties.AutomationId attached property. /// - public static readonly AttachedProperty AutomationIdProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty AutomationIdProperty = + AvaloniaProperty.RegisterAttached( "AutomationId", typeof(AutomationProperties)); @@ -78,8 +78,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.HelpText attached property. /// - public static readonly AttachedProperty HelpTextProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty HelpTextProperty = + AvaloniaProperty.RegisterAttached( "HelpText", typeof(AutomationProperties)); @@ -122,16 +122,16 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.ItemStatus attached property. /// - public static readonly AttachedProperty ItemStatusProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty ItemStatusProperty = + AvaloniaProperty.RegisterAttached( "ItemStatus", typeof(AutomationProperties)); /// /// Defines the AutomationProperties.ItemType attached property. /// - public static readonly AttachedProperty ItemTypeProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty ItemTypeProperty = + AvaloniaProperty.RegisterAttached( "ItemType", typeof(AutomationProperties)); @@ -155,8 +155,8 @@ namespace Avalonia.Automation /// /// Defines the AutomationProperties.Name attached attached property. /// - public static readonly AttachedProperty NameProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty NameProperty = + AvaloniaProperty.RegisterAttached( "Name", typeof(AutomationProperties)); @@ -193,25 +193,17 @@ namespace Avalonia.Automation /// public static void SetAcceleratorKey(StyledElement element, string value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AcceleratorKeyProperty, value); } /// /// Helper for reading AcceleratorKey property from a StyledElement. /// - public static string GetAcceleratorKey(StyledElement element) + public static string? GetAcceleratorKey(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(AcceleratorKeyProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(AcceleratorKeyProperty); } /// @@ -219,11 +211,7 @@ namespace Avalonia.Automation /// public static void SetAccessibilityView(StyledElement element, AccessibilityView value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AccessibilityViewProperty, value); } @@ -232,11 +220,7 @@ namespace Avalonia.Automation /// public static AccessibilityView GetAccessibilityView(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(AccessibilityViewProperty); } @@ -245,50 +229,34 @@ namespace Avalonia.Automation /// public static void SetAccessKey(StyledElement element, string value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AccessKeyProperty, value); } /// /// Helper for reading AccessKey property from a StyledElement. /// - public static string GetAccessKey(StyledElement element) + public static string? GetAccessKey(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(AccessKeyProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(AccessKeyProperty); } /// /// Helper for setting AutomationId property on a StyledElement. /// - public static void SetAutomationId(StyledElement element, string value) + public static void SetAutomationId(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(AutomationIdProperty, value); } /// /// Helper for reading AutomationId property from a StyledElement. /// - public static string GetAutomationId(StyledElement element) + public static string? GetAutomationId(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(AutomationIdProperty); } @@ -297,11 +265,7 @@ namespace Avalonia.Automation /// public static void SetControlTypeOverride(StyledElement element, AutomationControlType? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ControlTypeOverrideProperty, value); } @@ -310,38 +274,26 @@ namespace Avalonia.Automation /// public static AutomationControlType? GetControlTypeOverride(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(ControlTypeOverrideProperty); } /// /// Helper for setting HelpText property on a StyledElement. /// - public static void SetHelpText(StyledElement element, string value) + public static void SetHelpText(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(HelpTextProperty, value); } /// /// Helper for reading HelpText property from a StyledElement. /// - public static string GetHelpText(StyledElement element) + public static string? GetHelpText(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(HelpTextProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(HelpTextProperty); } /// @@ -349,11 +301,7 @@ namespace Avalonia.Automation /// public static void SetIsColumnHeader(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsColumnHeaderProperty, value); } @@ -362,12 +310,8 @@ namespace Avalonia.Automation /// public static bool GetIsColumnHeader(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsColumnHeaderProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsColumnHeaderProperty); } /// @@ -375,11 +319,7 @@ namespace Avalonia.Automation /// public static void SetIsRequiredForForm(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsRequiredForFormProperty, value); } @@ -388,12 +328,8 @@ namespace Avalonia.Automation /// public static bool GetIsRequiredForForm(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsRequiredForFormProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsRequiredForFormProperty); } /// @@ -401,12 +337,8 @@ namespace Avalonia.Automation /// public static bool GetIsRowHeader(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((bool)element.GetValue(IsRowHeaderProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsRowHeaderProperty); } /// @@ -414,11 +346,7 @@ namespace Avalonia.Automation /// public static void SetIsRowHeader(StyledElement element, bool value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsRowHeaderProperty, value); } @@ -427,11 +355,7 @@ namespace Avalonia.Automation /// public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(IsOffscreenBehaviorProperty, value); } @@ -440,64 +364,44 @@ namespace Avalonia.Automation /// public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(IsOffscreenBehaviorProperty); } /// /// Helper for setting ItemStatus property on a StyledElement. /// - public static void SetItemStatus(StyledElement element, string value) + public static void SetItemStatus(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ItemStatusProperty, value); } /// /// Helper for reading ItemStatus property from a StyledElement. /// - public static string GetItemStatus(StyledElement element) + public static string? GetItemStatus(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(ItemStatusProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ItemStatusProperty); } /// /// Helper for setting ItemType property on a StyledElement. /// - public static void SetItemType(StyledElement element, string value) + public static void SetItemType(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(ItemTypeProperty, value); } /// /// Helper for reading ItemType property from a StyledElement. /// - public static string GetItemType(StyledElement element) + public static string? GetItemType(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(ItemTypeProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ItemTypeProperty); } /// @@ -505,11 +409,7 @@ namespace Avalonia.Automation /// public static void SetLabeledBy(StyledElement element, Control value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(LabeledByProperty, value); } @@ -518,11 +418,7 @@ namespace Avalonia.Automation /// public static Control GetLabeledBy(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(LabeledByProperty); } @@ -531,11 +427,7 @@ namespace Avalonia.Automation /// public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(LiveSettingProperty, value); } @@ -544,38 +436,26 @@ namespace Avalonia.Automation /// public static AutomationLiveSetting GetLiveSetting(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(LiveSettingProperty); } /// /// Helper for setting Name property on a StyledElement. /// - public static void SetName(StyledElement element, string value) + public static void SetName(StyledElement element, string? value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(NameProperty, value); } /// /// Helper for reading Name property from a StyledElement. /// - public static string GetName(StyledElement element) + public static string? GetName(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((string)element.GetValue(NameProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(NameProperty); } /// @@ -583,11 +463,7 @@ namespace Avalonia.Automation /// public static void SetPositionInSet(StyledElement element, int value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(PositionInSetProperty, value); } @@ -596,12 +472,8 @@ namespace Avalonia.Automation /// public static int GetPositionInSet(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((int)element.GetValue(PositionInSetProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(PositionInSetProperty); } /// @@ -609,11 +481,7 @@ namespace Avalonia.Automation /// public static void SetSizeOfSet(StyledElement element, int value) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - + _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(SizeOfSetProperty, value); } @@ -622,12 +490,8 @@ namespace Avalonia.Automation /// public static int GetSizeOfSet(StyledElement element) { - if (element == null) - { - throw new ArgumentNullException(nameof(element)); - } - - return ((int)element.GetValue(SizeOfSetProperty)); + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(SizeOfSetProperty); } } } diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index ac23873e6a..aea91b5e26 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -36,7 +35,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is SelectingItemsControl parent) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) parent.SelectedIndex = index; @@ -50,7 +49,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Select(index); @@ -64,7 +63,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Deselect(index); diff --git a/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs new file mode 100644 index 0000000000..3c59f74c90 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ProgressBarAutomationPeer.cs @@ -0,0 +1,62 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ProgressBarAutomationPeer : RangeBaseAutomationPeer, IRangeValueProvider + { + public ProgressBarAutomationPeer(RangeBase owner) : base(owner) + { + } + + protected override string GetClassNameCore() + { + return "ProgressBar"; + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.ProgressBar; + } + + /// + /// Request to set the value that this UI element is representing + /// + /// Value to set the UI to, as an object + /// true if the UI element was successfully set to the specified value + void IRangeValueProvider.SetValue(double val) + { + throw new InvalidOperationException("ProgressBar is ReadOnly, value can't be set."); + } + + ///Indicates that the value can only be read, not modified. + ///returns True if the control is read-only + bool IRangeValueProvider.IsReadOnly + { + get + { + return true; + } + } + + ///Value of a Large Change + double IRangeValueProvider.LargeChange + { + get + { + return double.NaN; + } + } + + ///Value of a Small Change + double IRangeValueProvider.SmallChange + { + get + { + return double.NaN; + } + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs index 7ae0ba7244..11480fcb34 100644 --- a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -35,7 +35,7 @@ namespace Avalonia.Automation.Peers foreach (var i in selection.SelectedIndexes) { - var container = owner.ItemContainerGenerator.ContainerFromIndex(i); + var container = owner.ContainerFromIndex(i); if (container is Control c && c.IsAttachedToVisualTree) { diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 42c577041a..3195c38eef 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 1bb574acd2..78ba23c1dd 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -225,7 +225,7 @@ namespace Avalonia.Controls /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderHelper.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow, BorderDashOffset, BorderLineCap, BorderLineJoin, BorderDashArray); diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 9c88bae5f6..3300292857 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -237,11 +237,11 @@ namespace Avalonia.Controls private DateTime _selectedYear; private DateTime _displayDate = DateTime.Today; - private DateTime? _displayDateStart = null; - private DateTime? _displayDateEnd = null; + private DateTime? _displayDateStart; + private DateTime? _displayDateEnd; private bool _isShiftPressed; - private bool _displayDateIsChanging = false; + private bool _displayDateIsChanging; internal CalendarDayButton? FocusButton { get; set; } internal CalendarButton? FocusCalendarButton { get; set; } @@ -291,7 +291,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "Invalid DayOfWeek"); + throw new ArgumentOutOfRangeException(nameof(e), "Invalid DayOfWeek"); } } @@ -346,10 +346,10 @@ namespace Avalonia.Controls } } - public static readonly StyledProperty HeaderBackgroundProperty = - AvaloniaProperty.Register(nameof(HeaderBackground)); + public static readonly StyledProperty HeaderBackgroundProperty = + AvaloniaProperty.Register(nameof(HeaderBackground)); - public IBrush HeaderBackground + public IBrush? HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } set { SetValue(HeaderBackgroundProperty, value); } @@ -478,7 +478,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "Invalid SelectionMode"); + throw new ArgumentOutOfRangeException(nameof(e), "Invalid SelectionMode"); } } @@ -574,7 +574,7 @@ namespace Avalonia.Controls } else { - throw new ArgumentOutOfRangeException("d", "SelectedDate value is not valid."); + throw new ArgumentOutOfRangeException(nameof(e), "SelectedDate value is not valid."); } } else diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index fe8b616e02..8fb9b66f3d 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Primitives /// /// The Calendar whose dates this object represents. /// - private Calendar _owner; + private readonly Calendar _owner; /// /// Initializes a new instance of the @@ -79,13 +79,13 @@ namespace Avalonia.Controls.Primitives if (DateTime.Compare(end, start) > -1) { - rangeStart = DateTimeHelper.DiscardTime(start).Value; - rangeEnd = DateTimeHelper.DiscardTime(end).Value; + rangeStart = DateTimeHelper.DiscardTime(start); + rangeEnd = DateTimeHelper.DiscardTime(end); } else { - rangeStart = DateTimeHelper.DiscardTime(end).Value; - rangeEnd = DateTimeHelper.DiscardTime(start).Value; + rangeStart = DateTimeHelper.DiscardTime(end); + rangeEnd = DateTimeHelper.DiscardTime(start); } int count = Count; @@ -144,7 +144,7 @@ namespace Avalonia.Controls.Primitives if (!IsValid(item)) { - throw new ArgumentOutOfRangeException("Value is not valid."); + throw new ArgumentOutOfRangeException(nameof(item), "Value is not valid."); } base.InsertItem(index, item); @@ -186,7 +186,7 @@ namespace Avalonia.Controls.Primitives if (!IsValid(item)) { - throw new ArgumentOutOfRangeException("Value is not valid."); + throw new ArgumentOutOfRangeException(nameof(item), "Value is not valid."); } base.SetItem(index, item); diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 032f452111..3d436b4485 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -44,30 +44,30 @@ namespace Avalonia.Controls.Primitives private ITemplate? _dayTitleTemplate; private DateTime _currentMonth; - private bool _isMouseLeftButtonDown = false; - private bool _isMouseLeftButtonDownYearView = false; - private bool _isControlPressed = false; + private bool _isMouseLeftButtonDown; + private bool _isMouseLeftButtonDownYearView; + private bool _isControlPressed; - private System.Globalization.Calendar _calendar = new System.Globalization.GregorianCalendar(); - - private PointerPressedEventArgs? _downEventArg; - private PointerPressedEventArgs? _downEventArgYearView; + private readonly System.Globalization.Calendar _calendar = new GregorianCalendar(); internal Calendar? Owner { get; set; } internal CalendarDayButton? CurrentButton { get; set; } - public static readonly StyledProperty HeaderBackgroundProperty = Calendar.HeaderBackgroundProperty.AddOwner(); - public IBrush HeaderBackground + public static readonly StyledProperty HeaderBackgroundProperty = Calendar.HeaderBackgroundProperty.AddOwner(); + + public IBrush? HeaderBackground { get { return GetValue(HeaderBackgroundProperty); } set { SetValue(HeaderBackgroundProperty, value); } } + public static readonly DirectProperty?> DayTitleTemplateProperty = AvaloniaProperty.RegisterDirect?>( nameof(DayTitleTemplate), o => o.DayTitleTemplate, (o,v) => o.DayTitleTemplate = v, defaultBindingMode: BindingMode.OneTime); + public ITemplate? DayTitleTemplate { get { return _dayTitleTemplate; } @@ -178,7 +178,7 @@ namespace Avalonia.Controls.Primitives { if (_dayTitleTemplate != null) { - var cell = (Control) _dayTitleTemplate.Build(); + var cell = _dayTitleTemplate.Build(); cell.DataContext = string.Empty; cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); @@ -308,16 +308,13 @@ namespace Avalonia.Controls.Primitives for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++) { var daytitle = MonthView!.Children[childIndex]; - if (daytitle != null) + if (Owner != null) { - if (Owner != null) - { - daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)Owner.FirstDayOfWeek) % NumberOfDaysPerWeek]; - } - else - { - daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek) % NumberOfDaysPerWeek]; - } + daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)Owner.FirstDayOfWeek) % NumberOfDaysPerWeek]; + } + else + { + daytitle.DataContext = DateTimeHelper.GetCurrentDateFormat().ShortestDayNames[(childIndex + (int)DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek) % NumberOfDaysPerWeek]; } } } @@ -527,7 +524,7 @@ namespace Avalonia.Controls.Primitives childButton.Content = dateToAdd.Day.ToString(DateTimeHelper.GetCurrentDateFormat()); childButton.DataContext = dateToAdd; - if (DateTime.Compare((DateTime)DateTimeHelper.DiscardTime(DateTime.MaxValue), dateToAdd) > 0) + if (DateTime.Compare(DateTimeHelper.DiscardTime(DateTime.MaxValue), dateToAdd) > 0) { // Since we are sure DisplayDate is not equal to // DateTime.MaxValue, it is safe to use AddDays @@ -587,7 +584,7 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - _currentMonth = (DateTime)Owner.SelectedMonth; + _currentMonth = Owner.SelectedMonth; } else { @@ -676,7 +673,7 @@ namespace Avalonia.Controls.Primitives if (Owner != null) { selectedYear = Owner.SelectedYear; - _currentMonth = (DateTime)Owner.SelectedMonth; + _currentMonth = Owner.SelectedMonth; } else { @@ -696,9 +693,9 @@ namespace Avalonia.Controls.Primitives SetYearButtons(decade, decadeEnd); } } - internal void UpdateYearViewSelection(CalendarButton calendarButton) + internal void UpdateYearViewSelection(CalendarButton? calendarButton) { - if (Owner != null && calendarButton != null && calendarButton.DataContext != null) + if (Owner != null && calendarButton?.DataContext is DateTime selectedDate) { Owner.FocusCalendarButton!.IsCalendarButtonFocused = false; Owner.FocusCalendarButton = calendarButton; @@ -706,11 +703,11 @@ namespace Avalonia.Controls.Primitives if (Owner.DisplayMode == CalendarMode.Year) { - Owner.SelectedMonth = (DateTime)calendarButton.DataContext; + Owner.SelectedMonth = selectedDate; } else { - Owner.SelectedYear = (DateTime)calendarButton.DataContext; + Owner.SelectedYear = selectedDate; } } } @@ -719,7 +716,7 @@ namespace Avalonia.Controls.Primitives { int year; int count = -1; - foreach (object child in YearView!.Children) + foreach (var child in YearView!.Children) { CalendarButton childButton = (CalendarButton)child; year = decade + count; @@ -859,7 +856,8 @@ namespace Avalonia.Controls.Primitives { if (Owner != null) { - if (_isMouseLeftButtonDown && sender is CalendarDayButton b && b.IsEnabled && !b.IsBlackout) + if (_isMouseLeftButtonDown + && sender is CalendarDayButton { IsEnabled: true, IsBlackout: false, DataContext: DateTime selectedDate } b) { // Update the states of all buttons to be selected starting // from HoverStart to b @@ -867,7 +865,6 @@ namespace Avalonia.Controls.Primitives { case CalendarSelectionMode.SingleDate: { - DateTime selectedDate = (DateTime)b.DataContext!; Owner.CalendarDatePickerDisplayDateFlag = true; if (Owner.SelectedDates.Count == 0) { @@ -882,10 +879,9 @@ namespace Avalonia.Controls.Primitives case CalendarSelectionMode.SingleRange: case CalendarSelectionMode.MultipleRange: { - Debug.Assert(b.DataContext != null, "The DataContext should not be null!"); Owner.UnHighlightDays(); Owner.HoverEndIndex = b.Index; - Owner.HoverEnd = (DateTime?)b.DataContext; + Owner.HoverEnd = selectedDate; // Update the States of the buttons Owner.HighlightDays(); return; @@ -904,22 +900,14 @@ namespace Avalonia.Controls.Primitives Owner.Focus(); } - bool ctrl, shift; - CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out ctrl, out shift); - CalendarDayButton b = (CalendarDayButton)sender!; + CalendarExtensions.GetMetaKeyState(e.KeyModifiers, out var ctrl, out var shift); - if (b != null) + if (sender is CalendarDayButton b) { _isControlPressed = ctrl; - if (b.IsEnabled && !b.IsBlackout) + if (b.IsEnabled && !b.IsBlackout && b.DataContext is DateTime selectedDate) { - DateTime selectedDate = (DateTime)b.DataContext!; _isMouseLeftButtonDown = true; - // null check is added for unit tests - if (e != null) - { - _downEventArg = e; - } switch (Owner.SelectionMode) { @@ -1010,12 +998,12 @@ namespace Avalonia.Controls.Primitives } } } - private void AddSelection(CalendarDayButton b) + private void AddSelection(CalendarDayButton b, DateTime selectedDate) { if (Owner != null) { Owner.HoverEndIndex = b.Index; - Owner.HoverEnd = (DateTime)b.DataContext!; + Owner.HoverEnd = selectedDate; if (Owner.HoverEnd != null && Owner.HoverStart != null) { @@ -1025,7 +1013,7 @@ namespace Avalonia.Controls.Primitives // SelectionMode Owner.IsMouseSelection = true; Owner.SelectedDates.AddRange(Owner.HoverStart.Value, Owner.HoverEnd.Value); - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); } } } @@ -1039,11 +1027,11 @@ namespace Avalonia.Controls.Primitives Owner.OnDayButtonMouseUp(e); } _isMouseLeftButtonDown = false; - if (b != null && b.DataContext != null) + if (b != null && b.DataContext is DateTime selectedDate) { if (Owner.SelectionMode == CalendarSelectionMode.None || Owner.SelectionMode == CalendarSelectionMode.SingleDate) { - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); return; } if (Owner.HoverStart.HasValue) @@ -1058,14 +1046,14 @@ namespace Avalonia.Controls.Primitives Owner.RemovedItems.Add(item); } Owner.SelectedDates.ClearInternal(); - AddSelection(b); + AddSelection(b, selectedDate); return; } case CalendarSelectionMode.MultipleRange: { // add the selection (either single day or // SingleRange day) - AddSelection(b); + AddSelection(b, selectedDate); return; } } @@ -1076,7 +1064,7 @@ namespace Avalonia.Controls.Primitives // be able to switch months if (b.IsInactive && b.IsBlackout) { - Owner.OnDayClick((DateTime)b.DataContext); + Owner.OnDayClick(selectedDate); } } } @@ -1095,9 +1083,9 @@ namespace Avalonia.Controls.Primitives Owner.HoverStart = null; _isMouseLeftButtonDown = false; b.IsSelected = false; - if (b.DataContext != null) + if (b.DataContext is DateTime selectedDate) { - Owner.SelectedDates.Remove((DateTime)b.DataContext); + Owner.SelectedDates.Remove(selectedDate); } } } @@ -1107,35 +1095,26 @@ namespace Avalonia.Controls.Primitives private void Month_CalendarButtonMouseDown(object? sender, PointerPressedEventArgs e) { - CalendarButton b = (CalendarButton)sender!; - _isMouseLeftButtonDownYearView = true; - if (e != null) - { - _downEventArgYearView = e; - } - - UpdateYearViewSelection(b); + UpdateYearViewSelection(sender as CalendarButton); } internal void Month_CalendarButtonMouseUp(object? sender, PointerReleasedEventArgs e) { _isMouseLeftButtonDownYearView = false; - if (Owner != null) + if (Owner != null && (sender as CalendarButton)?.DataContext is DateTime newMonth) { - DateTime newmonth = (DateTime)((CalendarButton)sender!).DataContext!; - if (Owner.DisplayMode == CalendarMode.Year) { - Owner.DisplayDate = newmonth; + Owner.DisplayDate = newMonth; Owner.DisplayMode = CalendarMode.Month; } else { Debug.Assert(Owner.DisplayMode == CalendarMode.Decade, "The owning Calendar should be in decade mode!"); - Owner.SelectedMonth = newmonth; + Owner.SelectedMonth = newMonth; Owner.DisplayMode = CalendarMode.Year; } } @@ -1145,8 +1124,7 @@ namespace Avalonia.Controls.Primitives { if (_isMouseLeftButtonDownYearView) { - CalendarButton b = (CalendarButton)sender!; - UpdateYearViewSelection(b); + UpdateYearViewSelection(sender as CalendarButton); } } diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index bfff03a926..570f05cfe8 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls public static int CompareDays(DateTime dt1, DateTime dt2) { - return DateTime.Compare(DiscardTime(dt1).Value, DiscardTime(dt2).Value); + return DateTime.Compare(DiscardTime(dt1), DiscardTime(dt2)); } public static int CompareYearMonth(DateTime dt1, DateTime dt2) @@ -71,14 +71,9 @@ namespace Avalonia.Controls return new DateTime(d.Year, d.Month, 1, 0, 0, 0); } - [return: NotNullIfNotNull("d")] - public static DateTime? DiscardTime(DateTime? d) + public static DateTime DiscardTime(DateTime d) { - if (d == null) - { - return null; - } - return d.Value.Date; + return d.Date; } public static int EndOfDecade(DateTime date) @@ -127,28 +122,14 @@ namespace Avalonia.Controls public static string ToYearMonthPatternString(DateTime date) { - string result = string.Empty; - DateTimeFormatInfo format = GetCurrentDateFormat(); - - if (format != null) - { - result = date.ToString(format.YearMonthPattern, format); - } - - return result; + var format = GetCurrentDateFormat(); + return date.ToString(format.YearMonthPattern, format); } public static string ToYearString(DateTime date) { - string result = string.Empty; - DateTimeFormatInfo format = GetCurrentDateFormat(); - - if (format != null) - { - result = date.Year.ToString(format); - } - - return result; + var format = GetCurrentDateFormat(); + return date.Year.ToString(format); } } } diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index b17648f5bb..869bdeabea 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -51,11 +51,11 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private DateTime? _selectedDate; private string? _text; - private bool _suspendTextChangeHandler = false; - private bool _isPopupClosing = false; - private bool _ignoreButtonClick = false; - private bool _isFlyoutOpen = false; - private bool _isPressed = false; + private bool _suspendTextChangeHandler; + private bool _isPopupClosing; + private bool _ignoreButtonClick; + private bool _isFlyoutOpen; + private bool _isPressed; /// /// Occurs when the drop-down @@ -185,7 +185,7 @@ namespace Avalonia.Controls { _textBox.KeyDown += TextBox_KeyDown; _textBox.GotFocus += TextBox_GotFocus; - _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBox_TextChanged()); + _textBoxTextChangedSubscription = _textBox.GetObservable(TextBox.TextProperty).Subscribe(_ => TextBox_TextChanged()); if(SelectedDate.HasValue) { @@ -292,7 +292,7 @@ namespace Avalonia.Controls // Text else if (change.Property == TextProperty) { - var (oldValue, newValue) = change.GetOldAndNewValue(); + var (_, newValue) = change.GetOldAndNewValue(); if (!_suspendTextChangeHandler) { @@ -595,9 +595,9 @@ namespace Avalonia.Controls private void Calendar_KeyDown(object? sender, KeyEventArgs e) { - Calendar? c = sender as Calendar ?? throw new ArgumentException("Sender must be Calendar.", nameof(sender)); - - if (!e.Handled && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape) && c.DisplayMode == CalendarMode.Month) + if (!e.Handled + && sender is Calendar { DisplayMode: CalendarMode.Month } + && (e.Key == Key.Enter || e.Key == Key.Space || e.Key == Key.Escape)) { Focus(); IsDropDownOpen = false; diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 47b0bb6e2d..368c9d4c2f 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -17,28 +17,26 @@ namespace Avalonia.Controls.Chrome private void UpdateSize(Window window) { - if (window != null) + Margin = new Thickness( + window.OffScreenMargin.Left, + window.OffScreenMargin.Top, + window.OffScreenMargin.Right, + window.OffScreenMargin.Bottom); + + if (window.WindowState != WindowState.FullScreen) { - Margin = new Thickness( - window.OffScreenMargin.Left, - window.OffScreenMargin.Top, - window.OffScreenMargin.Right, - window.OffScreenMargin.Bottom); + Height = window.WindowDecorationMargin.Top; - if (window.WindowState != WindowState.FullScreen) + if (_captionButtons != null) { - Height = window.WindowDecorationMargin.Top; - - if (_captionButtons != null) - { - _captionButtons.Height = Height; - } + _captionButtons.Height = Height; } - - IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } + + IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false; } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -55,6 +53,7 @@ namespace Avalonia.Controls.Chrome } } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); @@ -64,13 +63,13 @@ namespace Avalonia.Controls.Chrome _disposables = new CompositeDisposable(6) { window.GetObservable(Window.WindowDecorationMarginProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.OffScreenMarginProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.ExtendClientAreaChromeHintsProperty) - .Subscribe(x => UpdateSize(window)), + .Subscribe(_ => UpdateSize(window)), window.GetObservable(Window.WindowStateProperty) .Subscribe(x => { @@ -80,11 +79,12 @@ namespace Avalonia.Controls.Chrome PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); }), window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty) - .Subscribe(x => UpdateSize(window)) + .Subscribe(_ => UpdateSize(window)) }; } } + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index aeaf94d728..17a6ad7a09 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,19 +1,19 @@ using System; +using System.Diagnostics; using System.Linq; using Avalonia.Automation.Peers; -using Avalonia.Reactive; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.VisualTree; -using Avalonia.Controls.Metadata; namespace Avalonia.Controls { @@ -219,7 +219,7 @@ namespace Avalonia.Controls } else if (e.Key == Key.Up) { - SelectPrev(); + SelectPrevious(); e.Handled = true; } } @@ -250,7 +250,7 @@ namespace Avalonia.Controls if (e.Delta.Y < 0) SelectNext(); else - SelectPrev(); + SelectPrevious(); e.Handled = true; } @@ -399,12 +399,12 @@ namespace Avalonia.Controls var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { - var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + var container = ContainerFromIndex(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); - container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + container = ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) @@ -478,19 +478,40 @@ namespace Avalonia.Controls } } - private void SelectNext() - { - if (ItemCount >= 1) - { - MoveSelection(NavigationDirection.Next, WrapSelection); - } - } + private void SelectNext() => MoveSelection(SelectedIndex, 1, WrapSelection); + private void SelectPrevious() => MoveSelection(SelectedIndex, -1, WrapSelection); - private void SelectPrev() + private void MoveSelection(int startIndex, int step, bool wrap) { - if (ItemCount >= 1) + static bool IsSelectable(object? o) => (o as AvaloniaObject)?.GetValue(IsEnabledProperty) ?? true; + + var count = ItemCount; + + for (int i = startIndex + step; i != startIndex; i += step) { - MoveSelection(NavigationDirection.Previous, WrapSelection); + if (i < 0 || i >= count) + { + if (wrap) + { + if (i < 0) + i += count; + else if (i >= count) + i %= count; + } + else + { + return; + } + } + + var item = ItemsView[i]; + var container = ContainerFromIndex(i); + + if (IsSelectable(item) && IsSelectable(container)) + { + SelectedIndex = i; + break; + } } } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ed24c3c7c2..dbece12575 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; using System.ComponentModel; using Avalonia.Automation.Peers; -using Avalonia.Controls.Documents; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.Media; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Threading; @@ -163,7 +161,7 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -211,8 +209,6 @@ namespace Avalonia.Controls remove => RemoveHandler(SizeChangedEvent, value); } - public new Control? Parent => (Control?)base.Parent; - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; diff --git a/src/Avalonia.Controls/Controls.cs b/src/Avalonia.Controls/Controls.cs index 8b0e998f64..736c7e8a77 100644 --- a/src/Avalonia.Controls/Controls.cs +++ b/src/Avalonia.Controls/Controls.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Avalonia.Collections; @@ -13,7 +14,7 @@ namespace Avalonia.Controls /// public Controls() { - ResetBehavior = ResetBehavior.Remove; + Configure(); } /// @@ -21,9 +22,22 @@ namespace Avalonia.Controls /// /// The initial items in the collection. public Controls(IEnumerable items) - : base(items) + { + Configure(); + AddRange(items); // virtual member call in ctor, ok for our current implementation + } + + private void Configure() { ResetBehavior = ResetBehavior.Remove; + Validate = item => + { + if (item is null) + { + throw new ArgumentNullException(nameof(item), + $"A null control cannot be added to a {nameof(Controls)} collection."); + } + }; } } } diff --git a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs index 9d859a753a..18d668e9a4 100644 --- a/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs +++ b/src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs @@ -14,7 +14,6 @@ namespace Avalonia.Controls.Converters public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { if (parameter == null || - values == null || values.Count != 4 || !(values[0] is ScrollBarVisibility visibility) || !(values[1] is double offset) || diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs index 5c35a09f1c..eb587fb157 100644 --- a/src/Avalonia.Controls/DefinitionBase.cs +++ b/src/Avalonia.Controls/DefinitionBase.cs @@ -21,9 +21,9 @@ namespace Avalonia.Controls /// /// SharedSizeGroup property. /// - public string SharedSizeGroup + public string? SharedSizeGroup { - get { return (string)GetValue(SharedSizeGroupProperty); } + get { return GetValue(SharedSizeGroupProperty); } set { SetValue(SharedSizeGroupProperty, value); } } @@ -32,20 +32,15 @@ namespace Avalonia.Controls /// internal void OnEnterParentTree() { - this.InheritanceParent = Parent; + InheritanceParent = Parent; if (_sharedState == null) { // start with getting SharedSizeGroup value. // this property is NOT inherited which should result in better overall perf. - string sharedSizeGroupId = SharedSizeGroup; - if (sharedSizeGroupId != null) + if (SharedSizeGroup is { } sharedSizeGroupId && PrivateSharedSizeScope is { } privateSharedSizeScope) { - SharedSizeScope? privateSharedSizeScope = PrivateSharedSizeScope; - if (privateSharedSizeScope != null) - { - _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); - _sharedState.AddMember(this); - } + _sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + _sharedState.AddMember(this); } } @@ -321,13 +316,12 @@ namespace Avalonia.Controls return ((_flags & flags) == flags); } - private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + private static void OnSharedSizeGroupPropertyChanged(DefinitionBase definition, + AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase)d; - if (definition.Parent != null) { - string sharedSizeGroupId = (string)e.NewValue!; + string? sharedSizeGroupId = e.NewValue.Value; if (definition._sharedState != null) { @@ -337,16 +331,14 @@ namespace Avalonia.Controls definition._sharedState = null; } - if ((definition._sharedState == null) && (sharedSizeGroupId != null)) + if (definition._sharedState == null + && sharedSizeGroupId != null + && definition.PrivateSharedSizeScope is { } privateSharedSizeScope) { - SharedSizeScope? privateSharedSizeScope = definition.PrivateSharedSizeScope; - if (privateSharedSizeScope != null) - { - // if definition is not registered and both: shared size group id AND private shared scope - // are available, then register definition. - definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); - definition._sharedState.AddMember(definition); - } + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId); + definition._sharedState.AddMember(definition); } } } @@ -357,17 +349,15 @@ namespace Avalonia.Controls /// b) contains only letters, digits and underscore ('_'). /// c) does not start with a digit. /// - private static bool SharedSizeGroupPropertyValueValid(string value) + private static bool SharedSizeGroupPropertyValueValid(string? id) { // null is default value - if (value == null) + if (id == null) { return true; } - string id = (string)value; - - if (!string.IsNullOrEmpty(id)) + if (id.Length > 0) { int i = -1; while (++i < id.Length) @@ -397,14 +387,11 @@ namespace Avalonia.Controls /// existing scope just left. In both cases if the DefinitionBase object is already registered /// in SharedSizeState, it should un-register and register itself in a new one. /// - private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + private static void OnPrivateSharedSizeScopePropertyChanged(DefinitionBase definition, + AvaloniaPropertyChangedEventArgs e) { - DefinitionBase definition = (DefinitionBase)d; - if (definition.Parent != null) { - SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue!; - if (definition._sharedState != null) { // if definition is already registered And shared size scope is changing, @@ -413,16 +400,14 @@ namespace Avalonia.Controls definition._sharedState = null; } - if ((definition._sharedState == null) && (privateSharedSizeScope != null)) + if (definition._sharedState == null + && e.NewValue.Value is { } privateSharedSizeScope + && definition.SharedSizeGroup is { } sharedSizeGroup) { - string sharedSizeGroup = definition.SharedSizeGroup; - if (sharedSizeGroup != null) - { - // if definition is not registered and both: shared size group id AND private shared scope - // are available, then register definition. - definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup); - definition._sharedState.AddMember(definition); - } + // if definition is not registered and both: shared size group id AND private shared scope + // are available, then register definition. + definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroup); + definition._sharedState.AddMember(definition); } } } @@ -432,7 +417,7 @@ namespace Avalonia.Controls /// private SharedSizeScope? PrivateSharedSizeScope { - get { return (SharedSizeScope?)GetValue(PrivateSharedSizeScopeProperty); } + get { return GetValue(PrivateSharedSizeScopeProperty); } } /// @@ -465,7 +450,7 @@ namespace Avalonia.Controls private SharedSizeState? _sharedState; // reference to shared state object this instance is registered with - [System.Flags] + [Flags] private enum Flags : byte { // @@ -520,11 +505,10 @@ namespace Avalonia.Controls /// internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId) { - Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null); _sharedSizeScope = sharedSizeScope; _sharedSizeGroupId = sharedSizeGroupId; _registry = new List(); - _layoutUpdated = new EventHandler(OnLayoutUpdated); + _layoutUpdated = OnLayoutUpdated; _broadcastInvalidation = true; } @@ -568,7 +552,7 @@ namespace Avalonia.Controls { for (int i = 0, count = _registry.Count; i < count; ++i) { - Grid parentGrid = (Grid)(_registry[i].Parent!); + Grid parentGrid = _registry[i].Parent!; parentGrid.Invalidate(); } _broadcastInvalidation = false; @@ -703,7 +687,7 @@ namespace Avalonia.Controls // measure is invalid - it used the old shared size, // which is larger than d's (possibly changed) minSize measureIsValid = (definitionBase.LayoutWasUpdated && - MathUtilities.GreaterThanOrClose(definitionBase._minSize, this.MinSize)); + MathUtilities.GreaterThanOrClose(definitionBase._minSize, MinSize)); } if(!measureIsValid) @@ -786,8 +770,8 @@ namespace Avalonia.Controls /// /// /// - public static readonly AttachedProperty SharedSizeGroupProperty = - AvaloniaProperty.RegisterAttached( + public static readonly AttachedProperty SharedSizeGroupProperty = + AvaloniaProperty.RegisterAttached( "SharedSizeGroup", validate: SharedSizeGroupPropertyValueValid); @@ -796,8 +780,8 @@ namespace Avalonia.Controls /// static DefinitionBase() { - SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); - PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); + SharedSizeGroupProperty.Changed.AddClassHandler(OnSharedSizeGroupPropertyChanged); + PrivateSharedSizeScopeProperty.Changed.AddClassHandler(OnPrivateSharedSizeScopePropertyChanged); } /// diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 3e3ed509b5..1a0cf1644a 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -101,9 +101,6 @@ namespace Avalonia.Controls Size childConstraint; // Contains the suggested input constraint for this child. Size childDesiredSize; // Contains the return size from child measure. - if (child == null) - { continue; } - // Child constraint is the remaining size; this is total size minus size consumed by previous children. childConstraint = new Size(Math.Max(0.0, constraint.Width - accumulatedWidth), Math.Max(0.0, constraint.Height - accumulatedHeight)); @@ -122,7 +119,7 @@ namespace Avalonia.Controls // will deal with computing our minimum size (parentSize) due to that accumulation. // Therefore, we only need to compute our minimum size (parentSize) in dimensions that this child does // not accumulate: Width for Top/Bottom, Height for Left/Right. - switch (DockPanel.GetDock((Control)child)) + switch (GetDock(child)) { case Dock.Left: case Dock.Right: @@ -164,8 +161,6 @@ namespace Avalonia.Controls for (int i = 0; i < totalChildrenCount; ++i) { var child = children[i]; - if (child == null) - { continue; } Size childDesiredSize = child.DesiredSize; Rect rcChild = new Rect( @@ -176,7 +171,7 @@ namespace Avalonia.Controls if (i < nonFillChildrenCount) { - switch (DockPanel.GetDock((Control)child)) + switch (GetDock(child)) { case Dock.Left: accumulatedLeft += childDesiredSize.Width; diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 47581e87f1..23b806583e 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -13,8 +13,8 @@ namespace Avalonia.Controls.Documents /// /// AvaloniaProperty for property. /// - public static readonly StyledProperty TextDecorationsProperty = - AvaloniaProperty.Register( + public static readonly StyledProperty TextDecorationsProperty = + AvaloniaProperty.Register( nameof(TextDecorations)); /// @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Documents /// /// The TextDecorations property specifies decorations that are added to the text of an element. /// - public TextDecorationCollection TextDecorations + public TextDecorationCollection? TextDecorations { get { return GetValue(TextDecorationsProperty); } set { SetValue(TextDecorationsProperty, value); } @@ -83,7 +83,8 @@ namespace Avalonia.Controls.Documents return new GenericTextRunProperties(new Typeface(FontFamily, fontStyle, fontWeight), FontSize, textDecorations, Foreground, background, BaselineAlignment); } - + + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 58afb24b5c..f06c8515ee 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -64,5 +64,23 @@ namespace Avalonia.Controls.Documents internal override void AppendText(StringBuilder stringBuilder) { } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ChildProperty) + { + if(change.OldValue is Control oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if(change.NewValue is Control newChild) + { + LogicalChildren.Add(newChild); + } + } + } } } diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index a7a702ceae..d3565cbdd5 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text; using Avalonia.Media.TextFormatting; @@ -51,6 +52,7 @@ namespace Avalonia.Controls.Documents } } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -68,26 +70,26 @@ namespace Avalonia.Controls.Documents { base.OnInlineHostChanged(oldValue, newValue); - if (Inlines is not null) - { - Inlines.InlineHost = newValue; - } + Inlines.InlineHost = newValue; } private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue) { + void OnInlinesInvalidated(object? sender, EventArgs e) + => InlineHost?.Invalidate(); + if (oldValue is not null) { oldValue.LogicalChildren = null; oldValue.InlineHost = null; - oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate(); + oldValue.Invalidated -= OnInlinesInvalidated; } if (newValue is not null) { newValue.LogicalChildren = LogicalChildren; newValue.InlineHost = InlineHost; - newValue.Invalidated += (s, e) => InlineHost?.Invalidate(); + newValue.Invalidated += OnInlinesInvalidated; } } } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 64f96b6987..3a4ae80cf4 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Threading; namespace Avalonia.Controls.Embedding.Offscreen { @@ -13,7 +16,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; - private PlatformRenderInterfaceContextManager _renderInterface = new(null); + private ManualRenderTimer _manualRenderTimer = new(); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -23,10 +26,19 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } + class ManualRenderTimer : IRenderTimer + { + static Stopwatch St = Stopwatch.StartNew(); + public event Action? Tick; + public bool RunsInBackground => false; + public void TriggerTick() => Tick?.Invoke(St.Elapsed); + } + + public IRenderer CreateRenderer(IRenderRoot root) => - new ImmediateRenderer((Visual)root, () => _renderInterface.CreateRenderTarget(Surfaces), _renderInterface); + new CompositingRenderer(root, new Compositor(new RenderLoop(_manualRenderTimer, Dispatcher.UIThread), null), + () => Surfaces); - public abstract void Invalidate(Rect rect); public abstract IEnumerable Surfaces { get; } public Size ClientSize @@ -82,5 +94,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public WindowTransparencyLevel TransparencyLevel { get; private set; } public IPopupImpl? CreatePopup() => null; + + public virtual object? TryGetFeature(Type featureType) => null; } } diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 65227a826a..2ad6a58d38 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -59,12 +59,11 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty IsExpandedProperty = - AvaloniaProperty.RegisterDirect( + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register( nameof(IsExpanded), - o => o.IsExpanded, - (o, v) => o.IsExpanded = v, - defaultBindingMode: Data.BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + coerce: CoerceIsExpanded); /// /// Defines the event. @@ -77,8 +76,8 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent CollapsingEvent = - RoutedEvent.Register( + public static readonly RoutedEvent CollapsingEvent = + RoutedEvent.Register( nameof(Collapsing), RoutingStrategies.Bubble); @@ -93,13 +92,12 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent ExpandingEvent = - RoutedEvent.Register( + public static readonly RoutedEvent ExpandingEvent = + RoutedEvent.Register( nameof(Expanding), RoutingStrategies.Bubble); private bool _ignorePropertyChanged = false; - private bool _isExpanded; private CancellationTokenSource? _lastTransitionCts; /// @@ -134,50 +132,8 @@ namespace Avalonia.Controls /// public bool IsExpanded { - get => _isExpanded; - set - { - // It is important here that IsExpanded is a direct property so events can be invoked - // BEFORE the property system gets notified of updated values. This is because events - // may be canceled by external code. - if (_isExpanded != value) - { - RoutedEventArgs eventArgs; - - if (value) - { - eventArgs = new RoutedEventArgs(ExpandingEvent, this); - OnExpanding(eventArgs); - } - else - { - eventArgs = new RoutedEventArgs(CollapsingEvent, this); - OnCollapsing(eventArgs); - } - - if (eventArgs.Handled) - { - // If the event was externally handled (canceled) we must still notify the value has changed. - // This property changed notification will update any external code observing this property that itself may have set the new value. - // We are essentially reverted any external state change along with ignoring the IsExpanded property set. - // Remember IsExpanded is usually controlled by a ToggleButton in the control theme. - _ignorePropertyChanged = true; - - RaisePropertyChanged( - IsExpandedProperty, - oldValue: value, - newValue: _isExpanded, - BindingPriority.LocalValue, - isEffectiveValue: true); - - _ignorePropertyChanged = false; - } - else - { - SetAndRaise(IsExpandedProperty, ref _isExpanded, value); - } - } - } + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); } /// @@ -193,10 +149,10 @@ namespace Avalonia.Controls /// Occurs as the content area is closing. /// /// - /// The event args property may be set to true to cancel the event + /// The event args property may be set to true to cancel the event /// and keep the control open (expanded). /// - public event EventHandler? Collapsing + public event EventHandler? Collapsing { add => AddHandler(CollapsingEvent, value); remove => RemoveHandler(CollapsingEvent, value); @@ -215,10 +171,10 @@ namespace Avalonia.Controls /// Occurs as the content area is opening. /// /// - /// The event args property may be set to true to cancel the event + /// The event args property may be set to true to cancel the event /// and keep the control closed (collapsed). /// - public event EventHandler? Expanding + public event EventHandler? Expanding { add => AddHandler(ExpandingEvent, value); remove => RemoveHandler(ExpandingEvent, value); @@ -332,5 +288,63 @@ namespace Avalonia.Controls PseudoClasses.Set(":expanded", IsExpanded); } + + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual bool OnCoerceIsExpanded(bool value) + { + CancelRoutedEventArgs eventArgs; + + if (value) + { + eventArgs = new CancelRoutedEventArgs(ExpandingEvent, this); + OnExpanding(eventArgs); + } + else + { + eventArgs = new CancelRoutedEventArgs(CollapsingEvent, this); + OnCollapsing(eventArgs); + } + + if (eventArgs.Cancel) + { + // If the event was externally canceled we must still notify the value has changed. + // This property changed notification will update any external code observing this property that itself may have set the new value. + // We are essentially reverted any external state change along with ignoring the IsExpanded property set. + // Remember IsExpanded is usually controlled by a ToggleButton in the control theme and is also used for animations. + _ignorePropertyChanged = true; + + RaisePropertyChanged( + IsExpandedProperty, + oldValue: value, + newValue: !value, + BindingPriority.LocalValue, + isEffectiveValue: true); + + _ignorePropertyChanged = false; + + return !value; + } + + return value; + } + + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static bool CoerceIsExpanded(AvaloniaObject instance, bool value) + { + if (instance is Expander expander) + { + return expander.OnCoerceIsExpanded(value); + } + + return value; + } } } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e4487d29fa..e1f840672d 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls _subscription?.Dispose(); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) { diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 97fda68051..b028a8f007 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -3,6 +3,7 @@ using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -27,6 +28,12 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(ItemTemplate), x => x.ItemTemplate, (x, v) => x.ItemTemplate = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemContainerThemeProperty = + ItemsControl.ItemContainerThemeProperty.AddOwner(); + public Classes FlyoutPresenterClasses => _classes ??= new Classes(); /// @@ -48,6 +55,15 @@ namespace Avalonia.Controls set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); } + /// + /// Gets or sets the that is applied to the container element generated for each item. + /// + public ControlTheme? ItemContainerTheme + { + get { return GetValue(ItemContainerThemeProperty); } + set { SetValue(ItemContainerThemeProperty, value); } + } + private Classes? _classes; private IEnumerable? _items; private IDataTemplate? _itemTemplate; @@ -57,7 +73,8 @@ namespace Avalonia.Controls return new MenuFlyoutPresenter { [!ItemsControl.ItemsProperty] = this[!ItemsProperty], - [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty] + [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty], + [!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty], }; } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 7aca21b42e..594d7da2fb 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -1,5 +1,4 @@ using System; -using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 7737fdac2e..ff9fb4e31d 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -164,20 +164,21 @@ namespace Avalonia.Controls /// /// Returns a ColumnDefinitions of column definitions. /// + [MemberNotNull(nameof(_extData))] public ColumnDefinitions ColumnDefinitions { get { - if (_data == null) { _data = new ExtendedData(); } - if (_data.ColumnDefinitions == null) { _data.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } + if (_extData == null) { _extData = new ExtendedData(); } + if (_extData.ColumnDefinitions == null) { _extData.ColumnDefinitions = new ColumnDefinitions() { Parent = this }; } - return (_data.ColumnDefinitions); + return (_extData.ColumnDefinitions); } set { - if (_data == null) { _data = new ExtendedData(); } - _data.ColumnDefinitions = value; - _data.ColumnDefinitions.Parent = this; + if (_extData == null) { _extData = new ExtendedData(); } + _extData.ColumnDefinitions = value; + _extData.ColumnDefinitions.Parent = this; InvalidateMeasure(); } } @@ -185,20 +186,21 @@ namespace Avalonia.Controls /// /// Returns a RowDefinitions of row definitions. /// + [MemberNotNull(nameof(_extData))] public RowDefinitions RowDefinitions { get { - if (_data == null) { _data = new ExtendedData(); } - if (_data.RowDefinitions == null) { _data.RowDefinitions = new RowDefinitions() { Parent = this }; } + if (_extData == null) { _extData = new ExtendedData(); } + if (_extData.RowDefinitions == null) { _extData.RowDefinitions = new RowDefinitions() { Parent = this }; } - return (_data.RowDefinitions); + return (_extData.RowDefinitions); } set { - if (_data == null) { _data = new ExtendedData(); } - _data.RowDefinitions = value; - _data.RowDefinitions.Parent = this; + if (_extData == null) { _extData = new ExtendedData(); } + _extData.RowDefinitions = value; + _extData.RowDefinitions.Parent = this; InvalidateMeasure(); } } @@ -211,7 +213,7 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size constraint) { Size gridDesiredSize; - ExtendedData extData = ExtData; + var extData = _extData; try { @@ -221,17 +223,14 @@ namespace Avalonia.Controls if (extData == null) { gridDesiredSize = new Size(); - var children = this.Children; + var children = Children; for (int i = 0, count = children.Count; i < count; ++i) { var child = children[i]; - if (child != null) - { - child.Measure(constraint); - gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), - Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); - } + child.Measure(constraint); + gridDesiredSize = new Size(Math.Max(gridDesiredSize.Width, child.DesiredSize.Width), + Math.Max(gridDesiredSize.Height, child.DesiredSize.Height)); } } else @@ -512,17 +511,14 @@ namespace Avalonia.Controls { ArrangeOverrideInProgress = true; - if (_data == null) + if (_extData is null) { - var children = this.Children; + var children = Children; for (int i = 0, count = children.Count; i < count; ++i) { var child = children[i]; - if (child != null) - { - child.Arrange(new Rect(arrangeSize)); - } + child.Arrange(new Rect(arrangeSize)); } } else @@ -532,15 +528,11 @@ namespace Avalonia.Controls SetFinalSize(DefinitionsU, arrangeSize.Width, true); SetFinalSize(DefinitionsV, arrangeSize.Height, false); - var children = this.Children; + var children = Children; for (int currentCell = 0; currentCell < PrivateCells.Length; ++currentCell) { var cell = children[currentCell]; - if (cell == null) - { - continue; - } int columnIndex = PrivateCells[currentCell].ColumnIndex; int rowIndex = PrivateCells[currentCell].RowIndex; @@ -599,7 +591,7 @@ namespace Avalonia.Controls { double value = 0.0; - Debug.Assert(_data != null); + Debug.Assert(_extData != null); // actual value calculations require structure to be up-to-date if (!ColumnDefinitionsDirty) @@ -621,7 +613,7 @@ namespace Avalonia.Controls { double value = 0.0; - Debug.Assert(_data != null); + Debug.Assert(_extData != null); // actual value calculations require structure to be up-to-date if (!RowDefinitionsDirty) @@ -654,18 +646,20 @@ namespace Avalonia.Controls /// /// Convenience accessor to ValidDefinitionsUStructure bit flag. /// + [MemberNotNull(nameof(_extData))] internal bool ColumnDefinitionsDirty { - get => ColumnDefinitions?.IsDirty ?? false; + get => ColumnDefinitions.IsDirty; set => ColumnDefinitions.IsDirty = value; } /// /// Convenience accessor to ValidDefinitionsVStructure bit flag. /// + [MemberNotNull(nameof(_extData))] internal bool RowDefinitionsDirty { - get => RowDefinitions?.IsDirty ?? false; + get => RowDefinitions.IsDirty; set => RowDefinitions.IsDirty = value; } @@ -686,8 +680,10 @@ namespace Avalonia.Controls /// private void ValidateCellsCore() { - var children = this.Children; - ExtendedData extData = ExtData; + Debug.Assert(_extData is not null); + + var children = Children; + var extData = _extData!; extData.CellCachesCollection = new CellCache[children.Count]; extData.CellGroup1 = int.MaxValue; @@ -702,10 +698,6 @@ namespace Avalonia.Controls for (int i = PrivateCells.Length - 1; i >= 0; --i) { var child = children[i]; - if (child == null) - { - continue; - } CellCache cell = new CellCache(); @@ -713,19 +705,19 @@ namespace Avalonia.Controls // Read indices from the corresponding properties: // clamp to value < number_of_columns // column >= 0 is guaranteed by property value validation callback - cell.ColumnIndex = Math.Min(GetColumn((Control)child), DefinitionsU.Count - 1); + cell.ColumnIndex = Math.Min(GetColumn(child), DefinitionsU.Count - 1); // clamp to value < number_of_rows // row >= 0 is guaranteed by property value validation callback - cell.RowIndex = Math.Min(GetRow((Control)child), DefinitionsV.Count - 1); + cell.RowIndex = Math.Min(GetRow(child), DefinitionsV.Count - 1); // Read span properties: // clamp to not exceed beyond right side of the grid // column_span > 0 is guaranteed by property value validation callback - cell.ColumnSpan = Math.Min(GetColumnSpan((Control)child), DefinitionsU.Count - cell.ColumnIndex); + cell.ColumnSpan = Math.Min(GetColumnSpan(child), DefinitionsU.Count - cell.ColumnIndex); // clamp to not exceed beyond bottom side of the grid // row_span > 0 is guaranteed by property value validation callback - cell.RowSpan = Math.Min(GetRowSpan((Control)child), DefinitionsV.Count - cell.RowIndex); + cell.RowSpan = Math.Min(GetRowSpan(child), DefinitionsV.Count - cell.RowIndex); Debug.Assert(0 <= cell.ColumnIndex && cell.ColumnIndex < DefinitionsU.Count); Debug.Assert(0 <= cell.RowIndex && cell.RowIndex < DefinitionsV.Count); @@ -792,7 +784,7 @@ namespace Avalonia.Controls { if (ColumnDefinitionsDirty) { - ExtendedData extData = ExtData; + var extData = _extData; if (extData.ColumnDefinitions == null) { @@ -818,7 +810,7 @@ namespace Avalonia.Controls ColumnDefinitionsDirty = false; } - Debug.Assert(ExtData.DefinitionsU != null && ExtData.DefinitionsU.Count > 0); + Debug.Assert(_extData is { DefinitionsU.Count: > 0 }); } /// @@ -833,7 +825,7 @@ namespace Avalonia.Controls { if (RowDefinitionsDirty) { - ExtendedData extData = ExtData; + var extData = _extData; if (extData.RowDefinitions == null) { @@ -859,7 +851,7 @@ namespace Avalonia.Controls RowDefinitionsDirty = false; } - Debug.Assert(ExtData.DefinitionsV != null && ExtData.DefinitionsV.Count > 0); + Debug.Assert(_extData is { DefinitionsV.Count: > 0 }); } /// @@ -965,8 +957,7 @@ namespace Avalonia.Controls bool ignoreDesiredSizeU, bool forceInfinityV) { - bool unusedHasDesiredSizeUChanged; - MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out unusedHasDesiredSizeUChanged); + MeasureCellsGroup(cellsHead, referenceSize, ignoreDesiredSizeU, forceInfinityV, out _); } /// @@ -994,7 +985,7 @@ namespace Avalonia.Controls return; } - var children = this.Children; + var children = Children; Hashtable? spanStore = null; bool ignoreDesiredSizeV = forceInfinityV; @@ -1101,8 +1092,6 @@ namespace Avalonia.Controls int cell, bool forceInfinityV) { - - double cellMeasureWidth; double cellMeasureHeight; @@ -1144,15 +1133,9 @@ namespace Avalonia.Controls } - var child = this.Children[cell]; - if (child != null) - { - Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); - child.Measure(childConstraint); - } - - - + var child = Children[cell]; + Size childConstraint = new Size(cellMeasureWidth, cellMeasureHeight); + child.Measure(childConstraint); } /// @@ -1230,7 +1213,7 @@ namespace Avalonia.Controls // avoid processing when asked to distribute "0" if (!MathUtilities.IsZero(requestedSize)) { - DefinitionBase[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting + DefinitionBase?[] tempDefinitions = TempDefinitions; // temp array used to remember definitions for sorting int end = start + count; int autoDefinitionsCount = 0; double rangeMinSize = 0; @@ -1288,20 +1271,24 @@ namespace Avalonia.Controls Array.Sort(tempDefinitions, 0, count, s_spanPreferredDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize; i < autoDefinitionsCount; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(tempDefinition.UserSize.IsAuto); // adjust sizeToDistribute value by subtracting auto definition min size - sizeToDistribute -= (tempDefinitions[i].MinSize); + sizeToDistribute -= (tempDefinition.MinSize); } for (; i < count; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(!tempDefinition.UserSize.IsAuto); - double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinitions[i].PreferredSize); - if (newMinSize > tempDefinitions[i].MinSize) { tempDefinitions[i].UpdateMinSize(newMinSize); } + double newMinSize = Math.Min(sizeToDistribute / (count - i), tempDefinition.PreferredSize); + if (newMinSize > tempDefinition.MinSize) { tempDefinition.UpdateMinSize(newMinSize); } sizeToDistribute -= newMinSize; } @@ -1325,24 +1312,28 @@ namespace Avalonia.Controls Array.Sort(tempDefinitions, 0, count, s_spanMaxDistributionOrderComparer); for (i = 0, sizeToDistribute = requestedSize - rangePreferredSize; i < count - autoDefinitionsCount; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: no auto definitions allowed in this loop - Debug.Assert(!tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(!tempDefinition.UserSize.IsAuto); - double preferredSize = tempDefinitions[i].PreferredSize; + double preferredSize = tempDefinition.PreferredSize; double newMinSize = preferredSize + sizeToDistribute / (count - autoDefinitionsCount - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + tempDefinition.UpdateMinSize(Math.Min(newMinSize, tempDefinition.SizeCache)); + sizeToDistribute -= (tempDefinition.MinSize - preferredSize); } for (; i < count; ++i) { + var tempDefinition = tempDefinitions[i]!; + // sanity check: only auto definitions allowed in this loop - Debug.Assert(tempDefinitions[i].UserSize.IsAuto); + Debug.Assert(tempDefinition.UserSize.IsAuto); - double preferredSize = tempDefinitions[i].MinSize; + double preferredSize = tempDefinition.MinSize; double newMinSize = preferredSize + sizeToDistribute / (count - i); - tempDefinitions[i].UpdateMinSize(Math.Min(newMinSize, tempDefinitions[i].SizeCache)); - sizeToDistribute -= (tempDefinitions[i].MinSize - preferredSize); + tempDefinition.UpdateMinSize(Math.Min(newMinSize, tempDefinition.SizeCache)); + sizeToDistribute -= (tempDefinition.MinSize - preferredSize); } // sanity check: requested size must all be distributed @@ -1376,8 +1367,10 @@ namespace Avalonia.Controls for (int i = 0; i < count; ++i) { - double deltaSize = (maxMaxSize - tempDefinitions[i].SizeCache) * sizeToDistribute / totalRemainingSize; - tempDefinitions[i].UpdateMinSize(tempDefinitions[i].SizeCache + deltaSize); + var tempDefinition = tempDefinitions[i]!; + + double deltaSize = (maxMaxSize - tempDefinition.SizeCache) * sizeToDistribute / totalRemainingSize; + tempDefinition.UpdateMinSize(tempDefinition.SizeCache + deltaSize); } } else @@ -1388,7 +1381,7 @@ namespace Avalonia.Controls // for (int i = 0; i < count; ++i) { - tempDefinitions[i].UpdateMinSize(equalSize); + tempDefinitions[i]!.UpdateMinSize(equalSize); } } } @@ -1429,7 +1422,7 @@ namespace Avalonia.Controls double availableSize) { int defCount = definitions.Count; - DefinitionBase[] tempDefinitions = TempDefinitions; + DefinitionBase?[] tempDefinitions = TempDefinitions; int minCount = 0, maxCount = 0; double takenSize = 0; double totalStarWeight = 0.0; @@ -1560,8 +1553,8 @@ namespace Avalonia.Controls remainingStarWeight = totalStarWeight - takenStarWeight; } - double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1].MeasureSize : Double.PositiveInfinity; - double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1].SizeCache : -1.0; + double minRatio = (minCount > 0) ? tempDefinitions[minCount - 1]!.MeasureSize : Double.PositiveInfinity; + double maxRatio = (maxCount > 0) ? tempDefinitions[defCount + maxCount - 1]!.SizeCache : -1.0; // choose the def with larger ratio to the current proportion ("max discrepancy") double proportion = remainingStarWeight / remainingAvailableSize; @@ -1579,13 +1572,13 @@ namespace Avalonia.Controls double resolvedSize; if (chooseMin == true) { - resolvedDef = tempDefinitions[minCount - 1]; + resolvedDef = tempDefinitions[minCount - 1]!; resolvedSize = resolvedDef.MinSize; --minCount; } else { - resolvedDef = tempDefinitions[defCount + maxCount - 1]; + resolvedDef = tempDefinitions[defCount + maxCount - 1]!; resolvedSize = Math.Max(resolvedDef.MinSize, resolvedDef.UserMaxSize); --maxCount; } @@ -1603,12 +1596,12 @@ namespace Avalonia.Controls // advance to the next candidate defs, removing ones that have been resolved. // Both counts are advanced, as a def might appear in both lists. - while (minCount > 0 && tempDefinitions[minCount - 1].MeasureSize < 0.0) + while (minCount > 0 && tempDefinitions[minCount - 1]!.MeasureSize < 0.0) { --minCount; tempDefinitions[minCount] = null!; } - while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1].MeasureSize < 0.0) + while (maxCount > 0 && tempDefinitions[defCount + maxCount - 1]!.MeasureSize < 0.0) { --maxCount; tempDefinitions[defCount + maxCount] = null!; @@ -1637,8 +1630,7 @@ namespace Avalonia.Controls // resolved as 'min'. Their allocation can be increased to make up the gap. for (int i = minCount; i < minCountPhase2; ++i) { - DefinitionBase def = tempDefinitions[i]; - if (def != null) + if (tempDefinitions[i] is { } def) { def.MeasureSize = 1.0; // mark as 'not yet resolved' ++starCount; @@ -1653,8 +1645,7 @@ namespace Avalonia.Controls // resolved as 'max'. Their allocation can be decreased to make up the gap. for (int i = maxCount; i < maxCountPhase2; ++i) { - DefinitionBase def = tempDefinitions[defCount + i]; - if (def != null) + if (tempDefinitions[defCount + i] is { } def) { def.MeasureSize = 1.0; // mark as 'not yet resolved' ++starCount; @@ -1695,7 +1686,7 @@ namespace Avalonia.Controls totalStarWeight = 0.0; for (int i = 0; i < starCount; ++i) { - DefinitionBase def = tempDefinitions[i]; + DefinitionBase def = tempDefinitions[i]!; totalStarWeight += def.MeasureSize; def.SizeCache = totalStarWeight; } @@ -1703,7 +1694,7 @@ namespace Avalonia.Controls // resolve the defs, in decreasing order of weight for (int i = starCount - 1; i >= 0; --i) { - DefinitionBase def = tempDefinitions[i]; + DefinitionBase def = tempDefinitions[i]!; double resolvedSize = (def.MeasureSize > 0.0) ? Math.Max(availableSize - takenSize, 0.0) * (def.MeasureSize / def.SizeCache) : 0.0; // min and max should have no effect by now, but just in case... @@ -2095,7 +2086,7 @@ namespace Avalonia.Controls { // DpiScale dpiScale = GetDpi(); // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; - var dpi = (VisualRoot as Layout.ILayoutRoot)?.LayoutScaling ?? 1.0; + var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; double roundedTakenSize = 0.0; @@ -2302,8 +2293,7 @@ namespace Avalonia.Controls /// private void SetValid() { - ExtendedData extData = ExtData; - if (extData != null) + if (_extData is { } extData) { // for (int i = 0; i < PrivateColumnCount; ++i) DefinitionsU[i].SetValid (); // for (int i = 0; i < PrivateRowCount; ++i) DefinitionsV[i].SetValid (); @@ -2330,12 +2320,12 @@ namespace Avalonia.Controls if (ShowGridLines && (_gridLinesRenderer == null)) { _gridLinesRenderer = new GridLinesRenderer(); - this.VisualChildren.Add(_gridLinesRenderer); + VisualChildren.Add(_gridLinesRenderer); } if ((!ShowGridLines) && (_gridLinesRenderer != null)) { - this.VisualChildren.Add(_gridLinesRenderer); + VisualChildren.Add(_gridLinesRenderer); _gridLinesRenderer = null; } @@ -2364,7 +2354,7 @@ namespace Avalonia.Controls { Grid grid = (Grid)d; - if (grid.ExtData != null // trivial grid is 1 by 1. there is no grid lines anyway + if (grid._extData != null // trivial grid is 1 by 1. there is no grid lines anyway && grid.ListenToNotifications) { grid.InvalidateVisual(); @@ -2375,13 +2365,11 @@ namespace Avalonia.Controls private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - Visual? child = d as Visual; - - if (child != null) + if (d is Visual child) { Grid? grid = child.GetVisualParent(); if (grid != null - && grid.ExtData != null + && grid._extData != null && grid.ListenToNotifications) { grid.CellsStructureDirty = true; @@ -2427,7 +2415,7 @@ namespace Avalonia.Controls /// private IReadOnlyList DefinitionsU { - get { return (ExtData.DefinitionsU!); } + get { return _extData!.DefinitionsU!; } } /// @@ -2435,17 +2423,19 @@ namespace Avalonia.Controls /// private IReadOnlyList DefinitionsV { - get { return (ExtData.DefinitionsV!); } + get { return _extData!.DefinitionsV!; } } /// /// Helper accessor to layout time array of definitions. /// - private DefinitionBase[] TempDefinitions + private DefinitionBase?[] TempDefinitions { get { - ExtendedData extData = ExtData; + Debug.Assert(_extData is not null); + + var extData = _extData!; int requiredLength = Math.Max(DefinitionsU.Count, DefinitionsV.Count) * 2; if (extData.TempDefinitions == null @@ -2516,7 +2506,7 @@ namespace Avalonia.Controls /// private CellCache[] PrivateCells { - get { return (ExtData.CellCachesCollection!); } + get { return _extData!.CellCachesCollection!; } } /// @@ -2582,18 +2572,10 @@ namespace Avalonia.Controls set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); } } - /// - /// Returns reference to extended data bag. - /// - private ExtendedData ExtData - { - get { return (_data!); } - } - /// /// Returns *-weight, adjusted for scale computed during Phase 1 /// - static double StarWeight(DefinitionBase def, double scale) + private static double StarWeight(DefinitionBase def, double scale) { if (scale < 0.0) { @@ -2609,17 +2591,17 @@ namespace Avalonia.Controls } // Extended data instantiated on demand, for non-trivial case handling only - private ExtendedData? _data; + private ExtendedData? _extData; // Grid validity / property caches dirtiness flags private Flags _flags; private GridLinesRenderer? _gridLinesRenderer; // Keeps track of definition indices. - int[]? _definitionIndices; + private int[]? _definitionIndices; // Stores unrounded values and rounding errors during layout rounding. - double[]? _roundingErrors; + private double[]? _roundingErrors; // 5 is an arbitrary constant chosen to end the measure loop private const int c_layoutLoopMaxCount = 5; @@ -2645,14 +2627,14 @@ namespace Avalonia.Controls internal int CellGroup2; // index of the first cell in second cell group internal int CellGroup3; // index of the first cell in third cell group internal int CellGroup4; // index of the first cell in forth cell group - internal DefinitionBase[]? TempDefinitions; // temporary array used during layout for various purposes + internal DefinitionBase?[]? TempDefinitions; // temporary array used during layout for various purposes // TempDefinitions.Length == Max(definitionsU.Length, definitionsV.Length) } /// /// Grid validity / property caches dirtiness flags /// - [System.Flags] + [Flags] private enum Flags { // @@ -2768,7 +2750,7 @@ namespace Avalonia.Controls /// /// LayoutTimeSizeType is used internally and reflects layout-time size type. /// - [System.Flags] + [Flags] internal enum LayoutTimeSizeType : byte { None = 0x00, @@ -3274,7 +3256,7 @@ namespace Avalonia.Controls /// /// UpdateRenderBounds. /// - public override void Render(DrawingContext drawingContext) + public sealed override void Render(DrawingContext drawingContext) { var grid = this.GetVisualParent(); @@ -3317,7 +3299,7 @@ namespace Avalonia.Controls internal void UpdateRenderBounds(Size arrangeSize) { _lastArrangeSize = arrangeSize; - this.InvalidateVisual(); + InvalidateVisual(); } private static Size _lastArrangeSize; diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 7408bff902..3e76835e92 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -14,8 +14,8 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Defines the property. @@ -42,7 +42,7 @@ namespace Avalonia.Controls /// Gets or sets the image that will be displayed. /// [Content] - public IImage Source + public IImage? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -66,13 +66,14 @@ namespace Avalonia.Controls set { SetValue(StretchDirectionProperty, value); } } + /// protected override bool BypassFlowDirectionPolicies => true; /// /// Renders the control. /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var source = Source; diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index fee394fe08..8c3c65a6a7 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics.CodeAnalysis; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls.Generators; @@ -12,10 +11,11 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Styling; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -23,7 +23,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IChildIndexProvider + public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo { /// /// The default value for the property. @@ -72,15 +72,28 @@ namespace Avalonia.Controls /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Gets or sets the to use for binding to the display member of each item. /// [AssignBinding] + [InheritDataTypeFromItems(nameof(Items))] public IBinding? DisplayMemberBinding { - get { return GetValue(DisplayMemberBindingProperty); } - set { SetValue(DisplayMemberBindingProperty, value); } + get => GetValue(DisplayMemberBindingProperty); + set => SetValue(DisplayMemberBindingProperty, value); } private IEnumerable? _items = new AvaloniaList(); @@ -91,6 +104,7 @@ namespace Avalonia.Controls private EventHandler? _totalCountChanged; private IDataTemplate? _displayMemberItemTemplate; private ScrollViewer? _scrollViewer; + private ItemsPresenter? _itemsPresenter; /// /// Initializes a new instance of the class. @@ -118,8 +132,8 @@ namespace Avalonia.Controls [Content] public IEnumerable? Items { - get { return _items; } - set { SetAndRaise(ItemsProperty, ref _items, value); } + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); } /// @@ -127,8 +141,8 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get { return GetValue(ItemContainerThemeProperty); } - set { SetValue(ItemContainerThemeProperty, value); } + get => GetValue(ItemContainerThemeProperty); + set => SetValue(ItemContainerThemeProperty, value); } /// @@ -145,17 +159,18 @@ namespace Avalonia.Controls /// public ITemplate ItemsPanel { - get { return GetValue(ItemsPanelProperty); } - set { SetValue(ItemsPanelProperty, value); } + get => GetValue(ItemsPanelProperty); + set => SetValue(ItemsPanelProperty, value); } /// /// Gets or sets the data template used to display the items in the control. /// + [InheritDataTypeFromItems(nameof(Items))] public IDataTemplate? ItemTemplate { - get { return GetValue(ItemTemplateProperty); } - set { SetValue(ItemTemplateProperty, value); } + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); } /// @@ -209,6 +224,64 @@ namespace Avalonia.Controls remove => _totalCountChanged -= value; } + /// + public event EventHandler HorizontalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged -= value; + } + } + } + + /// + public event EventHandler VerticalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged -= value; + } + } + } + + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get => GetValue(AreHorizontalSnapPointsRegularProperty); + set => SetValue(AreHorizontalSnapPointsRegularProperty, value); + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get => GetValue(AreVerticalSnapPointsRegularProperty); + set => SetValue(AreVerticalSnapPointsRegularProperty, value); + } + /// /// Returns the container for the item at the specified index. /// @@ -261,7 +334,6 @@ namespace Avalonia.Controls /// public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty(); - /// /// Creates or a container that can be used to display an item. /// @@ -357,10 +429,12 @@ namespace Avalonia.Controls /// true if the item is (or is eligible to be) its own container; otherwise, false. protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true; + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); + _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } /// @@ -407,11 +481,13 @@ namespace Avalonia.Controls base.OnKeyDown(e); } + /// protected override AutomationPeer OnCreateAutomationPeer() { return new ItemsControlAutomationPeer(this); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -489,7 +565,12 @@ namespace Avalonia.Controls return new ItemContainerGenerator(this); } - internal void AddLogicalChild(Control c) => LogicalChildren.Add(c); + internal void AddLogicalChild(Control c) + { + if (!LogicalChildren.Contains(c)) + LogicalChildren.Add(c); + } + internal void RemoveLogicalChild(Control c) => LogicalChildren.Remove(c); /// @@ -672,5 +753,19 @@ namespace Avalonia.Controls count = ItemsView.Count; return true; } + + /// + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List(); + } + + /// + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0; + + return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0; + } } } diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ce254684b7..387dc27562 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls .AddClassHandler((x, e) => x.OnLayoutTransformChanged(e)); ChildProperty.Changed - .AddClassHandler((x, e) => x.OnChildChanged(e)); + .AddClassHandler((x, _) => x.OnChildChanged()); UseRenderTransformProperty.Changed .AddClassHandler((x, e) => x.OnUseRenderTransformPropertyChanged(e)); @@ -146,7 +146,7 @@ namespace Avalonia.Controls return transformedDesiredSize; } - IDisposable? _renderTransformChangedEvent; + private IDisposable? _renderTransformChangedEvent; private void OnUseRenderTransformPropertyChanged(AvaloniaPropertyChangedEventArgs e) { @@ -167,8 +167,7 @@ namespace Avalonia.Controls .Subscribe( (x) => { - var target2 = x.Sender as LayoutTransformControl; - if (target2 != null) + if (x.Sender is LayoutTransformControl target2) { target2.LayoutTransform = target2.RenderTransform; } @@ -182,7 +181,7 @@ namespace Avalonia.Controls } } - private void OnChildChanged(AvaloniaPropertyChangedEventArgs e) + private void OnChildChanged() { if (null != TransformRoot) { @@ -206,18 +205,18 @@ namespace Avalonia.Controls /// /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). /// - private Size _childActualSize = default; + private Size _childActualSize; /// /// RenderTransform/MatrixTransform applied to TransformRoot. /// - private MatrixTransform _matrixTransform = new MatrixTransform(); + private readonly MatrixTransform _matrixTransform = new(); /// /// Transformation matrix corresponding to _matrixTransform. /// private Matrix _transformation; - private IDisposable? _transformChangedEvent = null; + private IDisposable? _transformChangedEvent; /// /// Returns true if Size a is smaller than Size b in either dimension. @@ -263,10 +262,7 @@ namespace Avalonia.Controls // Get the transform matrix and apply it _transformation = RoundMatrix(LayoutTransform.Value, DecimalsAfterRound); - if (null != _matrixTransform) - { - _matrixTransform.Matrix = _transformation; - } + _matrixTransform.Matrix = _transformation; // New transform means re-layout is necessary InvalidateMeasure(); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 8b1a307182..80d1677c2f 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -104,6 +104,7 @@ namespace Avalonia.Controls public void UnselectAll() => Selection.Clear(); protected internal override Control CreateContainerForItemOverride() => new ListBoxItem(); + protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem; /// protected override void OnGotFocus(GotFocusEventArgs e) diff --git a/src/Avalonia.Controls/MaskedTextBox.cs b/src/Avalonia.Controls/MaskedTextBox.cs index 080326606e..5a3eb47ce4 100644 --- a/src/Avalonia.Controls/MaskedTextBox.cs +++ b/src/Avalonia.Controls/MaskedTextBox.cs @@ -178,12 +178,11 @@ namespace Avalonia.Controls } } - - } Type IStyleable.StyleKey => typeof(TextBox); + /// protected override void OnGotFocus(GotFocusEventArgs e) { if (HidePromptOnLeave == true && MaskProvider != null) @@ -193,6 +192,7 @@ namespace Avalonia.Controls base.OnGotFocus(e); } + /// protected override async void OnKeyDown(KeyEventArgs e) { if (MaskProvider == null) @@ -271,15 +271,17 @@ namespace Avalonia.Controls } } + /// protected override void OnLostFocus(RoutedEventArgs e) { - if (HidePromptOnLeave == true && MaskProvider != null) + if (HidePromptOnLeave && MaskProvider != null) { Text = MaskProvider.ToString(!HidePromptOnLeave, true); } base.OnLostFocus(e); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { void UpdateMaskProvider() @@ -357,6 +359,8 @@ namespace Avalonia.Controls } base.OnPropertyChanged(change); } + + /// protected override void OnTextInput(TextInputEventArgs e) { _ignoreTextChanges = true; @@ -423,7 +427,7 @@ namespace Avalonia.Controls return startPosition; } - private void RefreshText(MaskedTextProvider provider, int position) + private void RefreshText(MaskedTextProvider? provider, int position) { if (provider != null) { diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 1918964bc8..da7a36fa73 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -84,13 +84,13 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem?)ContainerFromIndex(index) : null; } set { SelectedIndex = value is Control c ? - ItemContainerGenerator.IndexFromContainer(c) : -1; + IndexFromContainer(c) : -1; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index aeee6f8410..5588bde7c0 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,7 +13,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -319,12 +318,12 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem?)ContainerFromIndex(index) : null; } set { - SelectedIndex = value is Control c ? ItemContainerGenerator.IndexFromContainer(c) : -1; + SelectedIndex = value is Control c ? IndexFromContainer(c) : -1; } } @@ -691,7 +690,7 @@ namespace Avalonia.Controls if (selected != -1) { - var container = ItemContainerGenerator?.ContainerFromIndex(selected); + var container = ContainerFromIndex(selected); container?.Focus(); } } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 18dc1b1264..a94a1ee983 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -16,19 +16,17 @@ namespace Avalonia.Controls private IPlatformHandle? _nativeControlHandle; private bool _queuedForDestruction; private bool _queuedForMoveResize; - private readonly List _propertyChangedSubscriptions = new List(); + private readonly List _propertyChangedSubscriptions = new(); + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; var visual = (Visual)this; while (visual != null) { - if (visual is Visual v) - { - v.PropertyChanged += PropertyChangedHandler; - _propertyChangedSubscriptions.Add(v); - } + visual.PropertyChanged += PropertyChangedHandler; + _propertyChangedSubscriptions.Add(visual); visual = visual.GetVisualParent(); } @@ -42,15 +40,13 @@ namespace Avalonia.Controls EnqueueForMoveResize(); } + /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = null; - if (_propertyChangedSubscriptions != null) - { - foreach (var v in _propertyChangedSubscriptions) - v.PropertyChanged -= PropertyChangedHandler; - _propertyChangedSubscriptions.Clear(); - } + foreach (var v in _propertyChangedSubscriptions) + v.PropertyChanged -= PropertyChangedHandler; + _propertyChangedSubscriptions.Clear(); UpdateHost(); } @@ -58,7 +54,7 @@ namespace Avalonia.Controls private void UpdateHost() { _queuedForMoveResize = false; - _currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; + _currentHost = _currentRoot?.PlatformImpl?.TryGetFeature(); if (_currentHost != null) { @@ -128,7 +124,7 @@ namespace Avalonia.Controls return new Rect(position.Value, bounds.Size); } - void EnqueueForMoveResize() + private void EnqueueForMoveResize() { if(_queuedForMoveResize) return; diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index c556ce7b02..ab64416a2c 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Controls.Platform; using Avalonia.Reactive; +using Avalonia.Platform; namespace Avalonia.Controls { @@ -11,17 +12,17 @@ namespace Avalonia.Controls public static bool GetIsNativeMenuExported(TopLevel tl) => tl.GetValue(IsNativeMenuExportedProperty); - private static readonly AttachedProperty s_nativeMenuInfoProperty = - AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); - - class NativeMenuInfo + private static readonly AttachedProperty s_nativeMenuInfoProperty = + AvaloniaProperty.RegisterAttached("___NativeMenuInfo"); + + private sealed class NativeMenuInfo { public bool ChangingIsExported { get; set; } public ITopLevelNativeMenuExporter? Exporter { get; } public NativeMenuInfo(TopLevel target) { - Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; + Exporter = target.PlatformImpl?.TryGetFeature(); if (Exporter != null) { Exporter.OnIsNativeMenuExportedChanged += delegate @@ -32,7 +33,7 @@ namespace Avalonia.Controls } } - static NativeMenuInfo GetInfo(TopLevel target) + private static NativeMenuInfo GetInfo(TopLevel target) { var rv = target.GetValue(s_nativeMenuInfoProperty); if (rv == null) @@ -44,18 +45,18 @@ namespace Avalonia.Controls return rv; } - static void SetIsNativeMenuExported(TopLevel tl, bool value) + private static void SetIsNativeMenuExported(TopLevel tl, bool value) { GetInfo(tl).ChangingIsExported = true; tl.SetValue(IsNativeMenuExportedProperty, value); } - public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu"); + public static readonly AttachedProperty MenuProperty + = AvaloniaProperty.RegisterAttached("Menu"); - public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static void SetMenu(AvaloniaObject o, NativeMenu? menu) => o.SetValue(MenuProperty, menu); - public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); + public static NativeMenu? GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); static NativeMenu() { diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 3ccddf4155..b03099f750 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Notifications /// [TemplatePart("PART_Items", typeof(Panel))] [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] - public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest + public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { private IList? _items; @@ -160,7 +160,5 @@ namespace Avalonia.Controls.Notifications PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft); PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight); } - - public bool HitTest(Point point) => VisualChildren.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index ee18b3c4ab..3006efb7de 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls /// Renders the visual to a . /// /// The drawing context. - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var background = Background; if (background != null) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index e09da02f17..4dd868253e 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -350,13 +350,19 @@ namespace Avalonia.Controls.Platform { // HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. var item = GetMenuItem(e.Source as Control) as MenuItem; - if (item?.TransformedBounds == null) - { + + if (item == null) return; - } + + var serverTransform = item?.CompositionVisual?.TryGetServerGlobalTransform(); + if (serverTransform == null) + return; + var point = e.GetCurrentPoint(null); + var transformedPoint = point.Position.Transform(serverTransform.Value); - if (point.Properties.IsLeftButtonPressed && item.TransformedBounds.Value.Contains(point.Position) == false) + if (point.Properties.IsLeftButtonPressed && + new Rect(item!.Bounds.Size).Contains(transformedPoint) == false) { e.Pointer.Capture(null); } @@ -547,7 +553,7 @@ namespace Avalonia.Controls.Platform } } - protected static IMenuItem? GetMenuItem(Control? item) + protected static IMenuItem? GetMenuItem(StyledElement? item) { while (true) { diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index 2775c53803..20bfb440e3 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Avalonia.Platform.Storage; #nullable enable @@ -26,9 +27,7 @@ namespace Avalonia.Controls.Platform var files = await filePicker.OpenFilePickerAsync(options); return files - .Select(file => file.TryGetUri(out var fullPath) - ? fullPath.LocalPath - : file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } else if (dialog is SaveFileDialog saveDialog) @@ -47,9 +46,7 @@ namespace Avalonia.Controls.Platform return null; } - var filePath = file.TryGetUri(out var fullPath) - ? fullPath.LocalPath - : file.Name; + var filePath = file.TryGetLocalPath() ?? file.Name; return new[] { filePath }; } return null; @@ -67,7 +64,7 @@ namespace Avalonia.Controls.Platform var folders = await filePicker.OpenFolderPickerAsync(options); return folders - .Select(f => f.TryGetUri(out var uri) ? uri.LocalPath : null) + .Select(folder => folder.TryGetLocalPath() ?? folder.Name) .FirstOrDefault(u => u is not null); } } diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs index 5a34c5c0e1..f271abb59a 100644 --- a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs @@ -41,7 +41,7 @@ namespace Avalonia.Platform /// The fallback module will only be initialized if the Skia-specific module is not applicable. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportAvaloniaModuleAttribute : Attribute + public sealed class ExportAvaloniaModuleAttribute : Attribute { public ExportAvaloniaModuleAttribute(string name, Type moduleType) { diff --git a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs index ffa79aa8d6..a2805e69e9 100644 --- a/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs +++ b/src/Avalonia.Controls/Platform/INativeControlHostImpl.cs @@ -29,10 +29,4 @@ namespace Avalonia.Controls.Platform void HideWithSize(Size size); void ShowInBounds(Rect rect); } - - [Unstable] - public interface ITopLevelImplWithNativeControlHost - { - INativeControlHostImpl? NativeControlHost { get; } - } } diff --git a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs index 0658f9211c..d609dd94c8 100644 --- a/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs +++ b/src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs @@ -13,6 +13,6 @@ namespace Avalonia.Platform /// /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. /// - event EventHandler ShutdownRequested; + event EventHandler? ShutdownRequested; } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index bf74e0f8f4..29156f4030 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -51,7 +51,7 @@ namespace Avalonia.Platform /// . /// [Unstable] - public interface ITopLevelImpl : IDisposable + public interface ITopLevelImpl : IOptionalFeatureProvider, IDisposable { /// /// Gets the client size of the toplevel. @@ -111,11 +111,6 @@ namespace Avalonia.Platform /// The toplevel. IRenderer CreateRenderer(IRenderRoot root); - /// - /// Invalidates a rect on the toplevel. - /// - void Invalidate(Rect rect); - /// /// Sets the for the toplevel. /// diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs deleted file mode 100644 index b42040f3c3..0000000000 --- a/src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Metadata; -using Avalonia.Platform; -using Avalonia.Platform.Storage; - -namespace Avalonia.Controls.Platform; - -[Unstable] -public interface ITopLevelImplWithStorageProvider : ITopLevelImpl -{ - public IStorageProvider StorageProvider { get; } -} diff --git a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs b/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs deleted file mode 100644 index a2e426ca08..0000000000 --- a/src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia.Input; -using Avalonia.Input.TextInput; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Controls.Platform -{ - [Unstable] - public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl - { - public ITextInputMethodImpl? TextInputMethod { get; } - } -} diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 149a978c54..3093169f04 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -23,10 +23,4 @@ namespace Avalonia.Controls.Platform { INativeMenuExporter? NativeMenuExporter { get; } } - - [Unstable] - public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl - { - ITopLevelNativeMenuExporter? NativeMenuExporter { get; } - } } diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 8d9d8e0e7b..31b144ce00 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -19,7 +19,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the minimized/maximized state of the window changes. /// - Action WindowStateChanged { get; set; } + Action? WindowStateChanged { get; set; } /// /// Sets the title of the window. @@ -42,7 +42,7 @@ namespace Avalonia.Platform /// /// Called when a disabled window received input. Can be used to activate child windows. /// - Action GotInputWhenDisabled { get; set; } + Action? GotInputWhenDisabled { get; set; } /// /// Enables or disables system window decorations (title bar, buttons, etc) @@ -68,7 +68,7 @@ namespace Avalonia.Platform /// Gets or sets a method called before the underlying implementation is destroyed. /// Return true to prevent the underlying implementation from closing. /// - Func Closing { get; set; } + Func? Closing { get; set; } /// /// Gets a value to indicate if the platform was able to extend client area to non-client area. @@ -78,7 +78,7 @@ namespace Avalonia.Platform /// /// Gets or Sets an action that is called whenever one of the extend client area properties changed. /// - Action ExtendClientAreaToDecorationsChanged { get; set; } + Action? ExtendClientAreaToDecorationsChanged { get; set; } /// /// Gets a flag that indicates if Managed decorations i.e. caption buttons are required. diff --git a/src/Avalonia.Controls/PlatformInhibitionType.cs b/src/Avalonia.Controls/PlatformInhibitionType.cs new file mode 100644 index 0000000000..03e3270e0b --- /dev/null +++ b/src/Avalonia.Controls/PlatformInhibitionType.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// A platform specific behavior that can be inhibited. + /// + public enum PlatformInhibitionType + { + /// + /// When inhibited, prevents the app from being put to sleep or being given a lower priority when not in focus. + /// + AppSleep + } +} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 584dfea97f..be61bb18a1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -534,7 +534,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { _borderRenderer.Render(context, Bounds.Size, LayoutThickness, CornerRadius, Background, BorderBrush, BoxShadow); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e0252feed5..e8eaac7d17 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -3,13 +3,15 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { /// /// Presents items inside an . /// - public class ItemsPresenter : Control, ILogicalScrollable + public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo { /// /// Defines the property. @@ -19,8 +21,37 @@ namespace Avalonia.Controls.Presenters private PanelContainerGenerator? _generator; private ILogicalScrollable? _logicalScrollable; + private IScrollSnapPointsInfo? _scrollSnapPointsInfo; private EventHandler? _scrollInvalidated; + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); + static ItemsPresenter() { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( @@ -83,12 +114,48 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Occurs when the measurements for horizontal snap points change. + /// + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + /// + /// Occurs when the measurements for vertical snap points change. + /// + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + public override sealed void ApplyTemplate() { if (Panel is null && ItemsControl is not null) @@ -100,14 +167,36 @@ namespace Avalonia.Controls.Presenters Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); + _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); + if (_scrollSnapPointsInfo != null) + { + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + if (Panel is VirtualizingPanel v) v.Attach(ItemsControl); else CreateSimplePanelGenerator(); + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = VerticalSnapPointsChangedEvent; + RaiseEvent(e); + }; + + scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = HorizontalSnapPointsChangedEvent; + RaiseEvent(e); + }; + } + _logicalScrollable = Panel as ILogicalScrollable; if (_logicalScrollable is not null) @@ -151,6 +240,16 @@ namespace Avalonia.Controls.Presenters ResetState(); InvalidateMeasure(); } + else if(change.Property == AreHorizontalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + else if (change.Property == AreVerticalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + } } internal void Refresh() @@ -204,5 +303,27 @@ namespace Avalonia.Controls.Presenters } private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment); + } + + return new List(); + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset); + } + + offset = 0; + + return 0; + } } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0cfe4bada1..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Reactive; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -57,6 +58,30 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsTypeProperty = + ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsTypeProperty = + ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = + ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = + ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner(); + /// /// Defines the property. /// @@ -71,10 +96,19 @@ namespace Avalonia.Controls.Presenters private IDisposable? _logicalScrollSubscription; private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; + private Dictionary? _scrollGestureSnapPoints; private List? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; + private bool _areVerticalSnapPointsRegular; + private bool _areHorizontalSnapPointsRegular; + private IReadOnlyList? _horizontalSnapPoints; + private double _horizontalSnapPoint; + private IReadOnlyList? _verticalSnapPoints; + private double _verticalSnapPoint; + private double _verticalSnapPointOffset; + private double _horizontalSnapPointOffset; /// /// Initializes static members of the class. @@ -93,6 +127,7 @@ namespace Avalonia.Controls.Presenters AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -142,6 +177,42 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + /// + /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis. + /// + public SnapPointsType HorizontalSnapPointsType + { + get => GetValue(HorizontalSnapPointsTypeProperty); + set => SetValue(HorizontalSnapPointsTypeProperty, value); + } + + /// + /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis. + /// + public SnapPointsType VerticalSnapPointsType + { + get => GetValue(VerticalSnapPointsTypeProperty); + set => SetValue(VerticalSnapPointsTypeProperty, value); + } + + /// + /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport. + /// + public SnapPointsAlignment HorizontalSnapPointsAlignment + { + get => GetValue(HorizontalSnapPointsAlignmentProperty); + set => SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + /// + /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport. + /// + public SnapPointsAlignment VerticalSnapPointsAlignment + { + get => GetValue(VerticalSnapPointsAlignmentProperty); + set => SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets or sets if scroll chaining is enabled. The default value is true. /// @@ -356,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; @@ -424,6 +496,25 @@ namespace Avalonia.Controls.Presenters } Vector newOffset = new Vector(x, y); + + if (_scrollGestureSnapPoints?.TryGetValue(e.Id, out var snapPoint) == true) + { + double xOffset = x; + double yOffset = y; + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + xOffset = delta.X < 0 ? Math.Max(snapPoint.X, newOffset.X) : Math.Min(snapPoint.X, newOffset.X); + } + + if (VerticalSnapPointsType != SnapPointsType.None) + { + yOffset = delta.Y < 0 ? Math.Max(snapPoint.Y, newOffset.Y) : Math.Min(snapPoint.Y, newOffset.Y); + } + + newOffset = new Vector(xOffset, yOffset); + } + bool offsetChanged = newOffset != Offset; Offset = newOffset; @@ -434,7 +525,65 @@ namespace Avalonia.Controls.Presenters } private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) - => _activeLogicalGestureScrolls?.Remove(e.Id); + { + _activeLogicalGestureScrolls?.Remove(e.Id); + _scrollGestureSnapPoints?.Remove(e.Id); + + Offset = SnapOffset(Offset); + } + + private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) + { + if (Content is not IScrollSnapPointsInfo) + return; + + if (_scrollGestureSnapPoints == null) + _scrollGestureSnapPoints = new Dictionary(); + + var offset = Offset; + + if (HorizontalSnapPointsType != SnapPointsType.None && VerticalSnapPointsType != SnapPointsType.None) + { + return; + } + + double xDistance = 0; + double yDistance = 0; + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + xDistance = HorizontalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.X) : 0; + } + + if (VerticalSnapPointsType != SnapPointsType.None) + { + yDistance = VerticalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.Y) : 0; + } + + offset = new Vector(offset.X + xDistance, offset.Y + yDistance); + + System.Diagnostics.Debug.WriteLine($"{offset}"); + + _scrollGestureSnapPoints.Add(e.Id, SnapOffset(offset)); + + double GetDistance(double speed) + { + var time = Math.Log(ScrollGestureRecognizer.InertialScrollSpeedEnd / Math.Abs(speed)) / Math.Log(ScrollGestureRecognizer.InertialResistance); + + double timeElapsed = 0, distance = 0, step = 0; + + while (timeElapsed <= time) + { + double s = speed * Math.Pow(ScrollGestureRecognizer.InertialResistance, timeElapsed); + distance += (s * step); + + timeElapsed += 0.016f; + step = 0.016f; + } + + return distance; + } + } /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) @@ -471,7 +620,8 @@ namespace Avalonia.Controls.Presenters x = Math.Min(x, Extent.Width - Viewport.Width); } - Vector newOffset = new Vector(x, y); + Vector newOffset = SnapOffset(new Vector(x, y)); + bool offsetChanged = newOffset != Offset; Offset = newOffset; @@ -485,10 +635,36 @@ namespace Avalonia.Controls.Presenters { InvalidateArrange(); } + else if (change.Property == ContentProperty) + { + if (change.OldValue is IScrollSnapPointsInfo oldSnapPointsInfo) + { + oldSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged; + oldSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + } + + if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + } + + UpdateSnapPoints(); + } + else if (change.Property == HorizontalSnapPointsAlignmentProperty || + change.Property == VerticalSnapPointsAlignmentProperty) + { + UpdateSnapPoints(); + } base.OnPropertyChanged(change); } + private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e) + { + UpdateSnapPoints(); + } + private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e) { if (e.TargetObject is not null) @@ -635,5 +811,145 @@ namespace Avalonia.Controls.Presenters bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; return p.HasValue; } + + private void UpdateSnapPoints() + { + if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular; + _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular; + + if (!_areVerticalSnapPointsRegular) + { + _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment); + } + else + { + _verticalSnapPoints = new List(); + _verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset); + + } + + if (!_areHorizontalSnapPointsRegular) + { + _horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment); + } + else + { + _horizontalSnapPoints = new List(); + _horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _horizontalSnapPointOffset); + } + } + else + { + _horizontalSnapPoints = new List(); + _verticalSnapPoints = new List(); + } + } + + private Vector SnapOffset(Vector offset) + { + if(Content is not IScrollSnapPointsInfo) + return offset; + + var diff = GetAlignedDiff(); + + if (VerticalSnapPointsType != SnapPointsType.None) + { + offset = new Vector(offset.X, offset.Y + diff.Y); + double nearestSnapPoint = offset.Y; + + if (_areVerticalSnapPointsRegular) + { + var minSnapPoint = (int)(offset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset; + var maxSnapPoint = minSnapPoint + _verticalSnapPoint; + var midPoint = (minSnapPoint + maxSnapPoint) / 2; + + nearestSnapPoint = offset.Y < midPoint ? minSnapPoint : maxSnapPoint; + } + else if (_verticalSnapPoints != null && _verticalSnapPoints.Count > 0) + { + var higherSnapPoint = FindNearestSnapPoint(_verticalSnapPoints, offset.Y, out var lowerSnapPoint); + var midPoint = (lowerSnapPoint + higherSnapPoint) / 2; + + nearestSnapPoint = offset.Y < midPoint ? lowerSnapPoint : higherSnapPoint; + } + + offset = new Vector(offset.X, nearestSnapPoint - diff.Y); + } + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + offset = new Vector(offset.X + diff.X, offset.Y); + double nearestSnapPoint = offset.X; + + if (_areHorizontalSnapPointsRegular) + { + var minSnapPoint = (int)(offset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset; + var maxSnapPoint = minSnapPoint + _horizontalSnapPoint; + var midPoint = (minSnapPoint + maxSnapPoint) / 2; + + nearestSnapPoint = offset.X < midPoint ? minSnapPoint : maxSnapPoint; + } + else if (_horizontalSnapPoints != null && _horizontalSnapPoints.Count > 0) + { + var higherSnapPoint = FindNearestSnapPoint(_horizontalSnapPoints, offset.X, out var lowerSnapPoint); + var midPoint = (lowerSnapPoint + higherSnapPoint) / 2; + + nearestSnapPoint = offset.X < midPoint ? lowerSnapPoint : higherSnapPoint; + } + + offset = new Vector(nearestSnapPoint - diff.X, offset.Y); + + } + + Vector GetAlignedDiff() + { + var vector = offset; + + switch (VerticalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + vector += new Vector(0, Viewport.Height / 2); + break; + case SnapPointsAlignment.Far: + vector += new Vector(0, Viewport.Height); + break; + } + + switch (HorizontalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + vector += new Vector(Viewport.Width / 2, 0); + break; + case SnapPointsAlignment.Far: + vector += new Vector(Viewport.Width, 0); + break; + } + + return vector - offset; + } + + return offset; + } + + private static double FindNearestSnapPoint(IReadOnlyList snapPoints, double value, out double lowerSnapPoint) + { + var point = snapPoints.BinarySearch(value, Comparer.Default); + + if (point < 0) + { + point = ~point; + + lowerSnapPoint = snapPoints[Math.Max(0, point - 1)]; + } + else + { + lowerSnapPoint = snapPoints[point]; + + point += 1; + } + return snapPoints[Math.Min(point, snapPoints.Count - 1)]; + } } } diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f599511392..bb6b03d59a 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -63,6 +63,15 @@ namespace Avalonia.Controls.Presenters o => o.PreeditText, (o, v) => o.PreeditText = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty CompositionRegionProperty = + AvaloniaProperty.RegisterDirect( + nameof(CompositionRegion), + o => o.CompositionRegion, + (o, v) => o.CompositionRegion = v); + /// /// Defines the property. /// @@ -106,6 +115,7 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; + private TextRange? _compositionRegion; static TextPresenter() { @@ -146,6 +156,12 @@ namespace Avalonia.Controls.Presenters set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); } + public TextRange? CompositionRegion + { + get => _compositionRegion; + set => SetAndRaise(CompositionRegionProperty, ref _compositionRegion, value); + } + /// /// Gets or sets the font family. /// @@ -388,7 +404,7 @@ namespace Avalonia.Controls.Presenters TextLayout.Draw(context, new Point(left, top)); } - public override void Render(DrawingContext context) + public sealed override void Render(DrawingContext context) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; @@ -548,7 +564,20 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if (!string.IsNullOrEmpty(_preeditText)) + if(_compositionRegion != null) + { + var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: foreground, + textDecorations: TextDecorations.Underline)); + + textStyleOverrides = new[] + { + preeditHighlight + }; + + } + else if (!string.IsNullOrEmpty(_preeditText)) { var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, new GenericTextRunProperties(typeface, FontSize, @@ -911,6 +940,7 @@ namespace Avalonia.Controls.Presenters break; } + case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): case nameof(FontStyle): @@ -931,7 +961,6 @@ namespace Avalonia.Controls.Presenters case nameof(PasswordChar): case nameof(RevealPassword): - case nameof(FlowDirection): { InvalidateTextLayout(); diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 49e76d0728..ed3412bb45 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -60,10 +60,9 @@ namespace Avalonia.Controls.Primitives /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(DrawingContext context) + protected internal override void RenderCore(DrawingContext context) { - base.Render(context); - + base.RenderCore(context); int underscore = Text?.IndexOf('_') ?? -1; if (underscore != -1 && ShowAccessKey) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index c9585d50ae..611d57a980 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives /// /// TODO: Need to track position of adorned elements and move the adorner if they move. /// - public class AdornerLayer : Canvas, ICustomSimpleHitTest + public class AdornerLayer : Canvas { /// /// Allows for getting and setting of the adorned element. @@ -34,8 +34,8 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty AdornerProperty = AvaloniaProperty.RegisterAttached("Adorner"); - private static readonly AttachedProperty s_adornedElementInfoProperty = - AvaloniaProperty.RegisterAttached("AdornedElementInfo"); + private static readonly AttachedProperty s_adornedElementInfoProperty = + AvaloniaProperty.RegisterAttached("AdornedElementInfo"); private static readonly AttachedProperty s_savedAdornerLayerProperty = AvaloniaProperty.RegisterAttached("SavedAdornerLayer"); @@ -159,8 +159,8 @@ namespace Avalonia.Controls.Primitives return; } - AdornerLayer.SetAdornedElement(adorner, visual); - AdornerLayer.SetIsClipEnabled(adorner, false); + SetAdornedElement(adorner, visual); + SetIsClipEnabled(adorner, false); ((ISetLogicalParent) adorner).SetParent(visual); layer.Children.Add(adorner); @@ -177,6 +177,7 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent) adorner).SetParent(null); } + /// protected override Size MeasureOverride(Size availableSize) { foreach (var child in Children) @@ -199,6 +200,7 @@ namespace Avalonia.Controls.Primitives return default; } + /// protected override Size ArrangeOverride(Size finalSize) { foreach (var child in Children) @@ -217,7 +219,7 @@ namespace Avalonia.Controls.Primitives } else { - ArrangeChild((Control) child, finalSize); + ArrangeChild(child, finalSize); } } } @@ -277,8 +279,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) @@ -305,17 +310,9 @@ namespace Avalonia.Controls.Primitives info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity); InvalidateMeasure(); }); - else - info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => - { - info.Bounds = x; - InvalidateMeasure(); - }); } } - public bool HitTest(Point point) => Children.HitTestCustom(point); - private class AdornedElementInfo { public IDisposable? Subscription { get; set; } diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs index 20b3b849a6..74b5beecad 100644 --- a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { - public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest + public class ChromeOverlayLayer : Panel { public static Panel? GetOverlayLayer(Visual visual) { @@ -26,7 +26,5 @@ namespace Avalonia.Controls.Primitives { base.Children.Add(c); } - - public bool HitTest(Point point) => Children.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs new file mode 100644 index 0000000000..7b33db0df2 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Avalonia.Interactivity; +using Avalonia.Layout; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Describes snap point behavior for objects that contain and present items. + /// + public interface IScrollSnapPointsInfo + { + /// + /// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other. + /// + bool AreHorizontalSnapPointsRegular { get; set; } + + /// + /// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other. + /// + bool AreVerticalSnapPointsRegular { get; set; } + + /// + /// Returns the set of distances between irregular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// The read-only collection of snap point distances. Returns an empty collection when no snap points are present. + IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment); + + /// + /// Gets the distance between regular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// Out parameter. The offset of the first snap point. + /// The distance between the equidistant snap points. Returns 0 when no snap points are present. + double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset); + + /// + /// Occurs when the measurements for horizontal snap points change. + /// + event EventHandler HorizontalSnapPointsChanged; + + /// + /// Occurs when the measurements for vertical snap points change. + /// + event EventHandler VerticalSnapPointsChanged; + } +} diff --git a/src/Avalonia.Controls/Primitives/OverlayLayer.cs b/src/Avalonia.Controls/Primitives/OverlayLayer.cs index 91136cb295..76b56f3a11 100644 --- a/src/Avalonia.Controls/Primitives/OverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/OverlayLayer.cs @@ -4,7 +4,7 @@ using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { - public class OverlayLayer : Canvas, ICustomSimpleHitTest + public class OverlayLayer : Canvas { public Size AvailableSize { get; private set; } public static OverlayLayer? GetOverlayLayer(Visual visual) @@ -22,8 +22,6 @@ namespace Avalonia.Controls.Primitives return null; } - public bool HitTest(Point point) => Children.HitTestCustom(point); - protected override Size MeasureOverride(Size availableSize) { foreach (Control child in Children) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e265f4eb6a..e16633483b 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Reactive; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; @@ -18,8 +17,8 @@ namespace Avalonia.Controls.Primitives PopupRoot.TransformProperty.AddOwner(); private readonly OverlayLayer _overlayLayer; - private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); - private ManagedPopupPositioner _positioner; + private readonly ManagedPopupPositioner _positioner; + private PopupPositionerParameters _positionerParameters; private Point _lastRequestedPosition; private bool _shown; @@ -29,13 +28,16 @@ namespace Avalonia.Controls.Primitives _positioner = new ManagedPopupPositioner(this); } + /// public void SetChild(Control? control) { Content = control; } + /// public Visual? HostedVisualTreeRoot => null; + /// public Transform? Transform { get => GetValue(TransformProperty); @@ -48,23 +50,27 @@ namespace Avalonia.Controls.Primitives set { /* Not currently supported in overlay popups */ } } - protected internal override Interactive? InteractiveParent => Parent; + /// + protected internal override Interactive? InteractiveParent => (Interactive?)VisualParent; + /// public void Dispose() => Hide(); - + /// public void Show() { _overlayLayer.Children.Add(this); _shown = true; } + /// public void Hide() { _overlayLayer.Children.Remove(this); _shown = false; } + /// public void ConfigurePosition(Visual target, PlacementMode placement, Point offset, PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None, PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All, @@ -75,6 +81,7 @@ namespace Avalonia.Controls.Primitives UpdatePosition(); } + /// protected override Size ArrangeOverride(Size finalSize) { if (_positionerParameters.Size != finalSize) @@ -123,17 +130,18 @@ namespace Avalonia.Controls.Primitives public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) { - var platform = TopLevel.GetTopLevel(target)?.PlatformImpl?.CreatePopup(); - if (platform != null) - return new PopupRoot((TopLevel)target.GetVisualRoot()!, platform, dependencyResolver); - - var overlayLayer = OverlayLayer.GetOverlayLayer(target); - if (overlayLayer == null) - throw new InvalidOperationException( - "Unable to create IPopupImpl and no overlay layer is found for the target control"); + if (TopLevel.GetTopLevel(target) is { } topLevel && topLevel.PlatformImpl?.CreatePopup() is { } popupImpl) + { + return new PopupRoot(topLevel, popupImpl, dependencyResolver); + } + if (OverlayLayer.GetOverlayLayer(target) is { } overlayLayer) + { + return new OverlayPopupHost(overlayLayer); + } - return new OverlayPopupHost(overlayLayer); + throw new InvalidOperationException( + "Unable to create IPopupImpl and no overlay layer is found for the target control"); } } } diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 7cac12eabe..9d443d9289 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; @@ -119,7 +120,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost)); - private bool _isOpenRequested = false; + private bool _isOpenRequested; private bool _isOpen; private bool _ignoreIsOpenChanged; private PopupOpenState? _openState; @@ -376,19 +377,25 @@ namespace Avalonia.Controls.Primitives popupHost.SetChild(Child); ((ISetLogicalParent)popupHost).SetParent(this); - if (InheritsTransform && placementTarget is Control c) + if (InheritsTransform) { - SubscribeToEventHandler>( - c, - PlacementTargetPropertyChanged, - (x, handler) => x.PropertyChanged += handler, - (x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup); + TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged) + .DisposeWith(handlerCleanup); } else { popupHost.Transform = null; } + if (popupHost is PopupRoot topLevelPopup) + { + topLevelPopup + .Bind( + ThemeVariantScope.ActualThemeVariantProperty, + this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + .DisposeWith(handlerCleanup); + } + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, @@ -520,6 +527,7 @@ namespace Avalonia.Controls.Primitives Close(); } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -581,7 +589,7 @@ namespace Avalonia.Controls.Primitives var scaleX = 1.0; var scaleY = 1.0; - if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m) + if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is { } m) { scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12); @@ -625,6 +633,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override AutomationPeer OnCreateAutomationPeer() { return new PopupAutomationPeer(this); @@ -725,7 +734,7 @@ namespace Avalonia.Controls.Primitives while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible)) { - e = e.Parent; + e = e.VisualParent as Control; } if (e is object) @@ -852,7 +861,7 @@ namespace Avalonia.Controls.Primitives var popupHost = _openState.PopupHost; - return popupHost != null && ((Visual)popupHost).IsVisualAncestorOf(visual); + return ((Visual)popupHost).IsVisualAncestorOf(visual); } public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false; @@ -872,13 +881,11 @@ namespace Avalonia.Controls.Primitives Close(); } } - - private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + + private void PlacementTargetTransformChanged(Visual v, Matrix? matrix) { - if (_openState is not null && e.Property == Visual.TransformedBoundsProperty) - { + if (_openState is not null) UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget); - } } private void WindowLostFocus() diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 57ec864cad..b3436d4176 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -72,12 +72,12 @@ namespace Avalonia.Controls.Primitives /// /// Popup events are passed to their parent window. This facilitates this. /// - protected internal override Interactive? InteractiveParent => Parent; + protected internal override Interactive? InteractiveParent => (Interactive?)Parent; /// /// Gets the control that is hosting the popup root. /// - Visual? IHostedVisualTreeRoot.Host => Parent; + Visual? IHostedVisualTreeRoot.Host => VisualParent; /// /// Gets the styling parent of the popup root. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 5210362505..2ee32b0dda 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -3,16 +3,16 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Xml.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Selection; +using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -66,6 +66,19 @@ namespace Avalonia.Controls.Primitives (o, v) => o.SelectedItem = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); + /// + /// Defines the property + /// + public static readonly StyledProperty SelectedValueProperty = + AvaloniaProperty.Register(nameof(SelectedValue), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property + /// + public static readonly StyledProperty SelectedValueBindingProperty = + AvaloniaProperty.Register(nameof(SelectedValueBinding)); + /// /// Defines the property. /// @@ -129,6 +142,8 @@ namespace Avalonia.Controls.Primitives private bool _ignoreContainerSelectionChanged; private UpdateState? _updateState; private bool _hasScrolledToSelectedItem; + private BindingHelper? _bindingHelper; + private bool _isSelectionChangeActive; /// /// Initializes static members of the class. @@ -143,8 +158,8 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler? SelectionChanged { - add { AddHandler(SelectionChangedEvent, value); } - remove { RemoveHandler(SelectionChangedEvent, value); } + add => AddHandler(SelectionChangedEvent, value); + remove => RemoveHandler(SelectionChangedEvent, value); } /// @@ -152,8 +167,8 @@ namespace Avalonia.Controls.Primitives /// public bool AutoScrollToSelectedItem { - get { return GetValue(AutoScrollToSelectedItemProperty); } - set { SetValue(AutoScrollToSelectedItemProperty, value); } + get => GetValue(AutoScrollToSelectedItemProperty); + set => SetValue(AutoScrollToSelectedItemProperty, value); } /// @@ -209,6 +224,28 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Gets the instance used to obtain the + /// property + /// + [AssignBinding] + [InheritDataTypeFromItems(nameof(Items))] + public IBinding? SelectedValueBinding + { + get => GetValue(SelectedValueBindingProperty); + set => SetValue(SelectedValueBindingProperty, value); + } + + /// + /// Gets or sets the value of the selected item, obtained using + /// + /// + public object? SelectedValue + { + get => GetValue(SelectedValueProperty); + set => SetValue(SelectedValueProperty, value); + } + /// /// Gets or sets the selected items. /// @@ -255,6 +292,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the model that holds the current selection. /// + [AllowNull] protected ISelectionModel Selection { get @@ -307,10 +345,7 @@ namespace Avalonia.Controls.Primitives if (_oldSelectedItems != SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } } @@ -322,8 +357,8 @@ namespace Avalonia.Controls.Primitives /// public bool IsTextSearchEnabled { - get { return GetValue(IsTextSearchEnabledProperty); } - set { SetValue(IsTextSearchEnabledProperty, value); } + get => GetValue(IsTextSearchEnabledProperty); + set => SetValue(IsTextSearchEnabledProperty, value); } /// @@ -332,8 +367,8 @@ namespace Avalonia.Controls.Primitives /// public bool WrapSelection { - get { return GetValue(WrapSelectionProperty); } - set { SetValue(WrapSelectionProperty, value); } + get => GetValue(WrapSelectionProperty); + set => SetValue(WrapSelectionProperty, value); } /// @@ -345,8 +380,8 @@ namespace Avalonia.Controls.Primitives /// protected SelectionMode SelectionMode { - get { return GetValue(SelectionModeProperty); } - set { SetValue(SelectionModeProperty, value); } + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); } /// @@ -399,6 +434,7 @@ namespace Avalonia.Controls.Primitives return null; } + /// protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.ItemsCollectionChanged(sender!, e); @@ -409,12 +445,14 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); AutoScrollToSelectedItemIfNecessary(); } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -431,6 +469,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) { base.PrepareContainerForItemOverride(element, item, index); @@ -447,12 +486,14 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex) { base.ContainerIndexChangedOverride(container, oldIndex, newIndex); MarkContainerSelected(container, Selection.IsSelected(newIndex)); } + /// protected internal override void ClearContainerForItemOverride(Control element) { base.ClearContainerForItemOverride(element); @@ -463,7 +504,7 @@ namespace Avalonia.Controls.Primitives KeyboardNavigation.SetTabOnceActiveElement(panel, null); } - if (element is ISelectable selectable) + if (element is ISelectable) MarkContainerSelected(element, false); } @@ -498,7 +539,8 @@ namespace Avalonia.Controls.Primitives DataValidationErrors.SetError(this, error); } } - + + /// protected override void OnInitialized() { base.OnInitialized(); @@ -509,6 +551,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnTextInput(TextInputEventArgs e) { if (!e.Handled) @@ -551,6 +594,7 @@ namespace Avalonia.Controls.Primitives base.OnTextInput(e); } + /// protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); @@ -582,6 +626,7 @@ namespace Avalonia.Controls.Primitives } } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -592,7 +637,7 @@ namespace Avalonia.Controls.Primitives } if (change.Property == ItemsProperty && _updateState is null && _selection is object) { - var newValue = change.GetNewValue(); + var newValue = change.GetNewValue(); _selection.Source = newValue; if (newValue is null) @@ -609,6 +654,60 @@ namespace Avalonia.Controls.Primitives { WrapFocus = WrapSelection; } + else if (change.Property == SelectedValueProperty) + { + if (_isSelectionChangeActive) + return; + + if (_updateState is not null) + { + _updateState.SelectedValue = change.NewValue; + return; + } + + SelectItemWithValue(change.NewValue); + } + else if (change.Property == SelectedValueBindingProperty) + { + var idx = SelectedIndex; + + // If no selection is active, don't do anything as SelectedValue is already null + if (idx == -1) + { + return; + } + + var value = change.GetNewValue(); + if (value is null) + { + // Clearing SelectedValueBinding makes the SelectedValue the item itself + SelectedValue = SelectedItem; + return; + } + + var selectedItem = SelectedItem; + + try + { + _isSelectionChangeActive = true; + + if (_bindingHelper is null) + { + _bindingHelper = new BindingHelper(value); + } + else + { + _bindingHelper.UpdateBinding(value); + } + + // Re-evaluate SelectedValue with the new binding + SelectedValue = _bindingHelper.Evaluate(selectedItem); + } + finally + { + _isSelectionChangeActive = false; + } + } } /// @@ -695,7 +794,7 @@ namespace Avalonia.Controls.Primitives { if (multi) { - if (Selection.IsSelected(index) == true) + if (Selection.IsSelected(index)) { Selection.Deselect(index); } @@ -716,12 +815,10 @@ namespace Avalonia.Controls.Primitives Selection.Select(index); } - if (Presenter?.Panel != null) + if (Presenter?.Panel is { } panel) { var container = ContainerFromIndex(index); - KeyboardNavigation.SetTabOnceActiveElement( - (InputElement)Presenter.Panel, - container); + KeyboardNavigation.SetTabOnceActiveElement(panel, container); } } @@ -809,12 +906,13 @@ namespace Avalonia.Controls.Primitives else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } + else if (e.PropertyName == nameof(ISelectionModel.Source)) + { + ClearValue(SelectedValueProperty); + } } /// @@ -845,6 +943,11 @@ namespace Avalonia.Controls.Primitives Mark(i, false); } + if (!_isSelectionChangeActive) + { + UpdateSelectedValueFromItem(); + } + var route = BuildEventRoute(SelectionChangedEvent); if (route.HasHandlers) @@ -871,6 +974,109 @@ namespace Avalonia.Controls.Primitives } } + private void SelectItemWithValue(object? value) + { + if (ItemCount == 0 || _isSelectionChangeActive) + return; + + try + { + _isSelectionChangeActive = true; + var si = FindItemWithValue(value); + if (si != AvaloniaProperty.UnsetValue) + { + SelectedItem = si; + } + else + { + SelectedItem = null; + } + } + finally + { + _isSelectionChangeActive = false; + } + } + + private object FindItemWithValue(object? value) + { + if (ItemCount == 0 || value is null) + { + return AvaloniaProperty.UnsetValue; + } + + var items = Items; + var binding = SelectedValueBinding; + + if (binding is null) + { + // No SelectedValueBinding set, SelectedValue is the item itself + // Still verify the value passed in is in the Items list + var index = items!.IndexOf(value); + + if (index >= 0) + { + return value; + } + else + { + return AvaloniaProperty.UnsetValue; + } + } + + _bindingHelper ??= new BindingHelper(binding); + + // Matching UWP behavior, if duplicates are present, return the first item matching + // the SelectedValue provided + foreach (var item in items!) + { + var itemValue = _bindingHelper.Evaluate(item); + + if (itemValue.Equals(value)) + { + return item; + } + } + + return AvaloniaProperty.UnsetValue; + } + + private void UpdateSelectedValueFromItem() + { + if (_isSelectionChangeActive) + return; + + var binding = SelectedValueBinding; + var item = SelectedItem; + + if (binding is null || item is null) + { + // No SelectedValueBinding, SelectedValue is Item itself + try + { + _isSelectionChangeActive = true; + SelectedValue = item; + } + finally + { + _isSelectionChangeActive = false; + } + return; + } + + _bindingHelper ??= new BindingHelper(binding); + + try + { + _isSelectionChangeActive = true; + SelectedValue = _bindingHelper.Evaluate(item); + } + finally + { + _isSelectionChangeActive = false; + } + } + private void AutoScrollToSelectedItemIfNecessary() { if (AutoScrollToSelectedItem && @@ -940,7 +1146,7 @@ namespace Avalonia.Controls.Primitives private void UpdateContainerSelection() { - if (Presenter?.Panel is Panel panel) + if (Presenter?.Panel is { } panel) { foreach (var container in panel.Children) { @@ -1037,6 +1243,13 @@ namespace Avalonia.Controls.Primitives Selection.Clear(); } + if (state.SelectedValue.HasValue) + { + var item = FindItemWithValue(state.SelectedValue.Value); + if (item != AvaloniaProperty.UnsetValue) + state.SelectedItem = item; + } + if (state.SelectedIndex.HasValue) { SelectedIndex = state.SelectedIndex.Value; @@ -1098,6 +1311,7 @@ namespace Avalonia.Controls.Primitives { private Optional _selectedIndex; private Optional _selectedItem; + private Optional _selectedValue; public int UpdateCount { get; set; } public Optional Selection { get; set; } @@ -1122,6 +1336,54 @@ namespace Avalonia.Controls.Primitives _selectedIndex = default; } } + + public Optional SelectedValue + { + get => _selectedValue; + set + { + _selectedValue = value; + } + } + } + + /// + /// Helper class for evaluating a binding from an Item and IBinding instance + /// + private class BindingHelper : StyledElement + { + public BindingHelper(IBinding binding) + { + UpdateBinding(binding); + } + + public static readonly StyledProperty ValueProperty = + AvaloniaProperty.Register("Value"); + + public object Evaluate(object? dataContext) + { + dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext)); + + // Only update the DataContext if necessary + if (!dataContext.Equals(DataContext)) + DataContext = dataContext; + + return GetValue(ValueProperty); + } + + public void UpdateBinding(IBinding binding) + { + _lastBinding = binding; + var ib = binding.Initiate(this, ValueProperty); + if (ib is null) + { + throw new InvalidOperationException("Unable to create binding"); + } + + BindingOperations.Apply(this, ValueProperty, ib, null); + } + + private IBinding? _lastBinding; } } } diff --git a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs new file mode 100644 index 0000000000..77b93c50a0 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// Specify options for snap point alignment relative to an edge. Which edge depends on the orientation of the object where the alignment is applied + /// + public enum SnapPointsAlignment + { + /// + /// Use snap points grouped closer to the orientation edge. + /// + Near, + + /// + /// Use snap points that are centered in the orientation. + /// + Center, + + /// + /// Use snap points grouped farther from the orientation edge. + /// + Far + } +} diff --git a/src/Avalonia.Controls/Primitives/SnapPointsType.cs b/src/Avalonia.Controls/Primitives/SnapPointsType.cs new file mode 100644 index 0000000000..130fb85f77 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/SnapPointsType.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// Specify how panning snap points are processed for gesture input. + /// + public enum SnapPointsType + { + /// + /// No snapping behavior. + /// + None, + + /// + /// Content always stops at the snap point closest to where inertia would naturally stop along the direction of inertia. + /// + Mandatory, + + /// + /// Content always stops at the snap point closest to the release point along the direction of inertia. + /// + MandatorySingle + } +} diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 9a684c4534..d8874832bd 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -290,12 +290,6 @@ namespace Avalonia.Controls.Primitives ApplyTemplatedParent(child, this); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); - - // Existing code kinda expect to see a NameScope even if it's empty - if (nameScope == null) - { - nameScope = new NameScope(); - } var e = new TemplateAppliedEventArgs(nameScope); OnApplyTemplate(e); @@ -320,6 +314,7 @@ namespace Avalonia.Controls.Primitives return this; } + /// protected sealed override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e) { var count = VisualChildren.Count; diff --git a/src/Avalonia.Controls/Primitives/TextSearch.cs b/src/Avalonia.Controls/Primitives/TextSearch.cs index 949532cb16..962fba361e 100644 --- a/src/Avalonia.Controls/Primitives/TextSearch.cs +++ b/src/Avalonia.Controls/Primitives/TextSearch.cs @@ -11,15 +11,15 @@ namespace Avalonia.Controls.Primitives /// Defines the Text attached property. /// This text will be considered during text search in (such as ) /// - public static readonly AttachedProperty TextProperty - = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); + public static readonly AttachedProperty TextProperty + = AvaloniaProperty.RegisterAttached("Text", typeof(TextSearch)); /// /// Sets the for a control. /// /// The control /// The search text to set - public static void SetText(Control control, string text) + public static void SetText(Control control, string? text) { control.SetValue(TextProperty, text); } @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives /// /// The control /// The property value - public static string GetText(Control control) + public static string? GetText(Control control) { return control.GetValue(TextProperty); } diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index dfb436a55e..158c5d875b 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.Primitives nameof(IsChecked), o => o.IsChecked, (o, v) => o.IsChecked = v, - unsetValue: null, + unsetValue: false, defaultBindingMode: BindingMode.TwoWay); /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 14ec7a2849..9e8d1478fa 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -5,7 +5,6 @@ using System; using Avalonia.Controls.Metadata; -using Avalonia.Data; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Metadata; @@ -31,14 +30,14 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty OrientationProperty = ScrollBar.OrientationProperty.AddOwner(); - public static readonly StyledProperty ThumbProperty = - AvaloniaProperty.Register(nameof(Thumb)); + public static readonly StyledProperty ThumbProperty = + AvaloniaProperty.Register(nameof(Thumb)); - public static readonly StyledProperty