diff --git a/.editorconfig b/.editorconfig
index eac5870f96..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
@@ -145,10 +146,14 @@ 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
@@ -207,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 2f034bd083..1d182b1357 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -8,13 +8,14 @@
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
+ "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
- "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"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",
@@ -41,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",
diff --git a/Avalonia.sln b/Avalonia.sln
index ce9a37a3ce..1e8ee85ffb 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -231,7 +231,18 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
+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
@@ -548,6 +559,18 @@ Global
{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
@@ -613,6 +636,8 @@ Global
{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/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/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/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 @@
+[](https://avaloniaui.net/xpf)
+
[](https://t.me/Avalonia)
[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) []( https://aka.ms/dotnet-discord) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
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/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 83776ec2c1..3681298a72 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -14,8 +14,8 @@
-
-
+
+
@@ -168,6 +168,9 @@
+
+
+
@@ -201,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;
}
};
@@ -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/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
index 47753f56b6..fc3ad9b895 100644
--- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
+++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml
@@ -15,11 +15,11 @@
Spacing="16">
A simple DatePicker
-
-
+
@@ -31,7 +31,7 @@
-
@@ -42,12 +42,12 @@
A DatePicker with day formatted and year hidden.
-
-
+
@@ -58,15 +58,15 @@
-
+
A simple TimePicker.
-
-
+
@@ -77,7 +77,7 @@
-
@@ -88,11 +88,11 @@
A TimePicker with minute increments specified.
-
-
+
@@ -105,11 +105,11 @@
A TimePicker using a 12-hour clock.
-
-
+
@@ -122,11 +122,11 @@
A TimePicker using a 24-hour clock.
-
-
+
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index f7b020678d..e24860e3e1 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
@@ -40,7 +40,7 @@ namespace ControlCatalog.Pages
if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum))
{
- lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum);
+ lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum);
}
else
{
@@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
if (folderLink is not null)
{
- lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink);
+ lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink);
}
}
};
@@ -82,7 +82,13 @@ namespace ControlCatalog.Pages
return new List
{
FilePickerFileTypes.All,
- FilePickerFileTypes.TextPlain
+ FilePickerFileTypes.TextPlain,
+ new("Binary Log")
+ {
+ Patterns = new[] { "*.binlog", "*.buildlog" },
+ MimeTypes = new[] { "application/binlog", "application/buildlog" },
+ AppleUniformTypeIdentifiers = new []{ "public.data" }
+ }
};
}
@@ -142,7 +148,7 @@ namespace ControlCatalog.Pages
}
else
{
- SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
+ SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result };
resultsVisible.IsVisible = true;
}
@@ -223,7 +229,7 @@ namespace ControlCatalog.Pages
ShowOverwritePrompt = false
});
- if (file is not null && file.CanOpenWrite)
+ if (file is not null)
{
// Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER
@@ -275,7 +281,7 @@ namespace ControlCatalog.Pages
{
ignoreTextChanged = true;
lastSelectedDirectory = folder;
- currentFolderBox.Text = folder?.Path.LocalPath;
+ currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString();
ignoreTextChanged = false;
}
async Task SetPickerResult(IReadOnlyCollection? items)
@@ -298,31 +304,26 @@ namespace ControlCatalog.Pages
if (item is IStorageFile file)
{
resultText += @$"
- CanOpenRead: {file.CanOpenRead}
- CanOpenWrite: {file.CanOpenWrite}
Content:
";
- if (file.CanOpenRead)
- {
#if NET6_0_OR_GREATER
- await using var stream = await file.OpenReadAsync();
+ await using var stream = await file.OpenReadAsync();
#else
- using var stream = await file.OpenReadAsync();
+ using var stream = await file.OpenReadAsync();
#endif
- using var reader = new System.IO.StreamReader(stream);
+ using var reader = new System.IO.StreamReader(stream);
- // 4GB file test, shouldn't load more than 10000 chars into a memory.
- const int length = 10000;
- var buffer = ArrayPool.Shared.Rent(length);
- try
- {
- var charsRead = await reader.ReadAsync(buffer, 0, length);
- resultText += new string(buffer, 0, charsRead);
- }
- finally
- {
- ArrayPool.Shared.Return(buffer);
- }
+ // 4GB file test, shouldn't load more than 10000 chars into a memory.
+ const int length = 10000;
+ var buffer = ArrayPool.Shared.Rent(length);
+ try
+ {
+ var charsRead = await reader.ReadAsync(buffer, 0, length);
+ resultText += new string(buffer, 0, charsRead);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
}
}
diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
index c4d0bc3e67..54aa9d1b67 100644
--- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml
+++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml
@@ -26,31 +26,31 @@
-
-
+
-
-
+
-
-
@@ -70,7 +70,7 @@
-
+
@@ -78,21 +78,21 @@
-
-
+
-
@@ -215,7 +215,7 @@
-
diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs
index 0bb8f38219..cc4429f414 100644
--- a/samples/ControlCatalog/Pages/GesturePage.cs
+++ b/samples/ControlCatalog/Pages/GesturePage.cs
@@ -70,7 +70,6 @@ namespace ControlCatalog.Pages
_currentScale = 1;
Vector3 currentOffset = default;
- bool isZooming = false;
CompositionVisual? compositionVisual = null;
diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
index 5ca4ca9bdd..6bf29765f4 100644
--- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
+++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
@@ -62,7 +62,7 @@
-
+
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/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/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/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/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 693a26f3bd..e511ed9a8b 100644
--- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
@@ -28,6 +28,7 @@ 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
{
@@ -410,159 +411,73 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
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 override IEditable Editable => _editable;
- public string ComposingText { get; internal set; }
-
- public ComposingRegion? ComposingRegion { get; internal set; }
-
- 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();
- }
-
- 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);
+ var ret = base.SetComposingText(text, newCursorPosition);
- var end = System.Math.Min(start + length - 1, SurroundingText.CursorOffset);
+ InputEditable.RaiseCompositionChanged();
- 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);
+ _editable.BeginBatchEdit();
- //System.Diagnostics.Debug.WriteLine($"Text After: {text}");
-
- 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 9838bb06c8..9d6dd46d0e 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
@@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{
}
-
- public bool CanOpenRead => true;
-
- public bool CanOpenWrite => true;
-
+
public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream"));
diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
index f611f50164..e35bde0acd 100644
--- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
+++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
@@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider
return Task.FromResult(new AndroidStorageFolder(_activity, uri, false));
}
- public async Task TryGetFileFromPath(Uri filePath)
+ public async Task TryGetFileFromPathAsync(Uri filePath)
{
if (filePath is null)
{
@@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFile(_activity, androidUri);
}
- public async Task TryGetFolderFromPath(Uri folderPath)
+ public async Task TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath is null)
{
@@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFolder(_activity, androidUri, false);
}
- public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
+ public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var dirCode = wellKnownFolder switch
{
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/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/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 1946d4ba5c..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);
}
///
@@ -152,7 +152,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- _values?.ClearLocalValue(property);
+ _values.ClearValue(property);
}
///
@@ -242,7 +242,14 @@ namespace Avalonia
return registered.InvokeGetter(this);
}
- ///
+ ///
+ /// 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));
@@ -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);
}
///
@@ -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.
///
@@ -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/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 5db4d81f03..45ab293a89 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -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
[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/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 0ec7152359..2966ceee8d 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -166,7 +166,7 @@ namespace Avalonia.Media
///
public Point BaselineOrigin
{
- get => _baselineOrigin ?? default;
+ get => PlatformImpl.Item.BaselineOrigin;
set => Set(ref _baselineOrigin, value);
}
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/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/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/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/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
index 9b443391c5..0b625080e3 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
{
@@ -31,11 +30,11 @@ namespace Avalonia.Media.Immutable
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 dc9e5cb907..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); }
@@ -220,7 +220,7 @@ namespace Avalonia.Media
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/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 efcd866bbc..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,14 +32,9 @@ 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();
@@ -107,7 +100,8 @@ 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.GlyphInfos = shapedBuffer.GlyphInfos;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
index 82cf3297fd..b4734d702b 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
@@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span;
- if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
+ if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count))
{
- if (script == Script.Common && previousGlyphTypeface is not null)
- {
- if (TryGetShapeableLength(textSpan, previousGlyphTypeface, 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(defaultTypeface),
biDiLevel);
}
if (previousGlyphTypeface is not null)
{
- if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
+ if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count))
{
return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
@@ -127,14 +118,17 @@ namespace Avalonia.Media.TextFormatting
fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out var fallbackTypeface);
-
- var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
-
- if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
+
+ if (matchFound)
{
- //Fallback found
- return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
- 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
@@ -160,17 +154,15 @@ namespace Avalonia.Media.TextFormatting
/// The typeface that is used to find matching characters.
/// The default typeface.
/// The shapeable length.
- ///
///
internal static bool TryGetShapeableLength(
ReadOnlySpan text,
IGlyphTypeface glyphTypeface,
IGlyphTypeface? defaultGlyphTypeface,
- out int length,
- out Script script)
+ out int length)
{
length = 0;
- script = Script.Unknown;
+ var script = Script.Unknown;
if (text.IsEmpty)
{
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 7505b9ccdd..7f74f49982 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -2,7 +2,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@@ -19,71 +18,63 @@ namespace Avalonia.Media.TextFormatting
[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 objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
- var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
- out var textEndOfLine, out var textSourceLength);
+ // 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)
+ {
+ return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth,
+ paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool);
+ }
+ RentedList? fetchedRuns = null;
RentedList? shapedTextRuns = null;
-
try
{
- if (previousLineBreak?.RemainingRuns is { } remainingRuns)
+ fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
+ out var textSourceLength);
+
+ if (fetchedRuns.Count == 0)
{
- resolvedFlowDirection = previousLineBreak.FlowDirection;
- textRuns = remainingRuns;
- nextLineBreak = previousLineBreak;
- shapedTextRuns = null;
+ return null;
}
- else
- {
- shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
- out resolvedFlowDirection);
- textRuns = shapedTextRuns;
- if (nextLineBreak == null && textEndOfLine != null)
- {
- nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
- }
- }
+ shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
+ out var resolvedFlowDirection);
- TextLineImpl textLine;
+ if (nextLineBreak == null && textEndOfLine != null)
+ {
+ nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+ }
- switch (textWrapping)
+ switch (paragraphProperties.TextWrapping)
{
case TextWrapping.NoWrap:
{
- // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
- // which already uses an array: ToArray() won't ever be called in this case
- var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
-
- textLine = new TextLineImpl(textRunArray, 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:
{
- textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
- paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
- break;
+ return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
+ paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
}
default:
- throw new ArgumentOutOfRangeException(nameof(textWrapping));
+ throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping));
}
-
- return textLine;
}
finally
{
@@ -108,15 +99,16 @@ namespace Avalonia.Media.TextFormatting
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 firstCount = currentRunLength >= 1 ? i + 1 : i;
if (firstCount > 1)
{
@@ -128,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 ? 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++)
{
@@ -249,49 +241,49 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case UnshapedTextRun shapeableRun:
- {
- groupedRuns.Clear();
- groupedRuns.Add(shapeableRun);
+ {
+ groupedRuns.Clear();
+ groupedRuns.Add(shapeableRun);
- var text = shapeableRun.Text;
- var properties = shapeableRun.Properties;
+ var text = shapeableRun.Text;
+ var properties = shapeableRun.Properties;
- while (index + 1 < processedRuns.Count)
- {
- if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
+ 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(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;
}
-
- 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);
+ {
+ shapedRuns.Add(currentRun);
- break;
- }
+ break;
+ }
}
}
}
@@ -504,16 +496,7 @@ namespace Avalonia.Media.TextFormatting
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)
{
@@ -653,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
///
/// The empty text line.
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
- TextParagraphProperties paragraphProperties, FontManager fontManager)
+ TextParagraphProperties paragraphProperties)
{
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
@@ -675,21 +658,21 @@ namespace Avalonia.Media.TextFormatting
/// 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 font manager to use.
/// The wrapped text line.
- private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex,
- double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
- TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
+ private static TextLineImpl PerformTextWrapping(List textRuns, bool canReuseTextRunList,
+ int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties,
+ FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
{
if (textRuns.Count == 0)
{
- return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
+ return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -712,7 +695,7 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextRun:
- {
+ {
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext(out var lineBreak))
@@ -754,7 +737,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count)
+ while (lineBreaker.MoveNext(out lineBreak))
{
currentPosition += lineBreak.PositionWrap;
@@ -780,6 +763,11 @@ namespace Avalonia.Media.TextFormatting
currentPosition = currentLength + lineBreak.PositionWrap;
}
+ if (currentPosition == 0 && measuredLength > 0)
+ {
+ currentPosition = measuredLength;
+ }
+
breakFound = true;
break;
@@ -819,13 +807,37 @@ namespace Avalonia.Media.TextFormatting
try
{
- var textLineBreak = postSplitRuns?.Count > 0 ?
- new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
- null;
+ TextLineBreak? textLineBreak;
+ if (postSplitRuns?.Count > 0)
+ {
+ List remainingRuns;
+
+ // 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 (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+ for (var i = 0; i < postSplitRuns.Count; ++i)
+ {
+ remainingRuns.Add(postSplitRuns[i]);
+ }
+
+ textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns);
+ }
+ else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine)
{
- textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
+ textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+ }
+ else
+ {
+ textLineBreak = null;
}
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
@@ -833,6 +845,7 @@ namespace Avalonia.Media.TextFormatting
textLineBreak);
textLine.FinalizeLine();
+
return textLine;
}
finally
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 4923cdbe32..4dbc472133 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting
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;
@@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting
{
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,9 +438,11 @@ 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;
@@ -427,12 +451,10 @@ namespace Avalonia.Media.TextFormatting
private TextLine[] CreateTextLines()
{
var objectPool = FormattingObjectPool.Instance;
- var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
- var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
- fontManager);
+ var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
Bounds = new Rect(0, 0, 0, textLine.Height);
@@ -456,12 +478,12 @@ namespace Avalonia.Media.TextFormatting
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
- if (textLine.Length == 0)
+ if (textLine is null)
{
if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
- _paragraphProperties, fontManager);
+ _paragraphProperties);
textLines.Add(emptyTextLine);
@@ -504,7 +526,7 @@ namespace Avalonia.Media.TextFormatting
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
- if (textLine.TextLineBreak?.RemainingRuns is not null)
+ if (textLine.TextLineBreak is { IsSplit: true })
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
@@ -518,11 +540,9 @@ namespace Avalonia.Media.TextFormatting
}
}
- //Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
- var textLine =
- TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
+ var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
textLines.Add(textLine);
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 ad3244a3a5..187b3154ad 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
+ private TextLineBreak? _textLineBreak;
private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
@@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
{
FirstTextSourceIndex = firstTextSourceIndex;
Length = length;
- TextLineBreak = lineBreak;
+ _textLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
_textRuns = textRuns;
@@ -38,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; }
@@ -167,38 +168,54 @@ namespace Avalonia.Media.TextFormatting
{
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.Length - 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.Length; i++)
+ for (var i = 0; i <= lastIndex; i++)
{
var currentRun = _textRuns[i];
@@ -230,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
currentRun = _textRuns[j];
- if(currentRun is not ShapedTextRun)
+ if (currentRun is not ShapedTextRun)
{
continue;
}
@@ -262,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
continue;
}
}
- else
- {
- continue;
- }
break;
}
@@ -410,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)
@@ -563,386 +576,505 @@ 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.Length);
- 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.Length; 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;
+ var currentRun = _textRuns[i];
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ var firstRunIndex = i;
+ var lastRunIndex = firstRunIndex;
+ var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
+ var directionalWidth = 0.0;
+
+ if (currentRun is DrawableTextRun currentDrawable)
{
- startX += currentRun.Size.Width;
+ directionalWidth = currentDrawable.Size.Width;
+ }
- currentPosition += currentRun.Length;
+ // Find consecutive runs of same direction
+ for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
+ {
+ var nextRun = _textRuns[lastRunIndex + 1];
- continue;
+ var nextDirection = GetDirection(nextRun, currentDirection);
+
+ if (currentDirection != nextDirection)
+ {
+ break;
+ }
+
+ if (nextRun is DrawableTextRun nextDrawable)
+ {
+ directionalWidth += nextDrawable.Size.Width;
+ }
}
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
{
- var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
+ case FlowDirection.RightToLeft:
+ {
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
- double startOffset;
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
- double endOffset;
+ currentPosition += currentRun.Length;
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ directionalWidth -= drawableTextRun.Size.Width;
+ currentX += drawableTextRun.Size.Width;
+ }
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ if(lastRunIndex - 1 < 0)
+ {
+ break;
+ }
+ }
- startX += startOffset;
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- endX += endOffset;
+ if (currentPosition + currentRun.Length > firstTextSourceIndex)
+ {
+ break;
+ }
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+ currentPosition += currentRun.Length;
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+ if(firstRunIndex + 1 == _textRuns.Length)
+ {
+ break;
+ }
+ }
- currentDirection = FlowDirection.LeftToRight;
+ break;
+ }
}
- else
+
+ i = lastRunIndex;
+
+ if (directionalWidth == 0)
{
- var rightToLeftIndex = index;
- var rightToLeftWidth = currentShapedRun.Size.Width;
+ continue;
+ }
- while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
- {
- if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+ var coveredLength = 0;
+ TextBounds? textBounds = null;
+
+ switch (currentDirection)
+ {
+
+ case FlowDirection.RightToLeft:
{
+ 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);
- rightToLeftIndex++;
-
- rightToLeftWidth += nextShapedRun.Size.Width;
+ currentX = textBounds.Rectangle.Right;
- if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
- {
break;
}
+ }
- currentShapedRun = nextShapedRun;
- }
+ if (coveredLength > 0)
+ {
+ result.Add(textBounds);
- startX += rightToLeftWidth;
+ remainingLength -= coveredLength;
+ }
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (remainingLength <= 0)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ var currentX = Start + WidthIncludingTrailingWhitespace;
- remainingLength -= currentRunBounds.Length;
- currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
- endX = currentRunBounds.Rectangle.Right;
- startX = currentRunBounds.Rectangle.Left;
+ 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;
- var rightToLeftRunBounds = new List { currentRunBounds };
+ if (currentRun is DrawableTextRun currentDrawable)
+ {
+ directionalWidth = currentDrawable.Size.Width;
+ }
+
+ // Find consecutive runs of same direction
+ for (; firstRunIndex - 1 > 0; firstRunIndex--)
+ {
+ var previousRun = _textRuns[firstRunIndex - 1];
+
+ var previousDirection = GetDirection(previousRun, currentDirection);
+
+ if (currentDirection != previousDirection)
+ {
+ break;
+ }
- for (int i = rightToLeftIndex - 1; i >= index; i--)
+ if (currentRun is DrawableTextRun previousDrawable)
{
- if (_textRuns[i] is not ShapedTextRun shapedRun)
+ directionalWidth += previousDrawable.Size.Width;
+ }
+ }
+
+ //Skip runs that are not part of the hit test range
+ switch (currentDirection)
+ {
+ case FlowDirection.RightToLeft:
{
- continue;
- }
+ for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+ {
+ currentRun = _textRuns[lastRunIndex];
- currentShapedRun = shapedRun;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX -= drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- rightToLeftRunBounds.Insert(0, currentRunBounds);
+ continue;
+ }
- remainingLength -= currentRunBounds.Length;
- startX = currentRunBounds.Rectangle.Left;
+ break;
+ }
- currentPosition += currentRunBounds.Length;
- }
+ break;
+ }
+ default:
+ {
+ for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+ {
+ currentRun = _textRuns[firstRunIndex];
- combinedWidth = endX - startX;
+ if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+ {
+ currentPosition += currentRun.Length;
- currentRect = new Rect(startX, 0, combinedWidth, Height);
+ if (currentRun is DrawableTextRun drawableTextRun)
+ {
+ currentX += drawableTextRun.Size.Width;
+ directionalWidth -= drawableTextRun.Size.Width;
+ }
- currentDirection = FlowDirection.RightToLeft;
+ continue;
+ }
- if (!MathUtilities.IsZero(combinedWidth))
- {
- result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
- }
+ break;
+ }
- startX = endX;
+ break;
+ }
}
- }
- else
- {
- if (currentPosition + currentRun.Length <= firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- currentPosition += currentRun.Length;
+ i = firstRunIndex;
+ if (directionalWidth == 0)
+ {
continue;
}
- if (currentPosition < firstTextSourceIndex)
- {
- startX += currentRun.Size.Width;
- }
+ var coveredLength = 0;
+
+ TextBounds? textBounds = null;
- if (currentPosition + currentRun.Length <= characterIndex)
+ switch (currentDirection)
{
- endX += currentRun.Size.Width;
+ 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);
- characterLength = currentRun.Length;
+ currentX = textBounds.Rectangle.Left;
+
+ break;
+ }
}
- }
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
+ //Visual order is always left to right so we need to insert
+ result.Insert(0, textBounds);
- //Lines that only contain a linebreak need to be covered here
- if (characterLength == 0)
- {
- characterLength = NewLineLength;
+ remainingLength -= coveredLength;
+
+ if (remainingLength <= 0)
+ {
+ break;
+ }
}
+ }
- combinedWidth = endX - startX;
+ return result;
+ }
- currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+ private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
+ int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+ {
+ coveredLength = 0;
+ var textRunBounds = new List();
+ var startX = endX;
- currentPosition += characterLength;
+ for (int i = lastRunIndex; i >= firstRunIndex; i--)
+ {
+ var currentRun = _textRuns[i];
- remainingLength -= characterLength;
+ if (currentRun is ShapedTextRun shapedTextRun)
+ {
+ var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- startX = endX;
+ textRunBounds.Insert(0, runBounds);
- if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
- {
- if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+ if (offset > 0)
{
- currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+ endX = runBounds.Rectangle.Right;
- var textBounds = result[result.Count - 1];
+ startX = endX;
+ }
- textBounds.Rectangle = currentRect;
+ startX -= runBounds.Rectangle.Width;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
+ currentPosition += runBounds.Length + offset;
+
+ coveredLength += runBounds.Length;
+
+ remainingLength -= runBounds.Length;
+ }
+ else
+ {
+ if (currentRun is DrawableTextRun drawableTextRun)
{
- currentRect = currentRunBounds.Rectangle;
+ startX -= drawableTextRun.Size.Width;
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
+ textRunBounds.Insert(0,
+ new TextRunBounds(
+ new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
}
- }
- lastRunBounds = currentRunBounds;
+ currentPosition += currentRun.Length;
- currentWidth += combinedWidth;
+ coveredLength += currentRun.Length;
- if (remainingLength <= 0 || currentPosition >= characterIndex)
+ remainingLength -= currentRun.Length;
+ }
+
+ if (remainingLength <= 0)
{
break;
}
-
- lastDirection = currentDirection;
}
- return result;
- }
+ newPosition = currentPosition;
- private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
- {
- var characterIndex = firstTextSourceIndex + textLength;
+ var runWidth = endX - startX;
- var result = new List(_textRuns.Length);
- var lastDirection = FlowDirection.LeftToRight;
- var currentDirection = lastDirection;
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var currentPosition = FirstTextSourceIndex;
- var remainingLength = textLength;
+ return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
+ }
- var startX = WidthIncludingTrailingWhitespace;
- double currentWidth = 0;
- var currentRect = default(Rect);
+ 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 (var index = _textRuns.Length - 1; index >= 0; index--)
+ for (int i = firstRunIndex; i <= lastRunIndex; 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 = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
- var startIndex = currentPosition;
- double startOffset;
- double endOffset;
+ textRunBounds.Add(runBounds);
- if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+ if (offset > 0)
{
- if (currentPosition < startIndex)
- {
- startOffset = endOffset = 0;
- }
- else
- {
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ startX = runBounds.Rectangle.Left;
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- }
- }
- else
- {
- endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-
- startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ endX = startX;
}
- startX -= currentRun.Size.Width - startOffset;
- endX -= currentRun.Size.Width - endOffset;
+ currentPosition += runBounds.Length + offset;
- var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
- var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+ endX += runBounds.Rectangle.Width;
- 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;
+ textRunBounds.Add(
+ new TextRunBounds(
+ new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
+
+ endX += drawableTextRun.Size.Width;
}
- 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;
- if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
- {
- if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
- {
- currentRect = currentRect.WithWidth(currentWidth + runWidth);
+ var bounds = new Rect(startX, 0, runWidth, Height);
- var textBounds = result[result.Count - 1];
+ return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
+ }
- textBounds.Rectangle = currentRect;
+ private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
+ {
+ var startIndex = currentPosition;
- textBounds.TextRunBounds.Add(currentRunBounds);
- }
- else
- {
- currentRect = currentRunBounds.Rectangle;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds }));
- }
- }
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- currentWidth += runWidth;
- currentPosition += characterLength;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- if (currentPosition > characterIndex)
- {
- break;
- }
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
- lastDirection = currentDirection;
- remainingLength -= characterLength;
+ var endX = startX + endOffset;
+ startX += startOffset;
- if (remainingLength <= 0)
- {
- break;
- }
+ 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);
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
}
- result.Reverse();
+ //Lines that only contain a linebreak need to be covered here
+ if (characterLength == 0)
+ {
+ characterLength = NewLineLength;
+ }
- return result;
+ var runWidth = endX - startX;
+
+ return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
- private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
+ private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
+ int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
{
var startX = endX;
- var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+ var startIndex = currentPosition;
- currentPosition += offset;
+ offset = Math.Max(0, firstTextSourceIndex - currentPosition);
- var startIndex = currentPosition;
+ var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
- double startOffset;
- double endOffset;
+ if (currentPosition != firstCluster)
+ {
+ startIndex = firstCluster + offset;
+ }
+ else
+ {
+ startIndex += offset;
+ }
- endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+ var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
- startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+ var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
startX -= currentRun.Size.Width - startOffset;
endX -= currentRun.Size.Width - endOffset;
@@ -968,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
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);
- }
-
public override void Dispose()
{
for (int i = 0; i < _textRuns.Length; i++)
@@ -993,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
{
_textLineMetrics = CreateLineMetrics();
+ if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
+ {
+ _textLineBreak = new TextLineBreak(textEndOfLine);
+ }
+
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
}
@@ -1285,13 +1412,11 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextRun textRun:
{
- var properties = textRun.Properties;
- var textMetrics =
- new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
+ var textMetrics = textRun.TextMetrics;
- if (fontRenderingEmSize < properties.FontRenderingEmSize)
+ if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
{
- fontRenderingEmSize = properties.FontRenderingEmSize;
+ fontRenderingEmSize = textMetrics.FontRenderingEmSize;
if (ascent > textMetrics.Ascent)
{
@@ -1318,7 +1443,7 @@ 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.Size.Width;
@@ -1330,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)
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
index 6bb820d214..fac8cd8737 100644
--- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
+++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
@@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute
/// 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 .
+ /// 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/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 c05c04c22e..8509067cd0 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, IRef glyphRun);
+ void DrawGlyphRun(IBrush? foreground, IRef glyphRun);
///
/// Creates a new that can be used as a render layer
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/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/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/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs
index cad4ab2051..4b47c93eb5 100644
--- a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs
+++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs
@@ -1,10 +1,6 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
namespace Avalonia.Platform;
-public struct PlatformGraphicsExternalImageProperties
+public record struct PlatformGraphicsExternalImageProperties
{
public int Width { get; set; }
public int Height { get; set; }
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
index a4005d4f5f..5bf9ff9d9a 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
@@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile
}
public FileInfo FileInfo { get; }
-
- public bool CanOpenRead => true;
-
- public bool CanOpenWrite => true;
-
+
public string Name => FileInfo.Name;
public virtual bool CanBookmark => true;
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
index ee169d62a5..34409f5fda 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
@@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider
: Task.FromResult(null);
}
- public virtual Task TryGetFileFromPath(Uri filePath)
+ public virtual Task TryGetFileFromPathAsync(Uri filePath)
{
if (filePath.IsAbsoluteUri)
{
@@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult(null);
}
- public virtual Task TryGetFolderFromPath(Uri folderPath)
+ public virtual Task TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath.IsAbsoluteUri)
{
@@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult(null);
}
- public virtual Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
+ public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
// Note, this BCL API returns different values depending on the .NET version.
// We should also document it.
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/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
index 6922151e02..9d3c961e51 100644
--- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
+++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
@@ -66,7 +66,7 @@ public interface IStorageProvider
/// 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 TryGetFileFromPath(Uri filePath);
+ Task TryGetFileFromPathAsync(Uri filePath);
///
/// Attempts to read folder from the file-system by its path.
@@ -78,12 +78,12 @@ public interface IStorageProvider
/// 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 TryGetFolderFromPath(Uri folderPath);
+ 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 TryGetWellKnownFolder(WellKnownFolder wellKnownFolder);
+ Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder);
}
diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
index c7772d1196..6f8b945cd6 100644
--- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
+++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
@@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage;
///
public static class StorageProviderExtensions
{
- ///
- public static Task TryGetFileFromPath(this IStorageProvider provider, string filePath)
+ ///
+ public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
- return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath));
+ return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
- ///
- public static Task TryGetFolderFromPath(this IStorageProvider provider, string folderPath)
+ ///
+ public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
- return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath));
+ return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}
- internal static string? TryGetFullPath(this IStorageFolder folder)
+ ///
+ /// 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 (folder is BclStorageFolder storageFolder)
+ if (item is BclStorageFolder storageFolder)
{
return storageFolder.DirectoryInfo.FullName;
}
-
- if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
- {
- return absolutePath.LocalPath;
- }
-
- // android "content:", browser and ios relative links go here.
- return null;
- }
-
- internal static string? TryGetFullPath(this IStorageFile file)
- {
- if (file is BclStorageFile storageFolder)
+ if (item is BclStorageFile storageFile)
{
- return storageFolder.FileInfo.FullName;
+ return storageFile.FileInfo.FullName;
}
- if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
+ 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/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 3e20dcce56..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, StyledProperty 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,7 +60,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(priority != BindingPriority.LocalValue);
UpdateValueEntry(value, priority);
- SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority);
+ SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false);
}
public void SetLocalValueAndRaise(
@@ -65,7 +68,16 @@ namespace Avalonia.PropertyStore
StyledProperty property,
T value)
{
- SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue);
+ SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue, false);
+ }
+
+ public void SetCurrentValueAndRaise(
+ ValueStore owner,
+ StyledProperty property,
+ T value)
+ {
+ IsOverridenCurrentValue = true;
+ SetAndRaiseCore(owner, property, value, Priority, true);
}
public bool TryGetBaseValue([MaybeNullWhen(false)] out T value)
@@ -98,7 +110,7 @@ namespace Avalonia.PropertyStore
Debug.Assert(Priority != BindingPriority.Animation);
Debug.Assert(BasePriority != BindingPriority.Unset);
UpdateValueEntry(null, BindingPriority.Animation);
- SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority);
+ SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority, false);
}
public override void CoerceValue(ValueStore owner, AvaloniaProperty property)
@@ -158,15 +170,16 @@ namespace Avalonia.PropertyStore
ValueStore owner,
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);
@@ -209,7 +222,6 @@ namespace Avalonia.PropertyStore
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/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
index f89cb029b6..5908d9e535 100644
--- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
+++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
@@ -10,6 +10,8 @@ namespace Avalonia.PropertyStore
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
+ private T? _defaultValue;
+ private bool _isDefaultValueInitialized;
public LocalValueBindingObserver(ValueStore owner, StyledProperty property)
{
@@ -41,26 +43,28 @@ namespace Avalonia.PropertyStore
public void OnNext(T value)
{
- static void Execute(ValueStore owner, StyledProperty 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 2d157b2519..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,6 +9,8 @@ namespace Avalonia.PropertyStore
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
+ private T? _defaultValue;
+ private bool _isDefaultValueInitialized;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property)
{
@@ -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 b56d0d4529..b82714817b 100644
--- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
@@ -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 697725c87b..550f5c0001 100644
--- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
@@ -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/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs
index f36a96992b..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
{
@@ -156,11 +155,12 @@ 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);
}
}
@@ -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))
@@ -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)
{
@@ -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.
@@ -507,7 +509,7 @@ namespace Avalonia.PropertyStore
if (existing == observer)
{
_localValueBindings?.Remove(property.Id);
- ClearLocalValue(property);
+ ClearValue(property);
}
}
}
@@ -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/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 c5d7ec61e0..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,26 +59,27 @@ 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;
@@ -76,9 +90,11 @@ public class CompositingRenderer : IRendererWithCompositor
///
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();
}
@@ -125,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();
}
@@ -170,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;
@@ -178,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;
@@ -200,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);
}
@@ -209,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);
}
@@ -288,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)
@@ -303,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();
@@ -322,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/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
index ab4329df62..bfe70d593d 100644
--- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
+++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
@@ -1,4 +1,3 @@
-using System;
using System.Threading.Tasks;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
@@ -7,7 +6,7 @@ namespace Avalonia.Rendering.Composition;
public class CompositionDrawingSurface : CompositionSurface
{
- internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server;
+ internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!;
internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server))
{
}
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/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
index 6dba18704f..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));
diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
index aea4df525d..153b32c5f3 100644
--- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -35,11 +35,14 @@ namespace Avalonia.Rendering.Composition
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
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
index 05488a558f..b75d080cfd 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, IRef 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))
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 c58beebe7f..50df8bd32b 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, IRef glyphRun)
+ public void DrawGlyphRun(IBrush? foreground, IRef glyphRun)
{
_impl.DrawGlyphRun(foreground, glyphRun);
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
index ebab39cee8..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.AsMemory(), 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.PlatformImpl);
- 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/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..98be861afa 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server
{
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);
@@ -74,7 +75,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/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/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 fa3260ffb4..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
{
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/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
index c67ac7057d..8e5dc38317 100644
--- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
@@ -48,8 +48,10 @@ 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)
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/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/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/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/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..5b8dac2f53 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -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);
///
@@ -71,6 +75,23 @@ namespace Avalonia
public static readonly StyledProperty ThemeProperty =
AvaloniaProperty.Register(nameof(Theme));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ActualThemeVariantProperty =
+ AvaloniaProperty.Register(
+ nameof(ThemeVariant),
+ inherits: true,
+ defaultValue: ThemeVariant.Light);
+
+ ///
+ /// Defines the RequestedThemeVariant property.
+ ///
+ public static readonly StyledProperty RequestedThemeVariantProperty =
+ AvaloniaProperty.Register(
+ nameof(ThemeVariant),
+ defaultValue: ThemeVariant.Default);
+
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
@@ -257,6 +278,15 @@ namespace Avalonia
set => SetValue(ThemeProperty, value);
}
+ ///
+ /// Gets the UI theme that is currently used by the element, which might be different than the .
+ ///
+ ///
+ /// 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.
+ ///
+ public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
+
///
/// Gets the styled element's logical children.
///
@@ -439,11 +469,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 +524,7 @@ namespace Avalonia
NotifyResourcesChanged();
}
-#nullable disable
- RaisePropertyChanged(
- ParentProperty,
- new Optional(old),
- new BindingValue(Parent),
- BindingPriority.LocalValue);
-#nullable enable
+ RaisePropertyChanged(ParentProperty, old, Parent);
}
}
@@ -621,6 +645,13 @@ namespace Avalonia
if (change.Property == ThemeProperty)
OnControlThemeChanged();
+ else if (change.Property == RequestedThemeVariantProperty)
+ {
+ if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default)
+ SetValue(ActualThemeVariantProperty, themeVariant);
+ else
+ ClearValue(ActualThemeVariantProperty);
+ }
}
private protected virtual void OnControlThemeChanged()
@@ -658,7 +689,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 79d1b9202d..8695918c18 100644
--- a/src/Avalonia.Base/StyledProperty.cs
+++ b/src/Avalonia.Base/StyledProperty.cs
@@ -171,7 +171,7 @@ namespace Avalonia
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{
- return new EffectiveValue(o, this);
+ return o.GetValueStore().CreateEffectiveValue(this);
}
///
@@ -194,24 +194,48 @@ namespace Avalonia
}
///
- [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
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)
{
- return null;
+ converted = default;
+ return false;
}
- else if (value == UnsetValue)
+ if (value == UnsetValue)
{
target.ClearValue(this);
- return null;
+ converted = default;
+ return false;
}
- else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted))
+ else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var v))
{
- return target.SetValue(this, (TValue)converted!, priority);
+ converted = (TValue)v!;
+ return true;
}
else
{
@@ -220,14 +244,6 @@ namespace Avalonia
}
}
- 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));
diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
new file mode 100644
index 0000000000..2467d99b3b
--- /dev/null
+++ b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
@@ -0,0 +1,22 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Metadata;
+
+namespace Avalonia.Styling;
+
+///
+/// Interface for an application host element with a root theme variant.
+///
+[Unstable]
+public interface IGlobalThemeVariantProvider : IResourceHost
+{
+ ///
+ /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination.
+ ///
+ 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/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..8218533f4f
--- /dev/null
+++ b/src/Avalonia.Base/Styling/ThemeVariant.cs
@@ -0,0 +1,105 @@
+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
+///
+[TypeConverter(typeof(ThemeVariantTypeConverter))]
+public sealed record ThemeVariant
+{
+ ///
+ /// 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/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 e6d7492c51..8b0cc06136 100644
--- a/src/Avalonia.Base/Visual.cs
+++ b/src/Avalonia.Base/Visual.cs
@@ -348,7 +348,7 @@ namespace Avalonia
///
public void InvalidateVisual()
{
- VisualRoot?.Renderer?.AddDirty(this);
+ VisualRoot?.Renderer.AddDirty(this);
}
///
@@ -449,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);
}
///
@@ -477,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);
}
}
}
@@ -540,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);
}
}
}
@@ -581,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(
@@ -659,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 a0dbf238dc..31722974ee 100644
--- a/src/Avalonia.Base/composition-schema.xml
+++ b/src/Avalonia.Base/composition-schema.xml
@@ -26,6 +26,7 @@
+
@@ -39,8 +40,8 @@
-
-
+
+
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.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/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 6c761ab4cf..951e60c25b 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
@@ -44,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.
@@ -53,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);
@@ -63,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;
@@ -104,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);
@@ -164,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
{
@@ -269,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()
@@ -309,6 +281,7 @@ namespace Avalonia.Controls
// Don't invalidate measure when children change.
}
+ ///
protected override Size MeasureOverride(Size availableSize)
{
if (_isLayoutInProgress)
@@ -334,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);
@@ -364,6 +337,7 @@ namespace Avalonia.Controls
}
}
+ ///
protected override Size ArrangeOverride(Size finalSize)
{
if (_isLayoutInProgress)
@@ -380,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
@@ -421,6 +395,7 @@ namespace Avalonia.Controls
}
}
+ ///
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@@ -428,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)
@@ -501,7 +478,7 @@ namespace Avalonia.Controls
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
- return _viewManager.GetElementIndex(virtInfo!);
+ return _viewManager.GetElementIndex(virtInfo);
}
return -1;
@@ -529,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)
@@ -547,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);
}
@@ -647,9 +624,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.
@@ -693,7 +670,7 @@ namespace Avalonia.Controls
try
{
- virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args);
+ virtualLayout.OnItemsChanged(LayoutContext, newValue, args);
}
finally
{
@@ -760,7 +737,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();
}
@@ -788,7 +765,7 @@ namespace Avalonia.Controls
{
if (Layout is VirtualizingLayout virtualLayout)
{
- virtualLayout.OnItemsChanged(GetLayoutContext(), sender, args);
+ virtualLayout.OnItemsChanged(LayoutContext, sender, args);
}
else
{
@@ -807,15 +784,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..6d3ba3cf8a 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, IGlobalThemeVariantProvider, 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 =
+ StyledElement.ActualThemeVariantProperty.AddOwner();
+
+ ///
+ public static readonly StyledProperty RequestedThemeVariantProperty =
+ StyledElement.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,19 @@ namespace Avalonia
set { SetValue(DataContextProperty, value); }
}
+ ///
+ public ThemeVariant? RequestedThemeVariant
+ {
+ get => GetValue(RequestedThemeVariantProperty);
+ set => SetValue(RequestedThemeVariantProperty, value);
+ }
+
+ ///
+ public ThemeVariant ActualThemeVariant
+ {
+ get => GetValue(ActualThemeVariantProperty);
+ }
+
///
/// Gets the current instance of the class.
///
@@ -191,11 +217,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 +248,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 +321,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 85f139a6a3..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;
@@ -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/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/Button.cs b/src/Avalonia.Controls/Button.cs
index 9627f200df..1ec6f8dabc 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -394,10 +394,10 @@ namespace Avalonia.Controls
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
IsPressed = true;
+ e.Handled = true;
if (ClickMode == ClickMode.Press)
{
- e.Handled = true;
OnClick();
}
}
@@ -411,11 +411,11 @@ namespace Avalonia.Controls
if (IsPressed && e.InitialPressMouseButton == MouseButton.Left)
{
IsPressed = false;
+ e.Handled = true;
if (ClickMode == ClickMode.Release &&
this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c)))
{
- e.Handled = true;
OnClick();
}
}
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 b7a298bb16..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;
}
@@ -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..ab7c9948c4 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;
@@ -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/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/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 59b5bf48a5..ce12d5f2bf 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;
@@ -17,7 +16,6 @@ using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
-using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -91,10 +89,11 @@ namespace Avalonia.Controls
/// 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();
@@ -106,7 +105,6 @@ namespace Avalonia.Controls
private Tuple? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
- private IScrollSnapPointsInfo? _scrolSnapPointInfo;
///
/// Initializes a new instance of the class.
@@ -134,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);
}
///
@@ -143,8 +141,8 @@ namespace Avalonia.Controls
///
public ControlTheme? ItemContainerTheme
{
- get { return GetValue(ItemContainerThemeProperty); }
- set { SetValue(ItemContainerThemeProperty, value); }
+ get => GetValue(ItemContainerThemeProperty);
+ set => SetValue(ItemContainerThemeProperty, value);
}
///
@@ -161,8 +159,8 @@ namespace Avalonia.Controls
///
public ITemplate ItemsPanel
{
- get { return GetValue(ItemsPanelProperty); }
- set { SetValue(ItemsPanelProperty, value); }
+ get => GetValue(ItemsPanelProperty);
+ set => SetValue(ItemsPanelProperty, value);
}
///
@@ -171,8 +169,8 @@ namespace Avalonia.Controls
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
- get { return GetValue(ItemTemplateProperty); }
- set { SetValue(ItemTemplateProperty, value); }
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
}
///
@@ -221,6 +219,7 @@ namespace Avalonia.Controls
}
+ ///
public event EventHandler HorizontalSnapPointsChanged
{
add
@@ -240,6 +239,7 @@ namespace Avalonia.Controls
}
}
+ ///
public event EventHandler VerticalSnapPointsChanged
{
add
@@ -264,8 +264,8 @@ namespace Avalonia.Controls
///
public bool AreHorizontalSnapPointsRegular
{
- get { return GetValue(AreHorizontalSnapPointsRegularProperty); }
- set { SetValue(AreHorizontalSnapPointsRegularProperty, value); }
+ get => GetValue(AreHorizontalSnapPointsRegularProperty);
+ set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
///
@@ -273,8 +273,8 @@ namespace Avalonia.Controls
///
public bool AreVerticalSnapPointsRegular
{
- get { return GetValue(AreVerticalSnapPointsRegularProperty); }
- set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
+ get => GetValue(AreVerticalSnapPointsRegularProperty);
+ set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
///
@@ -424,13 +424,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");
-
- _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo;
}
///
@@ -477,11 +476,13 @@ namespace Avalonia.Controls
base.OnKeyDown(e);
}
+ ///
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ItemsControlAutomationPeer(this);
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -558,7 +559,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);
///
@@ -748,11 +754,13 @@ namespace Avalonia.Controls
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;
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/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs
index 6b9e378d3d..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();
}
@@ -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 9c1fb93a48..ab64416a2c 100644
--- a/src/Avalonia.Controls/NativeMenu.Export.cs
+++ b/src/Avalonia.Controls/NativeMenu.Export.cs
@@ -12,10 +12,10 @@ 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; }
@@ -33,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)
@@ -45,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/Panel.cs b/src/Avalonia.Controls/Panel.cs
index 007d18c813..a7dc035459 100644
--- a/src/Avalonia.Controls/Panel.cs
+++ b/src/Avalonia.Controls/Panel.cs
@@ -68,7 +68,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 de3aca76d9..4dd868253e 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -553,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 a8a266e378..20bfb440e3 100644
--- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
+++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
@@ -27,7 +27,7 @@ namespace Avalonia.Controls.Platform
var files = await filePicker.OpenFilePickerAsync(options);
return files
- .Select(file => file.TryGetFullPath() ?? file.Name)
+ .Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray();
}
else if (dialog is SaveFileDialog saveDialog)
@@ -46,7 +46,7 @@ namespace Avalonia.Controls.Platform
return null;
}
- var filePath = file.TryGetFullPath() ?? file.Name;
+ var filePath = file.TryGetLocalPath() ?? file.Name;
return new[] { filePath };
}
return null;
@@ -64,7 +64,7 @@ namespace Avalonia.Controls.Platform
var folders = await filePicker.OpenFolderPickerAsync(options);
return folders
- .Select(folder => folder.TryGetFullPath() ?? folder.Name)
+ .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/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/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 8594b584fa..e8eaac7d17 100644
--- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs
@@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters
/// Defines the property.
///
public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty =
- AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
+ AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
///
/// Defines the property.
///
public static readonly StyledProperty AreVerticalSnapPointsRegularProperty =
- AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
+ AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
///
/// Defines the event.
///
public static readonly RoutedEvent HorizontalSnapPointsChangedEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(HorizontalSnapPointsChanged),
RoutingStrategies.Bubble);
@@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters
/// Defines the event.
///
public static readonly RoutedEvent VerticalSnapPointsChangedEvent =
- RoutedEvent.Register(
+ RoutedEvent.Register(
nameof(VerticalSnapPointsChanged),
RoutingStrategies.Bubble);
@@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
///
- /// Gets or sets whether the horizontal snap points for the are equidistant from each other.
+ /// Gets or sets whether the horizontal snap points for the are equidistant from each other.
///
public bool AreHorizontalSnapPointsRegular
{
@@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters
}
///
- /// Gets or sets whether the vertical snap points for the are equidistant from each other.
+ /// Gets or sets whether the vertical snap points for the are equidistant from each other.
///
public bool AreVerticalSnapPointsRegular
{
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 762702efcc..454f7eac9d 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;
- private const int ProximityPoints = 10;
///
/// Defines the property.
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 3464857131..611d57a980 100644
--- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs
+++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs
@@ -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)
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 c85199a665..d6cd71aedc 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -120,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;
@@ -377,9 +377,9 @@ namespace Avalonia.Controls.Primitives
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
- if (InheritsTransform && placementTarget is Control c)
+ if (InheritsTransform)
{
- TransformTrackingHelper.Track(c, PlacementTargetTransformChanged)
+ TransformTrackingHelper.Track(placementTarget, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
}
else
@@ -518,6 +518,7 @@ namespace Avalonia.Controls.Primitives
Close();
}
+ ///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@@ -579,7 +580,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);
@@ -623,6 +624,7 @@ namespace Avalonia.Controls.Primitives
}
}
+ ///
protected override AutomationPeer OnCreateAutomationPeer()
{
return new PopupAutomationPeer(this);
@@ -723,7 +725,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)
@@ -850,7 +852,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;
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/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