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/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index 2f034bd083..3acd4bf9f2 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -15,6 +15,7 @@
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
"src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj",
+ "src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj",
"src\\Avalonia.Controls\\Avalonia.Controls.csproj",
"src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj",
"src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
index ce9a37a3ce..525e01c891 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -233,6 +233,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -548,6 +552,14 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -613,6 +625,7 @@ 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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
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/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm
index f345043f61..ce82f7d83f 100644
--- a/native/Avalonia.Native/src/OSX/WindowImpl.mm
+++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm
@@ -66,7 +66,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
_isModal = isDialog;
WindowBaseImpl::Show(activate, isDialog);
-
+ GetWindowState(&_actualWindowState);
HideOrShowTrafficLights();
return SetWindowState(_lastWindowState);
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/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.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/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/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/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..50a7a5c831 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -152,7 +152,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
- _values?.ClearLocalValue(property);
+ _values.ClearLocalValue(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);
}
///
@@ -279,7 +286,7 @@ namespace Avalonia
VerifyAccess();
- return _values?.IsSet(property) ?? false;
+ return _values.IsSet(property);
}
///
@@ -515,14 +522,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 +546,11 @@ 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);
}
- return new AvaloniaPropertyValue(
- property,
- GetValue(property),
- BindingPriority.Unset,
- "Unset");
+ return _values.GetDiagnostic(property);
}
internal ValueStore GetValueStore() => _values;
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
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/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/Internal/AssemblyDescriptor.cs b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs
index 467cd530fc..d1a803fefb 100644
--- a/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs
+++ b/src/Avalonia.Base/Platform/Internal/AssemblyDescriptor.cs
@@ -19,25 +19,20 @@ internal class AssemblyDescriptor : IAssemblyDescriptor
public AssemblyDescriptor(Assembly assembly)
{
Assembly = 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 +40,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/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/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/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/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/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index 6043175eee..5bf022cd51 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -71,6 +71,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 +274,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 +465,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);
}
///
@@ -621,6 +647,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 +691,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/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/Visual.cs b/src/Avalonia.Base/Visual.cs
index e6d7492c51..87bb1d3790 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);
}
}
}
@@ -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..36fd9fe709 100644
--- a/src/Avalonia.Base/composition-schema.xml
+++ b/src/Avalonia.Base/composition-schema.xml
@@ -39,8 +39,8 @@
-
-
+
+
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 100%
rename from src/Avalonia.Controls/Repeater/ItemsRepeater.cs
rename to src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
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 99%
rename from src/Avalonia.Controls/Repeater/ViewManager.cs
rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs
index 2dff18cd04..2d302a95dd 100644
--- a/src/Avalonia.Controls/Repeater/ViewManager.cs
+++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ViewManager.cs
@@ -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();
}
}
@@ -656,7 +656,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 +701,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
@@ -722,7 +722,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 98%
rename from src/Avalonia.Controls/Repeater/ViewportManager.cs
rename to src/Avalonia.Controls.ItemsRepeater/Controls/ViewportManager.cs
index 56e0cda8fe..336fb2d228 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;
}
}
}
@@ -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)
@@ -471,7 +471,7 @@ namespace Avalonia.Controls
break;
}
- parent = parent.VisualParent;
+ parent = parent.GetVisualParent();
}
if (!_managingViewportDisabled)
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/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
index 98885e11ca..55649660f7 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;
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/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/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/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/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/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/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/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/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/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index e265f4eb6a..d466edeb33 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
@@ -48,7 +48,7 @@ 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();
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index c85199a665..3b68cd2ae8 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -723,7 +723,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)
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/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs
index 7e0d695264..ce3158b282 100644
--- a/src/Avalonia.Controls/ProgressBar.cs
+++ b/src/Avalonia.Controls/ProgressBar.cs
@@ -231,7 +231,7 @@ namespace Avalonia.Controls
private void UpdateIndicator()
{
// Gets the size of the parent indicator container
- var barSize = _indicator?.Parent?.Bounds.Size ?? Bounds.Size;
+ var barSize = _indicator?.VisualParent?.Bounds.Size ?? Bounds.Size;
if (_indicator != null)
{
diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs
index a13b10d630..3c56c20db0 100644
--- a/src/Avalonia.Controls/ResolveByNameAttribute.cs
+++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs
@@ -7,7 +7,8 @@ namespace Avalonia.Controls
/// When applying this to attached properties, ensure to put on both
/// the Getter and Setter methods.
///
- public class ResolveByNameAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
+ public sealed class ResolveByNameAttribute : Attribute
{
}
}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 1c23919d0e..ab114da933 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -154,15 +154,15 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly StyledProperty HorizontalSnapPointsTypeProperty =
- AvaloniaProperty.Register(
+ public static readonly AttachedProperty HorizontalSnapPointsTypeProperty =
+ AvaloniaProperty.RegisterAttached(
nameof(HorizontalSnapPointsType));
///
/// Defines the property.
///
- public static readonly StyledProperty VerticalSnapPointsTypeProperty =
- AvaloniaProperty.Register(
+ public static readonly AttachedProperty VerticalSnapPointsTypeProperty =
+ AvaloniaProperty.RegisterAttached(
nameof(VerticalSnapPointsType));
///
@@ -625,6 +625,86 @@ namespace Avalonia.Controls
control.SetValue(HorizontalScrollBarVisibilityProperty, value);
}
+ ///
+ /// Gets the value of the HorizontalSnapPointsType attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsType GetHorizontalSnapPointsType(Control control)
+ {
+ return control.GetValue(HorizontalSnapPointsTypeProperty);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsType attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value)
+ {
+ control.SetValue(HorizontalSnapPointsTypeProperty, value);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsType attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsType GetVerticalSnapPointsType(Control control)
+ {
+ return control.GetValue(VerticalSnapPointsTypeProperty);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsType attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetVerticalSnapPointsType(Control control, SnapPointsType value)
+ {
+ control.SetValue(VerticalSnapPointsTypeProperty, value);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsAlignment attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control)
+ {
+ return control.GetValue(HorizontalSnapPointsAlignmentProperty);
+ }
+
+ ///
+ /// Gets the value of the HorizontalSnapPointsAlignment attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value)
+ {
+ control.SetValue(HorizontalSnapPointsAlignmentProperty, value);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsAlignment attached property.
+ ///
+ /// The control to read the value from.
+ /// The value of the property.
+ public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control)
+ {
+ return control.GetValue(VerticalSnapPointsAlignmentProperty);
+ }
+
+ ///
+ /// Gets the value of the VerticalSnapPointsAlignment attached property.
+ ///
+ /// The control to set the value on.
+ /// The value of the property.
+ public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value)
+ {
+ control.SetValue(VerticalSnapPointsAlignmentProperty, value);
+ }
+
///
/// Gets the value of the VerticalScrollBarVisibility attached property.
///
diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs
index f4c4d54951..f8ce5d23f6 100644
--- a/src/Avalonia.Controls/SelectableTextBlock.cs
+++ b/src/Avalonia.Controls/SelectableTextBlock.cs
@@ -229,7 +229,11 @@ namespace Avalonia.Controls
if (Match(keymap.Copy))
{
Copy();
-
+ handled = true;
+ }
+ else if (Match(keymap.SelectAll))
+ {
+ SelectAll();
handled = true;
}
diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs
index aa63ac975e..9362dab553 100644
--- a/src/Avalonia.Controls/StackPanel.cs
+++ b/src/Avalonia.Controls/StackPanel.cs
@@ -22,13 +22,13 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty SpacingProperty =
- StackLayout.SpacingProperty.AddOwner();
+ AvaloniaProperty.Register(nameof(Spacing));
///
/// Defines the property.
///
public static readonly StyledProperty OrientationProperty =
- StackLayout.OrientationProperty.AddOwner();
+ AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical);
///
/// Defines the property.
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index 9bd1dc95f9..ec31470126 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -673,8 +673,6 @@ namespace Avalonia.Controls
controlRun.Control is Control control)
{
VisualChildren.Remove(control);
-
- LogicalChildren.Remove(control);
}
}
}
@@ -693,8 +691,6 @@ namespace Avalonia.Controls
{
VisualChildren.Add(control);
- LogicalChildren.Add(control);
-
control.Measure(Size.Infinity);
}
}
@@ -720,6 +716,16 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ if (HasComplexContent)
+ {
+ ArrangeComplexContent(TextLayout, padding);
+ }
+
+ if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width))
+ {
+ return finalSize;
+ }
+
_constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout?.Dispose();
@@ -727,31 +733,36 @@ namespace Avalonia.Controls
if (HasComplexContent)
{
- var currentY = padding.Top;
+ ArrangeComplexContent(TextLayout, padding);
+ }
- foreach (var textLine in TextLayout.TextLines)
- {
- var currentX = padding.Left + textLine.Start;
+ return finalSize;
+ }
+
+ private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding)
+ {
+ var currentY = padding.Top;
- foreach (var run in textLine.TextRuns)
+ foreach (var textLine in textLayout.TextLines)
+ {
+ var currentX = padding.Left + textLine.Start;
+
+ foreach (var run in textLine.TextRuns)
+ {
+ if (run is DrawableTextRun drawable)
{
- if (run is DrawableTextRun drawable)
+ if (drawable is EmbeddedControlRun controlRun
+ && controlRun.Control is Control control)
{
- if (drawable is EmbeddedControlRun controlRun
- && controlRun.Control is Control control)
- {
- control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
- }
-
- currentX += drawable.Size.Width;
+ control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize));
}
- }
- currentY += textLine.Height;
+ currentX += drawable.Size.Width;
+ }
}
- }
- return finalSize;
+ currentY += textLine.Height;
+ }
}
protected override AutomationPeer OnCreateAutomationPeer()
@@ -892,7 +903,7 @@ namespace Avalonia.Controls
return textRun;
}
- return null;
+ return new TextEndOfParagraph();
}
}
}
diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs
new file mode 100644
index 0000000000..b9724251c7
--- /dev/null
+++ b/src/Avalonia.Controls/ThemeVariantScope.cs
@@ -0,0 +1,23 @@
+using Avalonia.Styling;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Decorator control that isolates controls subtree with locally defined .
+ ///
+ public class ThemeVariantScope : Decorator
+ {
+ ///
+ /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination.
+ /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant.
+ ///
+ ///
+ /// Setting RequestedThemeVariant to will apply parent's actual theme variant on the current scope.
+ ///
+ public ThemeVariant? RequestedThemeVariant
+ {
+ get => GetValue(RequestedThemeVariantProperty);
+ set => SetValue(RequestedThemeVariantProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index 06a829c418..4db71abfa8 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -1,9 +1,10 @@
using System;
+using System.ComponentModel;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
-using Avalonia.Controls.Notifications;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
@@ -17,7 +18,6 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
-using Avalonia.VisualTree;
using Avalonia.Input.Platform;
using System.Linq;
@@ -94,8 +94,8 @@ namespace Avalonia.Controls
private readonly IInputManager? _inputManager;
private readonly IAccessKeyHandler? _accessKeyHandler;
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
- private readonly IPlatformRenderInterface? _renderInterface;
private readonly IGlobalStyles? _globalStyles;
+ private readonly IGlobalThemeVariantProvider? _applicationThemeHost;
private readonly PointerOverPreProcessor? _pointerOverPreProcessor;
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription;
@@ -106,6 +106,7 @@ namespace Avalonia.Controls
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
+ private LayoutDiagnosticBridge? _layoutDiagnosticBridge;
///
/// Initializes static members of the class.
@@ -114,16 +115,6 @@ namespace Avalonia.Controls
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(KeyboardNavigationMode.Cycle);
AffectsMeasure(ClientSizeProperty);
-
- TransparencyLevelHintProperty.Changed.AddClassHandler(
- (tl, e) =>
- {
- if (tl.PlatformImpl != null)
- {
- tl.PlatformImpl.SetTransparencyLevelHint((WindowTransparencyLevel)e.NewValue!);
- tl.HandleTransparencyLevelChanged(tl.PlatformImpl.TransparencyLevel);
- }
- });
}
///
@@ -144,35 +135,21 @@ namespace Avalonia.Controls
///
public TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
{
- if (impl == null)
- {
- throw new InvalidOperationException(
- "Could not create window implementation: maybe no windowing subsystem was initialized?");
- }
-
- PlatformImpl = impl;
+ PlatformImpl = impl ?? throw new InvalidOperationException(
+ "Could not create window implementation: maybe no windowing subsystem was initialized?");
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
- dependencyResolver = dependencyResolver ?? AvaloniaLocator.Current;
+ dependencyResolver ??= AvaloniaLocator.Current;
_accessKeyHandler = TryGetService(dependencyResolver);
_inputManager = TryGetService(dependencyResolver);
_keyboardNavigationHandler = TryGetService(dependencyResolver);
- _renderInterface = TryGetService(dependencyResolver);
_globalStyles = TryGetService(dependencyResolver);
+ _applicationThemeHost = TryGetService(dependencyResolver);
Renderer = impl.CreateRenderer(this);
-
- if (Renderer != null)
- {
- Renderer.SceneInvalidated += SceneInvalidated;
- }
- else
- {
- // Prevent nullable error.
- Renderer = null!;
- }
+ Renderer.SceneInvalidated += SceneInvalidated;
impl.SetInputRoot(this);
@@ -191,10 +168,15 @@ namespace Avalonia.Controls
_globalStyles.GlobalStylesAdded += ((IStyleHost)this).StylesAdded;
_globalStyles.GlobalStylesRemoved += ((IStyleHost)this).StylesRemoved;
}
+ if (_applicationThemeHost is { })
+ {
+ SetValue(ActualThemeVariantProperty, _applicationThemeHost.ActualThemeVariant, BindingPriority.Template);
+ _applicationThemeHost.ActualThemeVariantChanged += GlobalActualThemeVariantChanged;
+ }
ClientSize = impl.ClientSize;
FrameSize = impl.FrameSize;
-
+
this.GetObservable(PointerOverElementProperty)
.Select(
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty())
@@ -218,7 +200,7 @@ namespace Avalonia.Controls
if(impl.TryGetFeature() is {} systemNavigationManager)
{
- systemNavigationManager.BackRequested += (s, e) =>
+ systemNavigationManager.BackRequested += (_, e) =>
{
e.RoutedEvent = BackRequestedEvent;
RaiseEvent(e);
@@ -315,6 +297,13 @@ namespace Avalonia.Controls
set => SetValue(TransparencyBackgroundFallbackProperty, value);
}
+ ///
+ public ThemeVariant? RequestedThemeVariant
+ {
+ get => GetValue(RequestedThemeVariantProperty);
+ set => SetValue(RequestedThemeVariantProperty, value);
+ }
+
///
/// Occurs when physical Back Button is pressed or a back navigation has been requested.
///
@@ -328,8 +317,17 @@ namespace Avalonia.Controls
{
get
{
- if (_layoutManager == null)
+ if (_layoutManager is null)
+ {
_layoutManager = CreateLayoutManager();
+
+ if (_layoutManager is LayoutManager typedLayoutManager)
+ {
+ _layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager);
+ _layoutDiagnosticBridge.SetupBridge();
+ }
+ }
+
return _layoutManager;
}
}
@@ -342,7 +340,7 @@ namespace Avalonia.Controls
///
/// Gets the renderer for the window.
///
- public IRenderer Renderer { get; private set; }
+ public IRenderer Renderer { get; }
internal PixelPoint? LastPointerPosition => _pointerOverPreProcessor?.LastPosition;
@@ -407,6 +405,24 @@ namespace Avalonia.Controls
return visual == null ? null : visual.VisualRoot as TopLevel;
}
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == TransparencyLevelHintProperty)
+ {
+ if (PlatformImpl != null)
+ {
+ PlatformImpl.SetTransparencyLevelHint(change.GetNewValue());
+ HandleTransparencyLevelChanged(PlatformImpl.TransparencyLevel);
+ }
+ }
+ else if (change.Property == ActualThemeVariantProperty)
+ {
+ PlatformImpl?.SetFrameThemeVariant((PlatformThemeVariant?)change.GetNewValue() ?? PlatformThemeVariant.Light);
+ }
+ }
+
///
/// Creates the layout manager for this .
///
@@ -418,7 +434,7 @@ namespace Avalonia.Controls
/// The dirty area.
protected virtual void HandlePaint(Rect rect)
{
- Renderer?.Paint(rect);
+ Renderer.Paint(rect);
}
///
@@ -431,9 +447,16 @@ namespace Avalonia.Controls
_globalStyles.GlobalStylesAdded -= ((IStyleHost)this).StylesAdded;
_globalStyles.GlobalStylesRemoved -= ((IStyleHost)this).StylesRemoved;
}
+ if (_applicationThemeHost is { })
+ {
+ _applicationThemeHost.ActualThemeVariantChanged -= GlobalActualThemeVariantChanged;
+ }
- Renderer?.Dispose();
- Renderer = null!;
+ Renderer.SceneInvalidated -= SceneInvalidated;
+ Renderer.Dispose();
+
+ _layoutDiagnosticBridge?.Dispose();
+ _layoutDiagnosticBridge = null;
_pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
@@ -449,7 +472,7 @@ namespace Avalonia.Controls
OnClosed(EventArgs.Empty);
- LayoutManager?.Dispose();
+ LayoutManager.Dispose();
}
///
@@ -464,7 +487,7 @@ namespace Avalonia.Controls
Width = clientSize.Width;
Height = clientSize.Height;
LayoutManager.ExecuteLayoutPass();
- Renderer?.Resized(clientSize);
+ Renderer.Resized(clientSize);
}
///
@@ -592,6 +615,11 @@ namespace Avalonia.Controls
}
}
+ private void GlobalActualThemeVariantChanged(object? sender, EventArgs e)
+ {
+ SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template);
+ }
+
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
{
_pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect);
@@ -617,5 +645,49 @@ namespace Avalonia.Controls
}
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature();
+
+ ///
+ /// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
+ ///
+ private sealed class LayoutDiagnosticBridge : IDisposable
+ {
+ private readonly RendererDiagnostics _diagnostics;
+ private readonly LayoutManager _layoutManager;
+ private bool _isHandling;
+
+ public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager)
+ {
+ _diagnostics = diagnostics;
+ _layoutManager = layoutManager;
+
+ diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged;
+ }
+
+ public void SetupBridge()
+ {
+ var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0;
+ if (needsHandling != _isHandling)
+ {
+ _isHandling = needsHandling;
+ _layoutManager.LayoutPassTimed = needsHandling
+ ? timing => _diagnostics.LastLayoutPassTiming = timing
+ : null;
+ }
+ }
+
+ private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays))
+ {
+ SetupBridge();
+ }
+ }
+
+ public void Dispose()
+ {
+ _diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged;
+ _layoutManager.LayoutPassTimed = null;
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs
index 022e1a74b1..9f8e3e38c0 100644
--- a/src/Avalonia.Controls/TreeViewItem.cs
+++ b/src/Avalonia.Controls/TreeViewItem.cs
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Threading;
@@ -22,11 +22,10 @@ namespace Avalonia.Controls
///
/// Defines the property.
///
- public static readonly DirectProperty IsExpandedProperty =
- AvaloniaProperty.RegisterDirect(
+ public static readonly StyledProperty IsExpandedProperty =
+ AvaloniaProperty.Register(
nameof(IsExpanded),
- o => o.IsExpanded,
- (o, v) => o.IsExpanded = v);
+ defaultBindingMode: BindingMode.TwoWay);
///
/// Defines the property.
@@ -46,7 +45,6 @@ namespace Avalonia.Controls
private TreeView? _treeView;
private Control? _header;
- private bool _isExpanded;
private int _level;
private bool _templateApplied;
private bool _deferredBringIntoViewFlag;
@@ -68,8 +66,8 @@ namespace Avalonia.Controls
///
public bool IsExpanded
{
- get { return _isExpanded; }
- set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); }
+ get => GetValue(IsExpandedProperty);
+ set => SetValue(IsExpandedProperty, value);
}
///
@@ -77,8 +75,8 @@ namespace Avalonia.Controls
///
public bool IsSelected
{
- get { return GetValue(IsSelectedProperty); }
- set { SetValue(IsSelectedProperty, value); }
+ get => GetValue(IsSelectedProperty);
+ set => SetValue(IsSelectedProperty, value);
}
///
@@ -86,8 +84,8 @@ namespace Avalonia.Controls
///
public int Level
{
- get { return _level; }
- private set { SetAndRaise(LevelProperty, ref _level, value); }
+ get => _level;
+ private set => SetAndRaise(LevelProperty, ref _level, value);
}
internal TreeView? TreeViewOwner => _treeView;
@@ -115,11 +113,6 @@ namespace Avalonia.Controls
}
}
- protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
- {
- base.OnDetachedFromLogicalTree(e);
- }
-
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
if (e.TargetObject == this)
@@ -264,9 +257,9 @@ namespace Avalonia.Controls
Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll
}
}
-
+
///
- /// Invoked when the event occurs in the header.
+ /// Invoked when the event occurs in the header.
///
protected virtual void OnHeaderDoubleTapped(TappedEventArgs e)
{
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index c5276741b6..634efbd699 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -23,7 +23,7 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty OrientationProperty =
- StackLayout.OrientationProperty.AddOwner();
+ StackPanel.OrientationProperty.AddOwner();
///
/// Defines the property.
diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs
index a20b4eee58..ba1b599421 100644
--- a/src/Avalonia.Controls/Window.cs
+++ b/src/Avalonia.Controls/Window.cs
@@ -450,7 +450,7 @@ namespace Avalonia.Controls
/// resulting task will produce the value when the window
/// is closed.
///
- public void Close(object dialogResult)
+ public void Close(object? dialogResult)
{
_dialogResult = dialogResult;
CloseCore(WindowCloseReason.WindowClosing, true);
@@ -573,7 +573,7 @@ namespace Avalonia.Controls
return;
}
- Renderer?.Stop();
+ Renderer.Stop();
if (Owner is Window owner)
{
@@ -721,7 +721,7 @@ namespace Avalonia.Controls
SetWindowStartupLocation(owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated, false);
- Renderer?.Start();
+ Renderer.Start();
OnOpened(EventArgs.Empty);
}
}
@@ -798,7 +798,7 @@ namespace Avalonia.Controls
PlatformImpl?.Show(ShowActivated, true);
- Renderer?.Start();
+ Renderer.Start();
Observable.FromEventPattern(
x => Closed += x,
diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index aad0482b50..0c9a91148b 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -129,7 +129,7 @@ namespace Avalonia.Controls
{
using (FreezeVisibilityChangeHandling())
{
- Renderer?.Stop();
+ Renderer.Stop();
PlatformImpl?.Hide();
IsVisible = false;
}
@@ -153,7 +153,7 @@ namespace Avalonia.Controls
}
PlatformImpl?.Show(true, false);
- Renderer?.Start();
+ Renderer.Start();
OnOpened(EventArgs.Empty);
}
}
@@ -219,7 +219,7 @@ namespace Avalonia.Controls
{
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
- Renderer?.Resized(clientSize);
+ Renderer.Resized(clientSize);
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
index 00173dbb35..a0ff3a714f 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs
@@ -1,19 +1,22 @@
using System;
using Avalonia.Controls;
+using Avalonia.Styling;
using Lifetimes = Avalonia.Controls.ApplicationLifetimes;
-using App = Avalonia.Application;
namespace Avalonia.Diagnostics.Controls
{
class Application : AvaloniaObject
- , Input.ICloseable
+ , Input.ICloseable, IDisposable
{
- private readonly App _application;
+ private readonly Avalonia.Application _application;
public event EventHandler? Closed;
- public Application(App application)
+ public static readonly StyledProperty RequestedThemeVariantProperty =
+ StyledElement.RequestedThemeVariantProperty.AddOwner();
+
+ public Application(Avalonia.Application application)
{
_application = application;
@@ -30,12 +33,15 @@ namespace Avalonia.Diagnostics.Controls
RendererRoot = application.ApplicationLifetime switch
{
Lifetimes.IClassicDesktopStyleApplicationLifetime classic => classic.MainWindow?.Renderer,
- Lifetimes.ISingleViewApplicationLifetime single => (single.MainView as Visual)?.VisualRoot?.Renderer,
+ Lifetimes.ISingleViewApplicationLifetime single => single.MainView?.VisualRoot?.Renderer,
_ => null
};
+
+ RequestedThemeVariant = application.RequestedThemeVariant;
+ _application.PropertyChanged += ApplicationOnPropertyChanged;
}
- internal App Instance => _application;
+ internal Avalonia.Application Instance => _application;
///
/// Defines the property.
@@ -114,5 +120,35 @@ namespace Avalonia.Diagnostics.Controls
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
internal Rendering.IRenderer? RendererRoot { get; }
+
+ ///
+ public ThemeVariant? RequestedThemeVariant
+ {
+ get => GetValue(RequestedThemeVariantProperty);
+ set => SetValue(RequestedThemeVariantProperty, value);
+ }
+
+ public void Dispose()
+ {
+ _application.PropertyChanged -= ApplicationOnPropertyChanged;
+ }
+
+ private void ApplicationOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == Avalonia.Application.RequestedThemeVariantProperty)
+ {
+ RequestedThemeVariant = e.GetNewValue();
+ }
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == RequestedThemeVariantProperty)
+ {
+ _application.RequestedThemeVariant = change.GetNewValue();
+ }
+ }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
index 5fc274a4e9..3cfb0246eb 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
@@ -1,4 +1,6 @@
-using Avalonia.Input;
+using System;
+using Avalonia.Input;
+using Avalonia.Styling;
namespace Avalonia.Diagnostics
{
@@ -42,8 +44,8 @@ namespace Avalonia.Diagnostics
= Conventions.DefaultScreenshotHandler;
///
- /// Gets or sets whether DevTools should use the dark mode theme
+ /// Gets or sets whether DevTools theme.
///
- public bool UseDarkMode { get; set; }
+ public ThemeVariant? ThemeVariant { get; set; }
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
index 8bff9ccde0..1951914273 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
@@ -123,7 +123,8 @@ namespace Avalonia.Diagnostics.ViewModels
private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
{
- if (value is StaticResourceExtension staticResource)
+ if (value is StaticResourceExtension staticResource
+ && staticResource.ResourceKey != null)
{
return (staticResource.ResourceKey, false);
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
index 3870cad7c5..3adad38ac6 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
@@ -1,11 +1,13 @@
using System;
using System.ComponentModel;
+using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.Reactive;
+using Avalonia.Rendering;
namespace Avalonia.Diagnostics.ViewModels
{
@@ -21,8 +23,6 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _focusedControl;
private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
- private bool _shouldVisualizeDirtyRects;
- private bool _showFpsOverlay;
private bool _freezePopups;
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
@@ -75,69 +75,76 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
- public bool ShouldVisualizeDirtyRects
+ public void ToggleVisualizeMarginPadding()
+ => ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
+
+ private IRenderer? TryGetRenderer()
+ => _root switch
+ {
+ TopLevel topLevel => topLevel.Renderer,
+ Controls.Application app => app.RendererRoot,
+ _ => null
+ };
+
+ private bool GetDebugOverlay(RendererDebugOverlays overlay)
+ => ((TryGetRenderer()?.Diagnostics.DebugOverlays ?? RendererDebugOverlays.None) & overlay) != 0;
+
+ private void SetDebugOverlay(RendererDebugOverlays overlay, bool enable,
+ [CallerMemberName] string? propertyName = null)
{
- get => _shouldVisualizeDirtyRects;
- set
+ if (TryGetRenderer() is not { } renderer)
{
- var changed = true;
- if (_root is TopLevel topLevel && topLevel.Renderer is { })
- {
- topLevel.Renderer.DrawDirtyRects = value;
- }
- else if (_root is Controls.Application app && app.RendererRoot is { })
- {
- app.RendererRoot.DrawDirtyRects = value;
- }
- else
- {
- changed = false;
- }
- if (changed)
- {
- RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
- }
+ return;
}
- }
- public void ToggleVisualizeDirtyRects()
- {
- ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
+ var oldValue = renderer.Diagnostics.DebugOverlays;
+ var newValue = enable ? oldValue | overlay : oldValue & ~overlay;
+
+ if (oldValue == newValue)
+ {
+ return;
+ }
+
+ renderer.Diagnostics.DebugOverlays = newValue;
+ RaisePropertyChanged(propertyName);
}
- public void ToggleVisualizeMarginPadding()
+ public bool ShowDirtyRectsOverlay
{
- ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
+ get => GetDebugOverlay(RendererDebugOverlays.DirtyRects);
+ set => SetDebugOverlay(RendererDebugOverlays.DirtyRects, value);
}
+ public void ToggleDirtyRectsOverlay()
+ => ShowDirtyRectsOverlay = !ShowDirtyRectsOverlay;
+
public bool ShowFpsOverlay
{
- get => _showFpsOverlay;
- set
- {
- var changed = true;
- if (_root is TopLevel topLevel && topLevel.Renderer is { })
- {
- topLevel.Renderer.DrawFps = value;
- }
- else if (_root is Controls.Application app && app.RendererRoot is { })
- {
- app.RendererRoot.DrawFps = value;
- }
- else
- {
- changed = false;
- }
- if(changed)
- RaiseAndSetIfChanged(ref _showFpsOverlay, value);
- }
+ get => GetDebugOverlay(RendererDebugOverlays.Fps);
+ set => SetDebugOverlay(RendererDebugOverlays.Fps, value);
}
public void ToggleFpsOverlay()
+ => ShowFpsOverlay = !ShowFpsOverlay;
+
+ public bool ShowLayoutTimeGraphOverlay
{
- ShowFpsOverlay = !ShowFpsOverlay;
+ get => GetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph);
+ set => SetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph, value);
}
+ public void ToggleLayoutTimeGraphOverlay()
+ => ShowLayoutTimeGraphOverlay = !ShowLayoutTimeGraphOverlay;
+
+ public bool ShowRenderTimeGraphOverlay
+ {
+ get => GetDebugOverlay(RendererDebugOverlays.RenderTimeGraph);
+ set => SetDebugOverlay(RendererDebugOverlays.RenderTimeGraph, value);
+ }
+
+ public void ToggleRenderTimeGraphOverlay()
+ => ShowRenderTimeGraphOverlay = !ShowRenderTimeGraphOverlay;
+
public ConsoleViewModel Console { get; }
public ViewModelBase? Content
@@ -254,10 +261,10 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
- if (_root is TopLevel top)
+
+ if (TryGetRenderer() is { } renderer)
{
- top.Renderer.DrawDirtyRects = false;
- top.Renderer.DrawFps = false;
+ renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None;
}
}
diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
index a2ee37c625..ec88db6664 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
+++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
}
- protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
+ protected bool RaiseAndSetIfChanged([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (!EqualityComparer.Default.Equals(field, value))
{
@@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false;
}
- protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
+ protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
index 97e21079c1..eac807a5bc 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
@@ -65,28 +65,42 @@
-
public class FluentTheme : Styles
{
- private readonly IResourceDictionary _baseDark;
- private readonly IResourceDictionary _fluentDark;
- private readonly IResourceDictionary _baseLight;
- private readonly IResourceDictionary _fluentLight;
private readonly Styles _compactStyles;
///
@@ -37,13 +27,8 @@ namespace Avalonia.Themes.Fluent
{
AvaloniaXamlLoader.Load(sp, this);
- _baseDark = (IResourceDictionary)GetAndRemove("BaseDark");
- _fluentDark = (IResourceDictionary)GetAndRemove("FluentDark");
- _baseLight = (IResourceDictionary)GetAndRemove("BaseLight");
- _fluentLight = (IResourceDictionary)GetAndRemove("FluentLight");
_compactStyles = (Styles)GetAndRemove("CompactStyles");
-
- EnsureThemeVariants();
+
EnsureCompactStyles();
object GetAndRemove(string key)
@@ -54,22 +39,10 @@ namespace Avalonia.Themes.Fluent
return val;
}
}
-
- public static readonly StyledProperty ModeProperty =
- AvaloniaProperty.Register(nameof(Mode));
-
+
public static readonly StyledProperty DensityStyleProperty =
AvaloniaProperty.Register(nameof(DensityStyle));
- ///
- /// Gets or sets the mode of the fluent theme (light, dark).
- ///
- public FluentThemeMode Mode
- {
- get => GetValue(ModeProperty);
- set => SetValue(ModeProperty, value);
- }
-
///
/// Gets or sets the density style of the fluent theme (normal, compact).
///
@@ -82,11 +55,6 @@ namespace Avalonia.Themes.Fluent
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
-
- if (change.Property == ModeProperty)
- {
- EnsureThemeVariants();
- }
if (change.Property == DensityStyleProperty)
{
@@ -94,23 +62,6 @@ namespace Avalonia.Themes.Fluent
}
}
- private void EnsureThemeVariants()
- {
- var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight;
- var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight;
- var dict = Resources.MergedDictionaries;
- if (dict.Count == 0)
- {
- dict.Add(themeVariantResource1);
- dict.Add(themeVariantResource2);
- }
- else
- {
- dict[0] = themeVariantResource1;
- dict[1] = themeVariantResource2;
- }
- }
-
private void EnsureCompactStyles()
{
if (DensityStyle == DensityStyle.Compact)
diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml
index bffdbd8a27..0c1354e475 100644
--- a/src/Avalonia.Themes.Simple/Accents/Base.xaml
+++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml
@@ -1,62 +1,131 @@
-
- #CC119EDA
- #99119EDA
- #66119EDA
- #33119EDA
- #FF808080
- #FFFFFFFF
- #FFFF0000
- #10FF0000
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1
- 0.5
+
+
+
+ #FFFFFFFF
+ #FFAAAAAA
+ #FF888888
+ #FF333333
+ #FF868999
+ #FFF5F5F5
+ #FFC2C3C9
+ #FF686868
+ #FF5B5B5B
+ #FFF0F0F0
+ #FFD0D0D0
+ #FF808080
+ #FF000000
+ #FF086F9E
- 10
- 12
- 16
+
+
+
+
+
+
+
+
+
+
+
+
+
- 18
- 8
+
+
+
+
+
+ #FF282828
+ #FF505050
+ #FF808080
+ #FFA0A0A0
+ #FF282828
+ #FF505050
+ #FF686868
+ #FF808080
+ #FFEFEBEF
+ #FFA8A8A8
+ #FF828282
+ #FF505050
+ #FFDEDEDE
+ #FF119EDA
- 20
- 20
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #CC119EDA
+ #99119EDA
+ #66119EDA
+ #33119EDA
+ #FF808080
+ #FFFFFFFF
+ #FFFF0000
+ #10FF0000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ 0.5
+
+ 10
+ 12
+ 16
+
+ 18
+ 8
+
+ 20
+ 20
diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml
deleted file mode 100644
index 88c2681f65..0000000000
--- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
- #FF282828
- #FF505050
- #FF808080
- #FFA0A0A0
- #FF282828
- #FF505050
- #FF686868
- #FF808080
- #FFEFEBEF
- #FFA8A8A8
- #FF828282
- #FF505050
- #FFDEDEDE
- #FF119EDA
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml
deleted file mode 100644
index 77166a9d8a..0000000000
--- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
- #FFFFFFFF
- #FFAAAAAA
- #FF888888
- #FF333333
- #FF868999
- #FFF5F5F5
- #FFC2C3C9
- #FF686868
- #FF5B5B5B
- #FFF0F0F0
- #FFD0D0D0
- #FF808080
- #FF000000
- #FF086F9E
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
index 093adaeab2..479db9ed09 100644
--- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
+++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
@@ -67,6 +67,7 @@
+
diff --git a/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml
new file mode 100644
index 0000000000..a6022fb263
--- /dev/null
+++ b/src/Avalonia.Themes.Simple/Controls/ThemeVariantScope.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml
index 5b0cae7fd2..f6d6ddfec9 100644
--- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml
+++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml
@@ -4,12 +4,8 @@
-
+
-
-
-
-
diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs
index 42dfafd7e0..31b3243993 100644
--- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs
+++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs
@@ -4,68 +4,16 @@ using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
-namespace Avalonia.Themes.Simple
+namespace Avalonia.Themes.Simple;
+
+public class SimpleTheme : Styles
{
- public class SimpleTheme : Styles
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The parent's service provider.
+ public SimpleTheme(IServiceProvider? sp = null)
{
- public static readonly StyledProperty ModeProperty =
- AvaloniaProperty.Register(nameof(Mode));
-
- private readonly IResourceDictionary _simpleDark;
- private readonly IResourceDictionary _simpleLight;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The parent's service provider.
- public SimpleTheme(IServiceProvider? sp = null)
- {
- AvaloniaXamlLoader.Load(sp, this);
-
- _simpleDark = (IResourceDictionary)GetAndRemove("BaseDark");
- _simpleLight = (IResourceDictionary)GetAndRemove("BaseLight");
- EnsureThemeVariant();
-
- object GetAndRemove(string key)
- {
- var val = Resources[key]
- ?? throw new KeyNotFoundException($"Key {key} was not found in the resources");
- Resources.Remove(key);
- return val;
- }
- }
-
- ///
- /// Gets or sets the mode of the fluent theme (light, dark).
- ///
- public SimpleThemeMode Mode
- {
- get => GetValue(ModeProperty);
- set => SetValue(ModeProperty, value);
- }
-
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
-
- if (change.Property == ModeProperty)
- {
- EnsureThemeVariant();
- }
- }
-
- private void EnsureThemeVariant()
- {
- var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight;
- var dict = Resources.MergedDictionaries;
- if (dict.Count == 0)
- {
- dict.Add(themeVariantResource);
- }
- else
- {
- dict[0] = themeVariantResource;
- }
- }
+ AvaloniaXamlLoader.Load(sp, this);
}
}
diff --git a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs b/src/Avalonia.Themes.Simple/SimpleThemeMode.cs
deleted file mode 100644
index 683c751f10..0000000000
--- a/src/Avalonia.Themes.Simple/SimpleThemeMode.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Avalonia.Themes.Simple
-{
- public enum SimpleThemeMode
- {
- Light,
- Dark
- }
-}
diff --git a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
index a9cad0538f..9017ce1546 100644
--- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
+++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
index 2dcce12df9..c3e90f5fd7 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
@@ -154,7 +154,7 @@ public static class LinuxFramebufferPlatformExtensions
var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend);
builder.SetupWithLifetime(lifetime);
lifetime.Start(args);
- builder.Instance.Run(lifetime.Token);
+ builder.Instance!.Run(lifetime.Token);
return lifetime.ExitCode;
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
index d61dcd4f91..0135cb3d1f 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
@@ -43,13 +43,13 @@ namespace Avalonia.LinuxFramebuffer.Output
public IPlatformGraphics PlatformGraphics { get; private set; }
public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo,
- DrmOutputOptions? options = null)
+ DrmOutputOptions options = null)
{
if(options != null)
_outputOptions = options;
Init(card, resources, connector, modeInfo);
}
- public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null)
+ public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions options = null)
{
if(options != null)
_outputOptions = options;
@@ -63,7 +63,7 @@ namespace Avalonia.LinuxFramebuffer.Output
if(connector == null)
throw new InvalidOperationException("Unable to find connected DRM connector");
- DrmModeInfo? mode = null;
+ DrmModeInfo mode = null;
if (options?.VideoMode != null)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index aaaee39b0d..197815f9a0 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -49,6 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBefore(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
+ new AvaloniaXamlIlDuplicateSettersChecker(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
index 925bf0a4fa..4068caac21 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
@@ -291,6 +291,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return true;
}
+ if (type.Equals(types.ThemeVariant))
+ {
+ var variantText = text.Trim();
+ var foundConstProperty = types.ThemeVariant.Properties.FirstOrDefault(p =>
+ p.Name == variantText && p.PropertyType == types.ThemeVariant);
+ var themeVariantTypeRef = new XamlAstClrTypeReference(node, types.ThemeVariant, false);
+ if (foundConstProperty is not null)
+ {
+ result = new XamlStaticExtensionNode(new XamlAstObjectNode(node, node.Type), themeVariantTypeRef, foundConstProperty.Name);
+ return true;
+ }
+ }
+
result = null;
return false;
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs
index 8c83c74248..db8d604154 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs
@@ -4,6 +4,8 @@ using System.Linq;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX.Ast;
using XamlX.IL.Emitters;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
#nullable enable
@@ -72,14 +74,20 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer
}
}
- var manipulationGroup = new XamlManipulationGroupNode(node, new List());
+ if (!mergeSourceNodes.Any())
+ {
+ return node;
+ }
+
+ var manipulationGroup = new List();
foreach (var sourceNode in mergeSourceNodes)
{
var (originalAssetPath, propertyNode) =
AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true);
if (originalAssetPath is null)
{
- return node;
+ return context.ParseError(
+ $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node);
}
var targetDocument = context.Documents.FirstOrDefault(d =>
@@ -99,15 +107,95 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer
$"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node);
}
- manipulationGroup.Children.Add(singleRootObject.Manipulation);
+ manipulationGroup.Add(singleRootObject.Manipulation);
}
+
+ // Order of resources is defined by ResourceDictionary.TryGetResource.
+ // It is read by following priority:
+ // - own resources.
+ // - own theme dictionaries.
+ // - merged dictionaries.
+ // We need to maintain this order when we inject "compiled merged" resources.
+ // Doing this by injecting merged dictionaries in the beginning, so it can be overwritten by "own resources".
+ // MergedDictionaries are read first, so we need ot inject our merged values in the beginning.
+ var children = resourceDictionaryManipulation.Children;
+ children.InsertRange(0, manipulationGroup);
- if (manipulationGroup.Children.Any())
+ // Flatten resource assignments.
+ for (var i = 0; i < children.Count; i++)
{
- // MergedDictionaries are read first, so we need ot inject our merged values in the beginning.
- resourceDictionaryManipulation.Children.Insert(0, manipulationGroup);
+ if (children[i] is XamlManipulationGroupNode group)
+ {
+ children.RemoveAt(i);
+ children.AddRange(group.Children);
+ i--; // step back, so new items can be reiterated.
+ }
+ }
+
+ // Merge "ThemeDictionaries" as well.
+ for (var i = children.Count - 1; i >= 0; i--)
+ {
+ if (children[i] is XamlPropertyAssignmentNode assignmentNode
+ && assignmentNode.Property.Name == "ThemeDictionaries"
+ && assignmentNode.Values.Count == 2
+ && assignmentNode.Values[0] is {} key
+ && assignmentNode.Values[1] is XamlValueWithManipulationNode
+ {
+ Manipulation: XamlObjectInitializationNode
+ {
+ Manipulation: XamlManipulationGroupNode valueGroup
+ }
+ })
+ {
+ for (var j = i - 1; j >= 0; j--)
+ {
+ if (children[j] is XamlPropertyAssignmentNode sameKeyPrevAssignmentNode
+ && sameKeyPrevAssignmentNode.Property.Name == "ThemeDictionaries"
+ && sameKeyPrevAssignmentNode.Values.Count == 2
+ && sameKeyPrevAssignmentNode.Values[1] is XamlValueWithManipulationNode
+ {
+ Manipulation: XamlObjectInitializationNode
+ {
+ Manipulation: XamlManipulationGroupNode sameKeyPrevValueGroup
+ }
+ }
+ && ThemeVariantNodeEquals(context, key, sameKeyPrevAssignmentNode.Values[0]))
+ {
+ sameKeyPrevValueGroup.Children.AddRange(valueGroup.Children);
+ children.RemoveAt(i);
+ break;
+ }
+ }
+ }
}
return node;
}
+
+ public static bool ThemeVariantNodeEquals(AstGroupTransformationContext context, IXamlAstValueNode left, IXamlAstValueNode right)
+ {
+ if (left is XamlConstantNode leftConst
+ && right is XamlConstantNode rightConst)
+ {
+ return leftConst.Constant == rightConst.Constant;
+ }
+ if (left is XamlStaticExtensionNode leftStaticExt
+ && right is XamlStaticExtensionNode rightStaticExt)
+ {
+ return leftStaticExt.Type.GetClrType().GetFullName() == rightStaticExt.Type.GetClrType().GetFullName()
+ && leftStaticExt.Member == rightStaticExt.Member;
+ }
+ if (left is XamlAstNewClrObjectNode leftClrObjectNode
+ && right is XamlAstNewClrObjectNode rightClrObjectNode)
+ {
+ var themeVariant = context.GetAvaloniaTypes().ThemeVariant;
+ return leftClrObjectNode.Type.GetClrType() == themeVariant
+ && leftClrObjectNode.Type == rightClrObjectNode.Type
+ && leftClrObjectNode.Constructor == rightClrObjectNode.Constructor
+ && ThemeVariantNodeEquals(context, leftClrObjectNode.Arguments.Single(),
+ leftClrObjectNode.Arguments.Single());
+ }
+
+ return false;
+ }
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
index 1338dc7248..51fe58d1c9 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlThemeTransformer.cs
@@ -11,7 +11,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
- if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.ControlTheme"))
+ if (node is not XamlAstObjectNode on ||
+ !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(on.Type.GetClrType()))
return node;
// Check if we've already transformed this node.
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs
new file mode 100644
index 0000000000..4ab9594cd8
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDuplicateSettersChecker.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Linq;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
+
+class AvaloniaXamlIlDuplicateSettersChecker : IXamlAstTransformer
+{
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is not XamlAstObjectNode objectNode)
+ {
+ return node;
+ }
+
+ var nodeType = objectNode.Type.GetClrType();
+ if (!context.GetAvaloniaTypes().Style.IsAssignableFrom(nodeType) &&
+ !context.GetAvaloniaTypes().ControlTheme.IsAssignableFrom(nodeType))
+ {
+ return node;
+ }
+
+ var properties = objectNode.Children
+ .OfType()
+ .Where(n => n.Type.GetClrType().Name == "Setter")
+ .SelectMany(setter =>
+ setter.Children.OfType()
+ .Where(c => c.Property.GetClrProperty().Name == "Property"))
+ .Select(p => p.Values[0])
+ .OfType()
+ .Select(x => x.Text);
+ var index = new HashSet();
+ foreach (var property in properties)
+ {
+ if (!index.Add(property))
+ {
+ throw new XamlParseException($"Duplicate setter encountered for property '{property}'", node);
+ }
+ }
+
+ return node;
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
index 078e23bc02..edddc5424a 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
@@ -19,7 +19,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
- if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style"))
+ if (node is not XamlAstObjectNode on ||
+ !context.GetAvaloniaTypes().Style.IsAssignableFrom(on.Type.GetClrType()))
return node;
var pn = on.Children.OfType()
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index 0aa3dda693..60a7d953ab 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -56,7 +56,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType DataTemplate { get; }
public IXamlType IDataTemplate { get; }
public IXamlType ItemsControl { get; }
- public IXamlType ItemsRepeater { get; }
public IXamlType ReflectionBindingExtension { get; }
public IXamlType RelativeSource { get; }
@@ -68,6 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
public IXamlConstructor ThicknessFullConstructor { get; }
+ public IXamlType ThemeVariant { get; }
public IXamlType Point { get; }
public IXamlConstructor PointFullConstructor { get; }
public IXamlType Vector { get; }
@@ -111,6 +111,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlMethod ResourceDictionaryDeferredAdd { get; }
public IXamlType UriKind { get; }
public IXamlConstructor UriConstructor { get; }
+ public IXamlType Style { get; }
+ public IXamlType ControlTheme { get; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@@ -181,7 +183,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate");
ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl");
- ItemsRepeater = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsRepeater");
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");
RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource");
UInt = cfg.TypeSystem.GetType("System.UInt32");
@@ -190,6 +191,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Uri = cfg.TypeSystem.GetType("System.Uri");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String });
+ ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");
(IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount)
{
@@ -248,6 +250,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
XamlIlTypes.Object));
UriKind = cfg.TypeSystem.GetType("System.UriKind");
UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind });
+ Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style");
+ ControlTheme = cfg.TypeSystem.GetType("Avalonia.Styling.ControlTheme");
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
index 84b4f3bdba..cdd344becc 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Markup.Data;
@@ -7,6 +8,8 @@ using Avalonia.Markup.Xaml.Converters;
using Avalonia.Markup.Xaml.XamlIl.Runtime;
using Avalonia.Styling;
+#nullable enable
+
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public class StaticResourceExtension
@@ -20,12 +23,18 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
ResourceKey = resourceKey;
}
- public object ResourceKey { get; set; }
+ public object? ResourceKey { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
+ if (ResourceKey is not { } resourceKey)
+ {
+ throw new ArgumentException("StaticResourceExtension.ResourceKey must be set.");
+ }
+
var stack = serviceProvider.GetService();
var provideTarget = serviceProvider.GetService();
+ var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant;
var targetType = provideTarget.TargetProperty switch
{
@@ -36,14 +45,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
if (provideTarget.TargetObject is Setter { Property: not null } setter)
{
- targetType = setter.Property.PropertyType;
+ targetType = setter.Property?.PropertyType;
}
// Look upwards though the ambient context for IResourceNodes
// which might be able to give us the resource.
foreach (var parent in stack.Parents)
{
- if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value))
+ if (parent is IResourceNode node && node.TryGetResource(resourceKey, themeVariant, out var value))
{
return ColorToBrushConverter.Convert(value, targetType);
}
@@ -60,12 +69,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
return AvaloniaProperty.UnsetValue;
}
- throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found.");
+ throw new KeyNotFoundException($"Static resource '{resourceKey}' not found.");
}
- private object GetValue(StyledElement control, Type targetType)
+ private object GetValue(StyledElement control, Type? targetType)
{
- return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType);
+ return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType);
}
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
index 595b37f7d1..4ff105cf1f 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
+using Avalonia.Styling;
#nullable enable
@@ -74,11 +75,11 @@ namespace Avalonia.Markup.Xaml.Styling
remove => Loaded.OwnerChanged -= value;
}
- bool IResourceNode.TryGetResource(object key, out object? value)
+ public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
{
if (!_isLoading)
{
- return Loaded.TryGetResource(key, out value);
+ return Loaded.TryGetResource(key, theme, out value);
}
value = null;
diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
index b87aa64297..27367fce5e 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
@@ -91,11 +91,11 @@ namespace Avalonia.Markup.Xaml.Styling
}
}
- public bool TryGetResource(object key, out object? value)
+ public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
{
if (!_isLoading)
{
- return Loaded.TryGetResource(key, out value);
+ return Loaded.TryGetResource(key, theme, out value);
}
value = null;
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
index 8d6f8cdf3a..da4d7374d4 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
@@ -34,7 +34,8 @@ namespace Avalonia.Markup.Xaml
}
- public class ConstructorArgumentAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class ConstructorArgumentAttribute : Attribute
{
public ConstructorArgumentAttribute(string name)
{
diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs
index a72929e06f..e58b296474 100644
--- a/src/Shared/ModuleInitializer.cs
+++ b/src/Shared/ModuleInitializer.cs
@@ -1,7 +1,8 @@
namespace System.Runtime.CompilerServices
{
#if NETSTANDARD2_0
- internal class ModuleInitializerAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class ModuleInitializerAttribute : Attribute
{
}
diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs
index 3f00fbef57..bdd21d0426 100644
--- a/src/Shared/SourceGeneratorAttributes.cs
+++ b/src/Shared/SourceGeneratorAttributes.cs
@@ -16,7 +16,9 @@ namespace Avalonia.SourceGenerator
}
- internal class GetProcAddressAttribute : Attribute
+
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GetProcAddressAttribute : Attribute
{
public GetProcAddressAttribute(string proc)
{
@@ -39,11 +41,14 @@ namespace Avalonia.SourceGenerator
}
}
- internal class GenerateEnumValueDictionaryAttribute : Attribute
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GenerateEnumValueDictionaryAttribute : Attribute
{
}
- internal class GenerateEnumValueListAttribute : Attribute
+
+ [AttributeUsage(AttributeTargets.Method)]
+ internal sealed class GenerateEnumValueListAttribute : Attribute
{
}
}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index dcb20d2a44..ba646c64ee 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -208,6 +208,12 @@ namespace Avalonia.Skia
public void DrawLine(IPen pen, Point p1, Point p2)
{
CheckLease();
+
+ if (pen is null)
+ {
+ return;
+ }
+
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
if (paint.Paint is object)
@@ -495,6 +501,12 @@ namespace Avalonia.Skia
public void DrawGlyphRun(IBrush foreground, IRef glyphRun)
{
CheckLease();
+
+ if (foreground is null)
+ {
+ return;
+ }
+
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.Item;
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index d12db39ad6..e795f3d304 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -86,7 +86,7 @@ namespace Avalonia.Skia
SKPath path = new SKPath();
- var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
+ var (currentX, currentY) = glyphRun.BaselineOrigin;
for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
{
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index eb3f9911df..99c01dd111 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1
sink.Close();
}
- var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
+ var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
Direct2D1Factory,
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
index 24b8fc04b3..446db47d92 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Avalonia.Platform;
using SharpDX.DirectWrite;
@@ -25,8 +26,6 @@ namespace Avalonia.Direct2D1.Media
}
public IReadOnlyList GetIntersections(float lowerBound, float upperBound)
- {
- return null;
- }
+ => Array.Empty();
}
}
diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
index cc8a40e2d4..60bb75a342 100644
--- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
+++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
@@ -13,9 +13,6 @@
-
-
-
diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
index aff533c443..2d0f351d58 100644
--- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
+++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
@@ -12,6 +12,7 @@ using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
using Key = Avalonia.Input.Key;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using MouseButton = System.Windows.Input.MouseButton;
@@ -90,8 +91,7 @@ namespace Avalonia.Win32.Interop.Wpf
public IRenderer CreateRenderer(IRenderRoot root)
{
- var mgr = new PlatformRenderInterfaceContextManager(null);
- return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr);
+ return new CompositingRenderer(root, Win32Platform.Compositor, () => _surfaces);
}
public void Dispose()
@@ -134,7 +134,7 @@ namespace Avalonia.Win32.Interop.Wpf
drawingContext.DrawImage(ImageSource, new System.Windows.Rect(0, 0, ActualWidth, ActualHeight));
}
- void ITopLevelImpl.Invalidate(Rect rect) => InvalidateVisual();
+
void ITopLevelImpl.SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot;
diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs
index 05fa9d9426..04b4a53580 100644
--- a/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs
+++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WritableBitmapSurface.cs
@@ -25,7 +25,7 @@ namespace Avalonia.Win32.Interop.Wpf
if (_bitmap == null || _bitmap.PixelWidth != (int) size.Width || _bitmap.PixelHeight != (int) size.Height)
{
_bitmap = new WriteableBitmap((int) size.Width, (int) size.Height, dpi.X, dpi.Y,
- PixelFormats.Bgra32, null);
+ System.Windows.Media.PixelFormats.Bgra32, null);
}
return new LockedFramebuffer(_impl, _bitmap, dpi);
}
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index eb51b7fd07..a24fe31df8 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -10,7 +10,7 @@
-
+
@@ -27,4 +27,7 @@
$(NoWarn);CA1416
+
+
+
diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
index 0c0fe5b921..f3af312d1a 100644
--- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
+++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
index 181883656c..690926a193 100644
--- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
+++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
@@ -1,16 +1,79 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Reflection;
+using System.Text.RegularExpressions;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.XamlIl;
-namespace Avalonia.Designer.HostApp
+namespace Avalonia.Designer.HostApp;
+
+class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
{
- class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
+ public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
+ {
+ PreloadDepsAssemblies(configuration.LocalAssembly ?? Assembly.GetEntryAssembly());
+
+ return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration);
+ }
+
+ private void PreloadDepsAssemblies(Assembly targetAssembly)
{
- public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
+ // Assemblies loaded in memory (e.g. single file) return empty string from Location.
+ // In these cases, don't try probing next to the assembly.
+ var assemblyLocation = targetAssembly.Location;
+ if (string.IsNullOrEmpty(assemblyLocation))
+ {
+ return;
+ }
+
+ var depsJsonFile = Path.ChangeExtension(assemblyLocation, ".deps.json");
+ if (!File.Exists(depsJsonFile))
+ {
+ return;
+ }
+
+ using var stream = File.OpenRead(depsJsonFile);
+
+ /*
+ We can't use any references in the Avalonia.Designer.HostApp. Including even json.
+ Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here.
+ So, instead we need to fallback to some JSON parsing using pretty easy regex.
+
+ Json part example:
+"Avalonia.Xaml.Interactions/11.0.0-preview5": {
+ "dependencies": {
+ "Avalonia": "11.0.999",
+ "Avalonia.Xaml.Interactivity": "11.0.0-preview5"
+ },
+ "runtime": {
+ "lib/net6.0/Avalonia.Xaml.Interactions.dll": {
+ "assemblyVersion": "11.0.0.0",
+ "fileVersion": "11.0.0.0"
+ }
+ }
+},
+ We want to extract "lib/net6.0/Avalonia.Xaml.Interactions.dll" from here.
+ No need to resolve real path of ref assemblies.
+ No need to handle special cases with .NET Framework and GAC.
+ */
+ var text = new StreamReader(stream).ReadToEnd();
+ var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)""");
+
+ foreach (Match match in matches)
{
- return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration);
+ if (match.Groups[1] is { Success: true } g)
+ {
+ var assemblyName = Path.GetFileNameWithoutExtension(g.Value);
+ try
+ {
+ _ = Assembly.Load(new AssemblyName(assemblyName));
+ }
+ catch
+ {
+ }
+ }
}
}
}
diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
index 135ab0426e..c293a9101d 100644
--- a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
+++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs
@@ -112,7 +112,7 @@ class Template
var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults");
- cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements(
+ cl = cl.ReplaceNode(defs.Body!, defs.Body!.AddStatements(
ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);")));
diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
index 3b5d3d8c3f..dfc8b45579 100644
--- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs
+++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs
@@ -297,8 +297,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
server = server.WithBaseList(
server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
- client = client.AddMembers(
- ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;"));
+ if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member)
+ client = client.AddMembers(member);
}
diff --git a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
index 86dbb3a452..c975bb8444 100644
--- a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
+++ b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs
@@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator
).Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
- foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
+ foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var classBuilder = new StringBuilder();
if (typeGroup.Key.ContainingNamespace != null)
diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs
index aedc13e7f6..e8d7c251fa 100644
--- a/src/tools/DevGenerators/GetProcAddressInitialization.cs
+++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs
@@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator
var all = fieldsWithAttribute.Collect();
context.RegisterSourceOutput(all, static (context, methods) =>
{
- foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
+ foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default))
{
var nextContext = 0;
var contexts = new Dictionary();
diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
index 466aba43ee..3d7dc66cc4 100644
--- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
@@ -1,15 +1,7 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Controls;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Templates;
+using Avalonia.Controls;
using Avalonia.Input;
-using Avalonia.Input.Raw;
using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering;
using Avalonia.UnitTests;
-using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Input
@@ -21,7 +13,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = new MouseDevice();
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -59,7 +51,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = new MouseDevice();
var impl = CreateTopLevelImplMock(renderer.Object);
diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
index 1ac50446c0..629188800a 100644
--- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -50,7 +50,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -93,7 +93,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -119,7 +119,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var pointer = new Mock();
var device = CreatePointerDeviceMock(pointer.Object).Object;
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -155,7 +155,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var device = CreatePointerDeviceMock().Object;
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -201,7 +201,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>();
@@ -256,7 +256,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string)>();
@@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests.Input
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
var expectedPosition = new Point(15, 15);
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
var result = new List<(object?, string, Point)>();
@@ -351,7 +351,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -405,7 +405,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
@@ -442,7 +442,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var deviceMock = CreatePointerDeviceMock();
var impl = CreateTopLevelImplMock(renderer.Object);
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
index 07d2d672ae..c1468a28e4 100644
--- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
double height,
double scaleX,
double scaleY,
- double? penThickness,
+ double penThickness,
double expectedX,
double expectedY,
double expectedWidth,
@@ -38,7 +38,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
var target = new TestRectangleDrawOperation(
new Rect(x, y, width, height),
Matrix.CreateScale(scaleX, scaleY),
- penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null);
+ new Pen(Brushes.Black, penThickness));
Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds);
}
diff --git a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs
index 86b1b897d4..5527eda6ee 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Styling/ResourceDictionaryTests.cs
@@ -29,7 +29,7 @@ namespace Avalonia.Base.UnitTests.Styling
{ "foo", "bar" },
};
- Assert.True(target.TryGetResource("foo", out var result));
+ Assert.True(target.TryGetResource("foo", null, out var result));
Assert.Equal("bar", result);
}
@@ -47,7 +47,7 @@ namespace Avalonia.Base.UnitTests.Styling
}
};
- Assert.True(target.TryGetResource("foo", out var result));
+ Assert.True(target.TryGetResource("foo", null, out var result));
Assert.Equal("bar", result);
}
@@ -64,7 +64,7 @@ namespace Avalonia.Base.UnitTests.Styling
{ "foo", "baz" },
});
- Assert.True(target.TryGetResource("foo", out var result));
+ Assert.True(target.TryGetResource("foo", null, out var result));
Assert.Equal("bar", result);
}
@@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.Styling
}
};
- Assert.True(target.TryGetResource("foo", out var result));
+ Assert.True(target.TryGetResource("foo", null, out var result));
Assert.Equal("baz", result);
}
diff --git a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs
index a6777c9466..c9fc86e205 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Styling/StylesTests.cs
@@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Styling
}
};
- Assert.True(target.TryGetResource("foo", out var result));
+ Assert.True(target.TryGetResource("foo", ThemeVariant.Dark, out var result));
Assert.Equal("bar", result);
}
}
diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs
index fb214a6b34..11bdc4bc68 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTests.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs
@@ -150,7 +150,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Attaching_To_Visual_Tree_Should_Invalidate_Visual()
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var child = new Decorator();
var root = new TestRoot
{
@@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests
[Fact]
public void Detaching_From_Visual_Tree_Should_Invalidate_Visual()
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var child = new Decorator();
var root = new TestRoot
{
@@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests
public void Changing_ZIndex_Should_InvalidateVisual()
{
Canvas canvas1;
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var root = new TestRoot
{
Child = new StackPanel
@@ -331,7 +331,7 @@ namespace Avalonia.Base.UnitTests
{
Canvas canvas1;
StackPanel stackPanel;
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var root = new TestRoot
{
Child = stackPanel = new StackPanel
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index 0ddee2ad7a..9ea0482abc 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs
deleted file mode 100644
index feb325f630..0000000000
--- a/tests/Avalonia.Benchmarks/NullRenderer.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Avalonia.Rendering;
-using Avalonia.VisualTree;
-
-namespace Avalonia.Benchmarks
-{
- internal class NullRenderer : IRenderer
- {
- public bool DrawFps { get; set; }
- public bool DrawDirtyRects { get; set; }
-#pragma warning disable CS0067
- public event EventHandler SceneInvalidated;
-#pragma warning restore CS0067
- public void AddDirty(Visual visual)
- {
- }
-
- public void Dispose()
- {
- }
-
- public IEnumerable HitTest(Point p, Visual root, Func filter) => null;
-
- public Visual HitTestFirst(Point p, Visual root, Func filter) => null;
-
- public void Paint(Rect rect)
- {
- }
-
- public void RecalculateChildren(Visual visual)
- {
- }
-
- public void Resized(Size size)
- {
- }
-
- public void Start()
- {
- }
-
- public void Stop()
- {
- }
-
- public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(0);
- }
-}
diff --git a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
index 59953f457a..bc47e68bc1 100644
--- a/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
+++ b/tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Benchmarks.Styling
return new Styles
{
preHost,
- new TestStyles(50, 3, 5),
+ new TestStyles(50, 3, 5, 0),
postHost
};
}
diff --git a/tests/Avalonia.Benchmarks/TestStyles.cs b/tests/Avalonia.Benchmarks/TestStyles.cs
index be2ad7d072..208f238101 100644
--- a/tests/Avalonia.Benchmarks/TestStyles.cs
+++ b/tests/Avalonia.Benchmarks/TestStyles.cs
@@ -1,10 +1,11 @@
-using Avalonia.Styling;
+using Avalonia.Controls;
+using Avalonia.Styling;
namespace Avalonia.Benchmarks
{
public class TestStyles : Styles
{
- public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount)
+ public TestStyles(int childStylesCount, int childInnerStyleCount, int childResourceCount, int childThemeResourcesCount)
{
for (int i = 0; i < childStylesCount; i++)
{
@@ -18,7 +19,19 @@ namespace Avalonia.Benchmarks
{
childStyle.Resources.Add($"resource.{i}.{j}.{k}", null);
}
-
+
+ if (childThemeResourcesCount > 0)
+ {
+ ResourceDictionary darkTheme, lightTheme;
+ childStyle.Resources.ThemeDictionaries[ThemeVariant.Dark] = darkTheme = new ResourceDictionary();
+ childStyle.Resources.ThemeDictionaries[ThemeVariant.Light] = lightTheme = new ResourceDictionary();
+ for (int k = 0; k < childThemeResourcesCount; k++)
+ {
+ darkTheme.Add($"resource.theme.{i}.{j}.{k}", null);
+ lightTheme.Add($"resource.theme.{i}.{j}.{k}", null);
+ }
+ }
+
childStyles.Add(childStyle);
}
diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
index 4dad8442de..03b85840a7 100644
--- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
+++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Benchmarks.Text;
public class HugeTextLayout : IDisposable
{
private static readonly Random s_rand = new();
- private static readonly bool s_useSkia = true;
+ private static readonly bool s_useSkia = false;
private readonly IDisposable _app;
private readonly string[] _manySmallStrings;
diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs
index 70636d1fe6..7c0a3f8bdf 100644
--- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs
+++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs
@@ -29,26 +29,16 @@ namespace Avalonia.Benchmarks.Themes
}
[Benchmark]
- [Arguments(FluentThemeMode.Dark)]
- [Arguments(FluentThemeMode.Light)]
- public bool InitFluentTheme(FluentThemeMode mode)
+ public bool InitFluentTheme()
{
- UnitTestApplication.Current.Styles[0] = new FluentTheme()
- {
- Mode = mode
- };
+ UnitTestApplication.Current.Styles[0] = new FluentTheme();
return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _);
}
[Benchmark]
- [Arguments(SimpleThemeMode.Dark)]
- [Arguments(SimpleThemeMode.Light)]
- public bool InitSimpleTheme(SimpleThemeMode mode)
+ public bool InitSimpleTheme()
{
- UnitTestApplication.Current.Styles[0] = new SimpleTheme()
- {
- Mode = mode
- };
+ UnitTestApplication.Current.Styles[0] = new SimpleTheme();
return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _);
}
@@ -58,7 +48,7 @@ namespace Avalonia.Benchmarks.Themes
[Arguments(typeof(DatePicker))]
public object FindFluentControlTheme(Type type)
{
- _reusableFluentTheme.TryGetResource(type, out var theme);
+ _reusableFluentTheme.TryGetResource(type, ThemeVariant.Default, out var theme);
return theme;
}
@@ -68,7 +58,7 @@ namespace Avalonia.Benchmarks.Themes
[Arguments(typeof(DatePicker))]
public object FindSimpleControlTheme(Type type)
{
- _reusableSimpleTheme.TryGetResource(type, out var theme);
+ _reusableSimpleTheme.TryGetResource(type, ThemeVariant.Default, out var theme);
return theme;
}
diff --git a/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj
new file mode 100644
index 0000000000..6f9815757e
--- /dev/null
+++ b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/Avalonia.Controls.ItemsRepeater.UnitTests.csproj
@@ -0,0 +1,22 @@
+
+
+ net6.0
+ Library
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs
similarity index 100%
rename from tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs
rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs
diff --git a/tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs
similarity index 100%
rename from tests/Avalonia.Base.UnitTests/Layout/NonVirtualizingStackLayoutTests.cs
rename to tests/Avalonia.Controls.ItemsRepeater.UnitTests/NonVirtualizingStackLayoutTests.cs
diff --git a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
index 8bd51ec500..2679d4ce06 100644
--- a/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ButtonTests.cs
@@ -134,16 +134,15 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_Raises_Click()
{
- var renderer = Mock.Of();
+ var renderer = RendererMocks.CreateRenderer();
var pt = new Point(50, 50);
- Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
.Returns>((p, r, f) =>
r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]);
- var target = new TestButton()
+ var target = new TestButton(renderer.Object)
{
- Bounds = new Rect(0, 0, 100, 100),
- Renderer = renderer
+ Bounds = new Rect(0, 0, 100, 100)
};
bool clicked = false;
@@ -166,16 +165,15 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
{
- var renderer = Mock.Of();
-
- Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ var renderer = RendererMocks.CreateRenderer();
+
+ renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
.Returns>((p, r, f) =>
r.Bounds.Contains(p) ? new Visual[] { r } : new Visual[0]);
- var target = new TestButton()
+ var target = new TestButton(renderer.Object)
{
- Bounds = new Rect(0, 0, 100, 100),
- Renderer = renderer
+ Bounds = new Rect(0, 0, 100, 100)
};
bool clicked = false;
@@ -199,18 +197,17 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Button_With_RenderTransform_Raises_Click()
{
- var renderer = Mock.Of();
+ var renderer = RendererMocks.CreateRenderer();
var pt = new Point(150, 50);
- Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ renderer.Setup(r => r.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
.Returns>((p, r, f) =>
r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
new Visual[] { r } : new Visual[0]);
- var target = new TestButton()
+ var target = new TestButton(renderer.Object)
{
Bounds = new Rect(0, 0, 100, 100),
- RenderTransform = new TranslateTransform { X = 100, Y = 0 },
- Renderer = renderer
+ RenderTransform = new TranslateTransform { X = 100, Y = 0 }
};
//actual bounds of button should be 100,0,100,100 x -> translated 100 pixels
@@ -386,9 +383,10 @@ namespace Avalonia.Controls.UnitTests
private class TestButton : Button, IRenderRoot
{
- public TestButton()
+ public TestButton(IRenderer renderer)
{
IsVisible = true;
+ Renderer = renderer;
}
public new Rect Bounds
@@ -399,7 +397,7 @@ namespace Avalonia.Controls.UnitTests
public Size ClientSize => throw new NotImplementedException();
- public IRenderer Renderer { get; set; }
+ public IRenderer Renderer { get; }
public double RenderScaling => throw new NotImplementedException();
diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
index baf933bd66..d99c90cb77 100644
--- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
@@ -595,7 +595,7 @@ namespace Avalonia.Controls.UnitTests
private static Window PreparedWindow(object content = null)
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object);
diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
index 3a2e1c08bd..8cd5816984 100644
--- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
+using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@@ -189,6 +190,8 @@ namespace Avalonia.Controls.UnitTests
public void Impl_Closing_Should_Remove_Window_From_OpenWindows()
{
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
index 02767a21eb..7767de11c7 100644
--- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
@@ -569,7 +569,7 @@ namespace Avalonia.Controls.UnitTests
private static Window PreparedWindow(object content = null)
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object);
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index f4206959a9..4804b29fee 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -563,7 +563,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
{
using (CreateServices())
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var platform = AvaloniaLocator.Current.GetRequiredService();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object);
diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
index d63251c1f5..2644e7184a 100644
--- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
@@ -6,6 +6,7 @@ using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Platform;
+using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Moq;
@@ -20,7 +21,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
var target = new TestTopLevel(impl.Object);
Assert.True(((ILogical)target).IsAttachedToLogicalTree);
@@ -32,7 +33,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
@@ -46,7 +47,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
@@ -60,7 +61,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
@@ -76,7 +77,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(services))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
var target = new TestTopLevel(impl.Object, Mock.Of());
@@ -91,7 +92,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupProperty(x => x.Resized);
impl.SetupGet(x => x.RenderScaling).Returns(1);
@@ -117,7 +118,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
@@ -133,7 +134,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
@@ -151,7 +152,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
bool raised = false;
@@ -169,7 +170,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
var target = new TestTopLevel(impl.Object);
@@ -200,7 +201,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(services))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
var target = new TestTopLevel(impl.Object);
@@ -222,7 +223,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
var target = new TestTopLevel(impl.Object);
var child = new TestTopLevel(impl.Object);
@@ -240,7 +241,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
var target = new TestTopLevel(impl.Object);
var raised = false;
@@ -257,7 +258,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupAllProperties();
var layoutManager = new Mock();
@@ -274,7 +275,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockTopLevelImpl();
impl.SetupGet(x => x.RenderScaling).Returns(1);
var child = new Border { Classes = { "foo" } };
@@ -317,6 +318,14 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope));
}
+ private static Mock CreateMockTopLevelImpl()
+ {
+ var renderer = new Mock();
+ renderer.Setup(r => r.CreateRenderer(It.IsAny()))
+ .Returns(RendererMocks.CreateRenderer().Object);
+ return renderer;
+ }
+
private class TestTopLevel : TopLevel
{
private readonly ILayoutManager _layoutManager;
diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
index f367112cc0..336aad79da 100644
--- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
@@ -4,9 +4,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
-using Avalonia.Styling;
using Avalonia.UnitTests;
-using Moq;
using Xunit;
using Factory = System.Func, Avalonia.Controls.Window, Avalonia.AvaloniaObject>;
@@ -20,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Utils
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new WindowingPlatformMock());
+ .Bind().ToConstant(new MockWindowingPlatform());
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
var gesture2 = new KeyGesture(Key.B, KeyModifiers.Control);
@@ -64,7 +62,7 @@ namespace Avalonia.Controls.UnitTests.Utils
var commandResult = 0;
var expectedParameter = 1;
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new WindowingPlatformMock());
+ .Bind().ToConstant(new MockWindowingPlatform());
var gesture = new KeyGesture(Key.A, KeyModifiers.Control);
@@ -106,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Utils
var target = new KeyboardDevice();
var isExecuted = false;
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new WindowingPlatformMock());
+ .Bind().ToConstant(new MockWindowingPlatform());
var gesture = new KeyGesture(Key.A, KeyModifiers.Control);
@@ -146,7 +144,7 @@ namespace Avalonia.Controls.UnitTests.Utils
var target = new KeyboardDevice();
var clickExecutedCount = 0;
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new WindowingPlatformMock());
+ .Bind().ToConstant(new MockWindowingPlatform());
var gesture = new KeyGesture(Key.A, KeyModifiers.Control);
@@ -199,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Utils
var clickExecutedCount = 0;
var commandExecutedCount = 0;
AvaloniaLocator.CurrentMutable
- .Bind().ToConstant(new WindowingPlatformMock());
+ .Bind().ToConstant(new MockWindowingPlatform());
var gesture = new KeyGesture(Key.A, KeyModifiers.Control);
diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
index 8f9af52ed8..d65fa06183 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
@@ -22,7 +22,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockWindowBaseImpl();
var target = new TestWindowBase(impl.Object);
target.Activate();
@@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockWindowBaseImpl();
impl.SetupAllProperties();
bool raised = false;
@@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var impl = new Mock();
+ var impl = CreateMockWindowBaseImpl();
impl.SetupAllProperties();
bool raised = false;
@@ -110,6 +110,8 @@ namespace Avalonia.Controls.UnitTests
public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close()
{
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.SetupProperty(x => x.Closed);
@@ -129,6 +131,8 @@ namespace Avalonia.Controls.UnitTests
public void Setting_IsVisible_True_Shows_Window()
{
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
@@ -145,6 +149,8 @@ namespace Avalonia.Controls.UnitTests
public void Setting_IsVisible_False_Hides_Window()
{
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
@@ -163,7 +169,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var target = new TestWindowBase(renderer.Object);
target.Show();
@@ -194,7 +200,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var target = new TestWindowBase(renderer.Object);
target.Show();
@@ -209,7 +215,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var windowImpl = new Mock();
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
@@ -235,17 +241,28 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope));
}
+ private static Mock CreateMockWindowBaseImpl()
+ {
+ var renderer = new Mock();
+ renderer.Setup(r => r.CreateRenderer(It.IsAny()))
+ .Returns(RendererMocks.CreateRenderer().Object);
+ return renderer;
+ }
+
private class TestWindowBase : WindowBase
{
public bool IsClosed { get; private set; }
public TestWindowBase(IRenderer renderer = null)
- : base(Mock.Of(x =>
- x.RenderScaling == 1 &&
- x.CreateRenderer(It.IsAny()) == renderer))
+ : base(CreateWindowsBaseImplMock(renderer ?? RendererMocks.CreateRenderer().Object))
{
}
+ private static IWindowBaseImpl CreateWindowsBaseImplMock(IRenderer renderer)
+ => Mock.Of(x =>
+ x.RenderScaling == 1 &&
+ x.CreateRenderer(It.IsAny()) == renderer);
+
public TestWindowBase(IWindowBaseImpl impl)
: base(impl)
{
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index ca245005c2..cada2bfa6f 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -15,6 +15,8 @@ namespace Avalonia.Controls.UnitTests
public void Setting_Title_Should_Set_Impl_Title()
{
var windowImpl = new Mock();
+ windowImpl.Setup(r => r.CreateRenderer(It.IsAny()))
+ .Returns(RendererMocks.CreateRenderer().Object);
var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);
using (UnitTestApplication.Start(new TestServices(windowingPlatform: windowingPlatform)))
@@ -98,6 +100,8 @@ namespace Avalonia.Controls.UnitTests
public void IsVisible_Should_Be_False_After_Impl_Signals_Close()
{
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
@@ -269,7 +273,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer));
target.Show();
@@ -284,7 +288,7 @@ namespace Avalonia.Controls.UnitTests
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = new Window();
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer));
parent.Show();
@@ -317,7 +321,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
- var renderer = new Mock();
+ var renderer = RendererMocks.CreateRenderer();
var target = new Window(CreateImpl(renderer));
target.Show();
@@ -334,6 +338,8 @@ namespace Avalonia.Controls.UnitTests
{
var parent = new Window();
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
@@ -375,6 +381,8 @@ namespace Avalonia.Controls.UnitTests
{
var parent = new Window();
var windowImpl = new Mock();
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(() => RendererMocks.CreateRenderer().Object);
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
deleted file mode 100644
index e8471d41fb..0000000000
--- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-using Moq;
-using Avalonia.Platform;
-
-namespace Avalonia.Controls.UnitTests
-{
- public class WindowingPlatformMock : IWindowingPlatform
- {
- private readonly Func _windowImpl;
- private readonly Func