Browse Source

Merge branch 'master' into move-name-generator

pull/10407/head
Max Katz 4 years ago
committed by GitHub
parent
commit
60b3467123
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.Desktop.slnf
  2. 17
      Avalonia.sln
  3. 9
      azure-pipelines-integrationtests.yml
  4. 8
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  5. 104
      nukebuild/BuildTasksPatcher.cs
  6. 1
      samples/ControlCatalog/ControlCatalog.csproj
  7. 8
      samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs
  8. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  9. 8
      samples/ControlCatalog/Pages/PointerCanvas.cs
  10. 1
      samples/IntegrationTestApp/IntegrationTestApp.csproj
  11. 11
      samples/IntegrationTestApp/MainWindow.axaml
  12. 12
      samples/IntegrationTestApp/MainWindow.axaml.cs
  13. 22
      samples/IntegrationTestApp/Program.cs
  14. 20
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  15. 5
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  16. 4
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  17. 5
      src/Avalonia.Base/AvaloniaObject.cs
  18. 4
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  19. 13
      src/Avalonia.Base/AvaloniaProperty.cs
  20. 18
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  21. 27
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  22. 5
      src/Avalonia.Base/Input/AccessKeyHandler.cs
  23. 1
      src/Avalonia.Base/Input/MouseDevice.cs
  24. 1
      src/Avalonia.Base/Input/PenDevice.cs
  25. 69
      src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
  26. 2
      src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
  27. 66
      src/Avalonia.Base/Media/DrawingBrush.cs
  28. 404
      src/Avalonia.Base/Media/DrawingContext.cs
  29. 141
      src/Avalonia.Base/Media/DrawingGroup.cs
  30. 2
      src/Avalonia.Base/Media/DrawingImage.cs
  31. 4
      src/Avalonia.Base/Media/GeometryDrawing.cs
  32. 31
      src/Avalonia.Base/Media/ISceneBrush.cs
  33. 16
      src/Avalonia.Base/Media/IVisualBrush.cs
  34. 2
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  35. 18
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  36. 6
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  37. 66
      src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
  38. 112
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  39. 3
      src/Avalonia.Base/Media/PolyLineSegment.cs
  40. 6
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  41. 98
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  42. 6
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  43. 2
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  44. 24
      src/Avalonia.Base/Media/VisualBrush.cs
  45. 6
      src/Avalonia.Base/Platform/IRenderTarget.cs
  46. 76
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  47. 11
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  48. 5
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  49. 51
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  50. 26
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  51. 11
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  52. 8
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  53. 6
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  54. 114
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  55. 133
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  56. 94
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  57. 3
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  58. 6
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  59. 3
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  60. 37
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  61. 4
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  62. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  63. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  64. 183
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  65. 30
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  66. 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  67. 29
      src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
  68. 37
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  69. 15
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  70. 4
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  71. 39
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  72. 10
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  73. 37
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  74. 24
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  75. 4
      src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
  76. 30
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  77. 33
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
  78. 10
      src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
  79. 21
      src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
  80. 19
      src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
  81. 41
      src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
  82. 52
      src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
  83. 22
      src/Avalonia.Base/StyledElement.cs
  84. 6
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  85. 47
      src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
  86. 5
      src/Avalonia.Base/Styling/NthChildSelector.cs
  87. 13
      src/Avalonia.Base/Styling/OrSelector.cs
  88. 2
      src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
  89. 8
      src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
  90. 10
      src/Avalonia.Base/Styling/Setter.cs
  91. 15
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  92. 7
      src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
  93. 10
      src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs
  94. 17
      src/Avalonia.Base/Visual.cs
  95. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  96. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
  97. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  98. 2
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  99. 9
      src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
  100. 2
      src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs

1
Avalonia.Desktop.slnf

@ -21,6 +21,7 @@
"src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",
"src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj",
"src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj",
"src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj",
"src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj",
"src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj",
"src\\Avalonia.Headless\\Avalonia.Headless.csproj",

17
Avalonia.sln

@ -248,6 +248,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -559,9 +561,14 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
{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
@ -570,10 +577,6 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -582,6 +585,10 @@ Global
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -647,10 +654,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

9
azure-pipelines-integrationtests.yml

@ -1,11 +1,3 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
jobs:
- job: Mac
pool:
@ -25,6 +17,7 @@ jobs:
- script: system_profiler SPDisplaysDataType |grep Resolution
- script: |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
pkill node
appium &
pkill IntegrationTestApp

8
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -238,7 +238,7 @@
-(BOOL)canBecomeKeyWindow
{
if(_canBecomeKeyWindow)
if(_canBecomeKeyWindow && !_closed)
{
// If the window has a child window being shown as a dialog then don't allow it to become the key window.
auto parent = dynamic_cast<WindowImpl*>(_parent.getRaw());
@ -292,12 +292,14 @@
{
if (_parent == nullptr)
return;
_parent->BringToFront();
dispatch_async(dispatch_get_main_queue(), ^{
@try {
[self invalidateShadow];
[self invalidateShadow];
if (self->_parent != nullptr)
self->_parent->BringToFront();
}
@finally{
}

104
nukebuild/BuildTasksPatcher.cs

@ -4,9 +4,58 @@ using System.IO.Compression;
using System.Linq;
using ILRepacking;
using Mono.Cecil;
using Mono.Cecil.Cil;
public class BuildTasksPatcher
{
/// <summary>
/// This helper class, avoid argument null exception
/// when cecil write AssemblyNameDefinition on MemoryStream.
/// </summary>
private class Wrapper : ISymbolWriterProvider
{
readonly ISymbolWriterProvider _provider;
readonly string _filename;
public Wrapper(ISymbolWriterProvider provider, string filename)
{
_provider = provider;
_filename = filename;
}
public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName) =>
_provider.GetSymbolWriter(module, string.IsNullOrWhiteSpace(fileName) ? _filename : fileName);
public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream) =>
_provider.GetSymbolWriter(module, symbolStream);
}
private static string GetSourceLinkInfo(string path)
{
try
{
using (var asm = AssemblyDefinition.ReadAssembly(path,
new ReaderParameters
{
ReadWrite = true,
InMemory = true,
ReadSymbols = true,
SymbolReaderProvider = new DefaultSymbolReaderProvider(false),
}))
{
if (asm.MainModule.CustomDebugInformations?.OfType<SourceLinkDebugInformation>()?.FirstOrDefault() is { } sli)
{
return sli.Content;
}
}
}
catch
{
}
return null;
}
public static void PatchBuildTasksInPackage(string packagePath)
{
using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite),
@ -19,7 +68,7 @@ public class BuildTasksPatcher
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll");
var temp = Path.Combine(tempDir, entry.Name);
var output = temp + ".output";
File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName,
Path.Combine(tempDir, "Microsoft.Build.Framework.dll"));
@ -27,41 +76,74 @@ public class BuildTasksPatcher
try
{
entry.ExtractToFile(temp, true);
// Get Original SourceLinkInfo Content
var sourceLinkInfoContent = GetSourceLinkInfo(temp);
var repack = new ILRepacking.ILRepack(new RepackOptions()
{
Internalize = true,
InputAssemblies = new[]
{
temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0]
.FullyQualifiedName,
temp,
typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0].FullyQualifiedName,
typeof(Mono.Cecil.Rocks.MethodBodyRocks).Assembly.GetModules()[0].FullyQualifiedName,
typeof(Mono.Cecil.Pdb.PdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName,
typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName
typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName,
},
SearchDirectories = new string[0],
SearchDirectories = Array.Empty<string>(),
DebugInfo = true, // Allowed read debug info
OutputFile = output
});
repack.Repack();
// 'hurr-durr assembly with the same name is already loaded' prevention
using (var asm = AssemblyDefinition.ReadAssembly(output,
new ReaderParameters { ReadWrite = true, InMemory = true, }))
new ReaderParameters
{
ReadWrite = true,
InMemory = true,
ReadSymbols = true,
SymbolReaderProvider = new DefaultSymbolReaderProvider(false),
}))
{
asm.Name = new AssemblyNameDefinition(
"Avalonia.Build.Tasks."
+ Guid.NewGuid().ToString().Replace("-", ""),
new Version(0, 0, 0));
asm.Write(patched);
var mainModule = asm.MainModule;
// If we have SourceLink info copy to patched assembly.
if (!string.IsNullOrEmpty(sourceLinkInfoContent))
{
mainModule.CustomDebugInformations.Add(new SourceLinkDebugInformation(sourceLinkInfoContent));
}
// Try to get SymbolWriter if it has it
var reader = mainModule.SymbolReader;
var hasDebugInfo = reader is not null;
var proivder = reader?.GetWriterProvider() is ISymbolWriterProvider p
? new Wrapper(p, "Avalonia.Build.Tasks.dll")
: default(ISymbolWriterProvider);
var parameters = new WriterParameters
{
#if ISNETFULLFRAMEWORK
StrongNameKeyPair = signingStep.KeyPair,
#endif
WriteSymbols = hasDebugInfo,
SymbolWriterProvider = proivder,
DeterministicMvid = hasDebugInfo,
};
asm.Write(patched, parameters);
patched.Position = 0;
}
}
finally
{
try
{
if(Directory.Exists(tempDir))
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
}
catch
@ -79,4 +161,4 @@ public class BuildTasksPatcher
}
}
}
}
}

1
samples/ControlCatalog/ControlCatalog.csproj

@ -30,6 +30,7 @@
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />
</ItemGroup>

8
samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs

@ -16,10 +16,10 @@ namespace ControlCatalog.Pages
private Point _cursorPoint;
public StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Scale), 1.0d);
public static readonly StyledProperty<double> ScaleProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Scale), 1.0d);
public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); }
public StyledProperty<double> RotationProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Rotation));
public static readonly StyledProperty<double> RotationProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(Rotation));
/// <summary>
/// Rotation, measured in Radians!
/// </summary>
@ -33,10 +33,10 @@ namespace ControlCatalog.Pages
}
}
public StyledProperty<double> ViewportCenterYProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterY), 0.0d);
public static readonly StyledProperty<double> ViewportCenterYProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterY), 0.0d);
public double ViewportCenterY { get => GetValue(ViewportCenterYProperty); set => SetValue(ViewportCenterYProperty, value); }
public StyledProperty<double> ViewportCenterXProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterX), 0.0d);
public static readonly StyledProperty<double> ViewportCenterXProperty = AvaloniaProperty.Register<CustomDrawingExampleControl, double>(nameof(ViewportCenterX), 0.0d);
public double ViewportCenterX { get => GetValue(ViewportCenterXProperty); set => SetValue(ViewportCenterXProperty, value); }
private IPen _pen;

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,7 @@
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Background" Value="Blue" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
</DockPanel.Styles>

8
samples/ControlCatalog/Pages/PointerCanvas.cs

@ -93,7 +93,7 @@ public class PointerCanvas : Control
}
private int _threadSleep;
public static DirectProperty<PointerCanvas, int> ThreadSleepProperty =
public static readonly DirectProperty<PointerCanvas, int> ThreadSleepProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, int>(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v);
public int ThreadSleep
@ -103,7 +103,7 @@ public class PointerCanvas : Control
}
private bool _drawOnlyPoints;
public static DirectProperty<PointerCanvas, bool> DrawOnlyPointsProperty =
public static readonly DirectProperty<PointerCanvas, bool> DrawOnlyPointsProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, bool>(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v);
public bool DrawOnlyPoints
@ -113,8 +113,8 @@ public class PointerCanvas : Control
}
private string? _status;
public static DirectProperty<PointerCanvas, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v,
public static readonly DirectProperty<PointerCanvas, string?> StatusProperty =
AvaloniaProperty.RegisterDirect<PointerCanvas, string?>(nameof(Status), c => c.Status, (c, v) => c.Status = v,
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public string? Status

1
samples/IntegrationTestApp/IntegrationTestApp.csproj

@ -19,6 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />

11
samples/IntegrationTestApp/MainWindow.axaml

@ -25,6 +25,7 @@
<StackPanel DockPanel.Dock="Bottom" Margin="4" Orientation="Horizontal">
<TextBlock Margin="0,0,4,0">WindowState:</TextBlock>
<TextBlock Name="MainWindowState" Text="{Binding WindowState}"/>
<TextBlock Name="AppOverlayPopups" Margin="8 0"/>
</StackPanel>
<TabControl TabStripPlacement="Left" Name="MainTabs">
@ -56,6 +57,16 @@
</StackPanel>
</TabItem>
<TabItem Header="RadioButton">
<StackPanel Orientation="Vertical">
<RadioButton Name="BasicRadioButton">Sample RadioButton</RadioButton>
<StackPanel Orientation="Vertical">
<RadioButton Name="ThreeStatesRadioButton1" IsChecked="True" IsThreeState="True">Three States: Option 1</RadioButton>
<RadioButton Name="ThreeStatesRadioButton2" IsChecked="False" IsThreeState="True">Three States: Option 2</RadioButton>
</StackPanel>
</StackPanel>
</TabItem>
<TabItem Header="CheckBox">
<StackPanel>
<CheckBox Name="UncheckedCheckBox">Unchecked</CheckBox>

12
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -1,19 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.VisualTree;
using Microsoft.CodeAnalysis;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Avalonia.Controls.Primitives.PopupPositioning;
namespace IntegrationTestApp
{
@ -25,6 +23,10 @@ namespace IntegrationTestApp
InitializeViewMenu();
InitializeGesturesTab();
this.AttachDevTools();
var overlayPopups = this.Get<TextBlock>("AppOverlayPopups");
overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups";
AddHandler(Button.ClickEvent, OnButtonClick);
ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList();
DataContext = this;

22
samples/IntegrationTestApp/Program.cs

@ -1,17 +1,31 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace IntegrationTestApp
{
class Program
{
public static bool OverlayPopups { get; private set; }
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static void Main(string[] args)
{
OverlayPopups = args.Contains("--overlayPopups");
BuildAvaloniaApp()
.With(new Win32PlatformOptions
{
OverlayPopups = OverlayPopups,
})
.With(new AvaloniaNativePlatformOptions
{
OverlayPopups = OverlayPopups,
})
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()

20
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@ -8,22 +9,27 @@ using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Threading;
using Avalonia.Utilities;
using SkiaSharp;
namespace RenderDemo.Pages
{
public class CustomSkiaPage : Control
{
private readonly GlyphRun _noSkia;
public CustomSkiaPage()
{
ClipToBounds = true;
var text = "Current rendering API is not Skia";
var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray();
_noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs);
}
class CustomDrawOp : ICustomDrawOperation
{
private readonly FormattedText _noSkia;
private readonly GlyphRun _noSkia;
public CustomDrawOp(Rect bounds, FormattedText noSkia)
public CustomDrawOp(Rect bounds, GlyphRun noSkia)
{
_noSkia = noSkia;
Bounds = bounds;
@ -42,10 +48,7 @@ namespace RenderDemo.Pages
{
var leaseFeature = context.GetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature == null)
using (var c = new DrawingContext(context, false))
{
c.DrawText(_noSkia, new Point());
}
context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl);
else
{
using var lease = leaseFeature.Lease();
@ -114,10 +117,7 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
}
}

5
samples/RenderDemo/Pages/PathMeasurementPage.cs

@ -37,11 +37,8 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context)
{
using (var ctxi = _bitmap.CreateDrawingContext(null))
using (var bitmapCtx = new DrawingContext(ctxi, false))
using (var bitmapCtx = _bitmap.CreateDrawingContext())
{
ctxi.Clear(default);
var basePath = new PathGeometry();
using (var basePathCtx = basePath.Open())

4
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -28,13 +28,11 @@ namespace RenderDemo.Pages
readonly Stopwatch _st = Stopwatch.StartNew();
public override void Render(DrawingContext context)
{
using (var ctxi = _bitmap.CreateDrawingContext(null))
using(var ctx = new DrawingContext(ctxi, false))
using (var ctx = _bitmap.CreateDrawingContext())
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100)))
{
ctxi.Clear(default);
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
}

5
src/Avalonia.Base/AvaloniaObject.cs

@ -784,6 +784,11 @@ namespace Avalonia
}
}
internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
{
UpdateDataValidation(property, state, error);
}
/// <summary>
/// Gets a description of an observable that van be used in logs.
/// </summary>

4
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -199,13 +199,11 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding));
var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata;
var result = binding.Initiate(
target,
property,
anchor,
metadata?.EnableDataValidation ?? false);
property.GetMetadata(target.GetType()).EnableDataValidation ?? false);
if (result != null)
{

13
src/Avalonia.Base/AvaloniaProperty.cs

@ -227,6 +227,7 @@ namespace Avalonia
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
/// <returns>A <see cref="StyledProperty{TValue}"/></returns>
public static StyledProperty<TValue> Register<TOwner, TValue>(
string name,
@ -234,7 +235,8 @@ namespace Avalonia
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TValue, bool>? validate = null,
Func<AvaloniaObject, TValue, TValue>? coerce = null)
Func<AvaloniaObject, TValue, TValue>? coerce = null,
bool enableDataValidation = false)
where TOwner : AvaloniaObject
{
_ = name ?? throw new ArgumentNullException(nameof(name));
@ -242,7 +244,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode,
coerce: coerce);
coerce: coerce,
enableDataValidation: enableDataValidation);
var result = new StyledProperty<TValue>(
name,
@ -253,7 +256,7 @@ namespace Avalonia
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
/// <inheritdoc cref="Register{TOwner, TValue}" />
/// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an
@ -267,6 +270,7 @@ namespace Avalonia
BindingMode defaultBindingMode,
Func<TValue, bool>? validate,
Func<AvaloniaObject, TValue, TValue>? coerce,
bool enableDataValidation,
Action<AvaloniaObject, bool>? notifying)
where TOwner : AvaloniaObject
{
@ -275,7 +279,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode,
coerce: coerce);
coerce: coerce,
enableDataValidation: enableDataValidation);
var result = new StyledProperty<TValue>(
name,

18
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@ -13,10 +13,13 @@ namespace Avalonia
/// Initializes a new instance of the <see cref="AvaloniaPropertyMetadata"/> class.
/// </summary>
/// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
public AvaloniaPropertyMetadata(
BindingMode defaultBindingMode = BindingMode.Default)
BindingMode defaultBindingMode = BindingMode.Default,
bool? enableDataValidation = null)
{
_defaultBindingMode = defaultBindingMode;
EnableDataValidation = enableDataValidation;
}
/// <summary>
@ -31,6 +34,17 @@ namespace Avalonia
}
}
/// <summary>
/// Gets a value indicating whether the property is interested in data validation.
/// </summary>
/// <remarks>
/// Data validation is validation performed at the target of a binding, for example in a
/// view model using the INotifyDataErrorInfo interface. Only certain properties on a
/// control (such as a TextBox's Text property) will be interested in receiving data
/// validation messages so this feature must be explicitly enabled by setting this flag.
/// </remarks>
public bool? EnableDataValidation { get; private set; }
/// <summary>
/// Merges the metadata with the base metadata.
/// </summary>
@ -44,6 +58,8 @@ namespace Avalonia
{
_defaultBindingMode = baseMetadata.DefaultBindingMode;
}
EnableDataValidation ??= baseMetadata.EnableDataValidation;
}
}
}

27
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -21,10 +21,9 @@ namespace Avalonia
TValue unsetValue = default!,
BindingMode defaultBindingMode = BindingMode.Default,
bool? enableDataValidation = null)
: base(defaultBindingMode)
: base(defaultBindingMode, enableDataValidation)
{
UnsetValue = unsetValue;
EnableDataValidation = enableDataValidation;
}
/// <summary>
@ -32,16 +31,6 @@ namespace Avalonia
/// </summary>
public TValue UnsetValue { get; private set; }
/// <summary>
/// Gets a value indicating whether the property is interested in data validation.
/// </summary>
/// <remarks>
/// Data validation is validation performed at the target of a binding, for example in a
/// view model using the INotifyDataErrorInfo interface. Only certain properties on a
/// control (such as a TextBox's Text property) will be interested in receiving data
/// validation messages so this feature must be explicitly enabled by setting this flag.
/// </remarks>
public bool? EnableDataValidation { get; private set; }
/// <inheritdoc/>
object? IDirectPropertyMetadata.UnsetValue => UnsetValue;
@ -51,19 +40,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
var src = baseMetadata as DirectPropertyMetadata<TValue>;
if (src != null)
if (baseMetadata is DirectPropertyMetadata<TValue> src)
{
if (UnsetValue == null)
{
UnsetValue = src.UnsetValue;
}
if (EnableDataValidation == null)
{
EnableDataValidation = src.EnableDataValidation;
}
UnsetValue ??= src.UnsetValue;
}
}
}

5
src/Avalonia.Base/Input/AccessKeyHandler.cs

@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Avalonia.LogicalTree;
namespace Avalonia.Input
{
@ -190,7 +189,7 @@ namespace Avalonia.Input
// If the menu is open, only match controls in the menu's visual tree.
if (menuIsOpen)
{
matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsVisualAncestorOf((Visual)x));
matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x));
}
var match = matches.FirstOrDefault();

1
src/Avalonia.Base/Input/MouseDevice.cs

@ -184,6 +184,7 @@ namespace Avalonia.Input
source?.RaiseEvent(e);
_pointer.Capture(null);
_lastMouseDownButton = default;
return e.Handled;
}

1
src/Avalonia.Base/Input/PenDevice.cs

@ -131,6 +131,7 @@ namespace Avalonia.Input
source?.RaiseEvent(e);
pointer.Capture(null);
_lastMouseDownButton = default;
return e.Handled;
}

69
src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs

@ -1,28 +1,79 @@
#nullable enable
using System;
using System;
#nullable enable
namespace Avalonia.LogicalTree
{
/// <summary>
/// Describes the action that caused a <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public enum ChildIndexChangedAction
{
/// <summary>
/// The index of a single child changed.
/// </summary>
ChildIndexChanged,
/// <summary>
/// The index of multiple children changed and all children should be re-evaluated.
/// </summary>
ChildIndexesReset,
/// <summary>
/// The total number of children changed.
/// </summary>
TotalCountChanged,
}
/// <summary>
/// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
/// </summary>
public class ChildIndexChangedEventArgs : EventArgs
{
public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();
private ChildIndexChangedEventArgs()
/// <summary>
/// Initializes a new instance of the <see cref="ChildIndexChangedEventArgs"/> class with
/// an action of <see cref="ChildIndexChangedAction.ChildIndexChanged"/>.
/// </summary>
/// <param name="child">The child whose index was changed.</param>
/// <param name="index">The new index of the child.</param>
public ChildIndexChangedEventArgs(ILogical child, int index)
{
Action = ChildIndexChangedAction.ChildIndexChanged;
Child = child;
Index = index;
}
public ChildIndexChangedEventArgs(ILogical child)
private ChildIndexChangedEventArgs(ChildIndexChangedAction action)
{
Child = child;
Action = action;
Index = -1;
}
/// <summary>
/// Logical child which index was changed.
/// If null, all children should be reset.
/// Gets the type of change action that ocurred on the list control.
/// </summary>
public ChildIndexChangedAction Action { get; }
/// <summary>
/// Gets the logical child whose index was changed or null if all children should be re-evaluated.
/// </summary>
public ILogical? Child { get; }
/// <summary>
/// Gets the new index of <see cref="Child"/> or -1 if all children should be re-evaluated.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.ChildIndexesReset"/>.
/// </summary>
public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset);
/// <summary>
/// Gets an instance of the <see cref="ChildIndexChangedEventArgs"/> with an action of
/// <see cref="ChildIndexChangedAction.TotalCountChanged"/>.
/// </summary>
public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged);
}
}

2
src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs

@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree
bool TryGetTotalCount(out int count);
/// <summary>
/// Notifies subscriber when child's index or total count was changed.
/// Notifies subscriber when a child's index was changed.
/// </summary>
event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
}

66
src/Avalonia.Base/Media/DrawingBrush.cs

@ -0,0 +1,66 @@
using Avalonia.Media.Immutable;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="Drawing"/>.
/// </summary>
public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender
{
/// <summary>
/// Defines the <see cref="Drawing"/> property.
/// </summary>
public static readonly StyledProperty<Drawing?> DrawingProperty =
AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing));
static DrawingBrush()
{
AffectsRender<DrawingBrush>(DrawingProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary>
public DrawingBrush()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary>
/// <param name="visual">The visual to draw.</param>
public DrawingBrush(Drawing visual)
{
Drawing = visual;
}
/// <summary>
/// Gets or sets the visual to draw.
/// </summary>
public Drawing? Drawing
{
get { return GetValue(DrawingProperty); }
set { SetValue(DrawingProperty, value); }
}
ISceneBrushContent? ISceneBrush.CreateContent()
{
if (Drawing == null)
return null;
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
Drawing?.Draw(recorder);
var drawList = recorder.EndUpdate();
if (drawList == null)
return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
drawList.CalculateBounds(), true);
}
}
}

404
src/Avalonia.Base/Media/DrawingContext.cs

@ -8,83 +8,45 @@ using Avalonia.Media.Imaging;
namespace Avalonia.Media
{
public sealed class DrawingContext : IDisposable
public abstract class DrawingContext : IDisposable
{
private readonly bool _ownsImpl;
private int _currentLevel;
private static ThreadSafeObjectPool<Stack<RestoreState>> StateStackPool { get; } =
ThreadSafeObjectPool<Stack<RestoreState>>.Default;
private Stack<RestoreState>? _states;
private static ThreadSafeObjectPool<Stack<PushedState>> StateStackPool { get; } =
ThreadSafeObjectPool<Stack<PushedState>>.Default;
private static ThreadSafeObjectPool<Stack<TransformContainer>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<TransformContainer>>.Default;
private Stack<PushedState>? _states = StateStackPool.Get();
private Stack<TransformContainer>? _transformContainers = TransformStackPool.Get();
readonly struct TransformContainer
{
public readonly Matrix LocalTransform;
public readonly Matrix ContainerTransform;
public TransformContainer(Matrix localTransform, Matrix containerTransform)
{
LocalTransform = localTransform;
ContainerTransform = containerTransform;
}
}
public DrawingContext(IDrawingContextImpl impl)
internal DrawingContext()
{
PlatformImpl = impl;
_ownsImpl = true;
}
public DrawingContext(IDrawingContextImpl impl, bool ownsImpl)
{
_ownsImpl = ownsImpl;
PlatformImpl = impl;
}
public IDrawingContextImpl PlatformImpl { get; }
private Matrix _currentTransform = Matrix.Identity;
private Matrix _currentContainerTransform = Matrix.Identity;
/// <summary>
/// Gets the current transform of the drawing context.
/// </summary>
public Matrix CurrentTransform
public void Dispose()
{
get { return _currentTransform; }
private set
if (_states != null)
{
_currentTransform = value;
var transform = _currentTransform * _currentContainerTransform;
PlatformImpl.Transform = transform;
}
}
while (_states.Count > 0)
_states.Pop().Dispose();
//HACK: This is a temporary hack that is used in the render loop
//to update TransformedBounds property
[Obsolete("HACK for render loop, don't use")]
public Matrix CurrentContainerTransform => _currentContainerTransform;
StateStackPool.ReturnAndSetNull(ref _states);
}
DisposeCore();
}
protected abstract void DisposeCore();
/// <summary>
/// Draws an image.
/// </summary>
/// <param name="source">The image.</param>
/// <param name="rect">The rect in the output to draw to.</param>
public void DrawImage(IImage source, Rect rect)
public virtual void DrawImage(IImage source, Rect rect)
{
_ = source ?? throw new ArgumentNullException(nameof(source));
DrawImage(source, new Rect(source.Size), rect);
}
/// <summary>
/// Draws an image.
/// </summary>
@ -92,12 +54,22 @@ namespace Avalonia.Media
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default)
public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = default)
{
_ = source ?? throw new ArgumentNullException(nameof(source));
source.Draw(this, sourceRect, destRect, bitmapInterpolationMode);
}
/// <summary>
/// Draws a platform-specific bitmap impl.
/// </summary>
/// <param name="source">The bitmap image.</param>
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
internal abstract void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
/// <summary>
/// Draws a line.
@ -108,11 +80,11 @@ namespace Avalonia.Media
public void DrawLine(IPen pen, Point p1, Point p2)
{
if (PenIsVisible(pen))
{
PlatformImpl.DrawLine(pen, p1, p2);
}
DrawLineCore(pen, p1, p2);
}
protected abstract void DrawLineCore(IPen pen, Point p1, Point p2);
/// <summary>
/// Draws a geometry.
/// </summary>
@ -121,10 +93,10 @@ namespace Avalonia.Media
/// <param name="geometry">The geometry.</param>
public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry)
{
if (geometry.PlatformImpl is not null)
DrawGeometry(brush, pen, geometry.PlatformImpl);
if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null)
DrawGeometryCore(brush, pen, geometry.PlatformImpl);
}
/// <summary>
/// Draws a geometry.
/// </summary>
@ -133,14 +105,12 @@ namespace Avalonia.Media
/// <param name="geometry">The geometry.</param>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
_ = geometry ?? throw new ArgumentNullException(nameof(geometry));
if (brush != null || PenIsVisible(pen))
{
PlatformImpl.DrawGeometry(brush, pen, geometry);
}
if ((brush != null || PenIsVisible(pen)))
DrawGeometryCore(brush, pen, geometry);
}
protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry);
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
/// </summary>
@ -158,14 +128,12 @@ namespace Avalonia.Media
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0,
public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect,
double radiusX = 0, double radiusY = 0,
BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
{
return;
}
if (!MathUtilities.IsZero(radiusX))
{
radiusX = Math.Min(radiusX, rect.Width / 2);
@ -175,20 +143,48 @@ namespace Avalonia.Media
{
radiusY = Math.Min(radiusY, rect.Height / 2);
}
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows);
}
/// <summary>
/// Draws a rectangle with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rrect">The rectangle bounds.</param>
/// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{
if (brush == null && !PenIsVisible(pen))
return;
DrawRectangleCore(brush, pen, rrect, boxShadows);
}
protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
BoxShadows boxShadows = default);
/// <summary>
/// Draws the outline of a rectangle.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f)
{
public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) =>
DrawRectangle(null, pen, rect, cornerRadius, cornerRadius);
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) =>
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
/// <summary>
/// Draws an ellipse with the specified Brush and Pen.
@ -204,35 +200,50 @@ namespace Avalonia.Media
/// </remarks>
public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY)
{
if (brush == null && !PenIsVisible(pen))
if (brush != null || PenIsVisible(pen))
{
return;
var originX = center.X - radiusX;
var originY = center.Y - radiusY;
var width = radiusX * 2;
var height = radiusY * 2;
DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height));
}
var originX = center.X - radiusX;
var originY = center.Y - radiusY;
var width = radiusX * 2;
var height = radiusY * 2;
PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height));
}
/// <summary>
/// Draws an ellipse with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the ellipse, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the ellipse, or <c>null</c> for no stroke.</param>
/// <param name="rect">The bounding rect.</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if (brush != null || PenIsVisible(pen))
DrawEllipseCore(brush, pen, rect);
}
protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect);
/// <summary>
/// Draws a custom drawing operation
/// </summary>
/// <param name="custom">custom operation</param>
public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom);
public abstract void Custom(ICustomDrawOperation custom);
/// <summary>
/// Draws text.
/// </summary>
/// <param name="origin">The upper-left corner of the text.</param>
/// <param name="text">The text.</param>
public void DrawText(FormattedText text, Point origin)
public virtual void DrawText(FormattedText text, Point origin)
{
_ = text ?? throw new ArgumentNullException(nameof(text));
text.Draw(this, origin);
text.Draw(this, origin);
}
/// <summary>
@ -240,93 +251,87 @@ namespace Avalonia.Media
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun);
public record struct PushedState : IDisposable
{
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
private readonly DrawingContext _context;
private readonly int _level;
if (foreground != null)
public PushedState(DrawingContext context)
{
PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
_context = context;
_level = _context._states!.Count;
}
}
/// <summary>
/// Draws a filled rectangle.
/// </summary>
/// <param name="brush">The brush.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="cornerRadius">The corner radius.</param>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f)
{
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
public void Dispose()
{
if(_context?._states == null)
return;
if(_context._states.Count != _level)
throw new InvalidOperationException("Wrong Push/Pop state order");
_context._states.Pop().Dispose();
}
}
public readonly record struct PushedState : IDisposable
private readonly record struct RestoreState : IDisposable
{
private readonly int _level;
private readonly DrawingContext _context;
private readonly Matrix _matrix;
private readonly PushedStateType _type;
public enum PushedStateType
{
None,
Matrix,
Transform,
Opacity,
Clip,
MatrixContainer,
GeometryClip,
OpacityMask,
BitmapBlendMode
}
public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default)
public RestoreState(DrawingContext context, PushedStateType type)
{
if (context._states is null)
throw new ObjectDisposedException(nameof(DrawingContext));
_context = context;
_type = type;
_matrix = matrix;
_level = context._currentLevel += 1;
context._states.Push(this);
}
public void Dispose()
{
if (_type == PushedStateType.None)
return;
if (_context._states is null || _context._transformContainers is null)
if (_context._states is null)
throw new ObjectDisposedException(nameof(DrawingContext));
if (_context._currentLevel != _level)
throw new InvalidOperationException("Wrong Push/Pop state order");
_context._currentLevel--;
_context._states.Pop();
if (_type == PushedStateType.Matrix)
_context.CurrentTransform = _matrix;
if (_type == PushedStateType.Transform)
_context.PopTransformCore();
else if (_type == PushedStateType.Clip)
_context.PlatformImpl.PopClip();
_context.PopClipCore();
else if (_type == PushedStateType.Opacity)
_context.PlatformImpl.PopOpacity();
_context.PopOpacityCore();
else if (_type == PushedStateType.GeometryClip)
_context.PlatformImpl.PopGeometryClip();
_context.PopGeometryClipCore();
else if (_type == PushedStateType.OpacityMask)
_context.PlatformImpl.PopOpacityMask();
else if (_type == PushedStateType.MatrixContainer)
{
var cont = _context._transformContainers.Pop();
_context._currentContainerTransform = cont.ContainerTransform;
_context.CurrentTransform = cont.LocalTransform;
}
_context.PopOpacityMaskCore();
else if (_type == PushedStateType.BitmapBlendMode)
_context.PopBitmapBlendModeCore();
}
}
/// <summary>
/// Pushes a clip rectangle.
/// </summary>
/// <param name="clip">The clip rectangle.</param>
/// <returns>A disposable used to undo the clip rectangle.</returns>
public PushedState PushClip(RoundedRect clip)
{
PlatformImpl.PushClip(clip);
return new PushedState(this, PushedState.PushedStateType.Clip);
PushClipCore(clip);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
return new PushedState(this);
}
protected abstract void PushClipCore(RoundedRect rect);
/// <summary>
/// Pushes a clip rectangle.
/// </summary>
@ -334,9 +339,13 @@ namespace Avalonia.Media
/// <returns>A disposable used to undo the clip rectangle.</returns>
public PushedState PushClip(Rect clip)
{
PlatformImpl.PushClip(clip);
return new PushedState(this, PushedState.PushedStateType.Clip);
PushClipCore(clip);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip));
return new PushedState(this);
}
protected abstract void PushClipCore(Rect rect);
/// <summary>
/// Pushes a clip geometry.
@ -345,17 +354,13 @@ namespace Avalonia.Media
/// <returns>A disposable used to undo the clip geometry.</returns>
public PushedState PushGeometryClip(Geometry clip)
{
_ = clip ?? throw new ArgumentNullException(nameof(clip));
// HACK: This check was added when nullable annotations pointed out that we're potentially
// pushing a null value for the clip here. Ideally we'd return an empty PushedState here but
// I don't want to make that change as part of adding nullable annotations.
if (clip.PlatformImpl is null)
throw new InvalidOperationException("Cannot push empty geometry clip.");
PlatformImpl.PushGeometryClip(clip.PlatformImpl);
return new PushedState(this, PushedState.PushedStateType.GeometryClip);
PushGeometryClipCore(clip);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip));
return new PushedState(this);
}
protected abstract void PushGeometryClipCore(Geometry clip);
/// <summary>
/// Pushes an opacity value.
@ -364,11 +369,13 @@ namespace Avalonia.Media
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
PushOpacityCore(opacity, bounds);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity));
return new PushedState(this);
}
protected abstract void PushOpacityCore(double opacity, Rect bounds);
/// <summary>
/// Pushes an opacity mask.
@ -380,70 +387,53 @@ namespace Avalonia.Media
/// <returns>A disposable to undo the opacity mask.</returns>
public PushedState PushOpacityMask(IBrush mask, Rect bounds)
{
PlatformImpl.PushOpacityMask(mask, bounds);
return new PushedState(this, PushedState.PushedStateType.OpacityMask);
PushOpacityMaskCore(mask, bounds);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask));
return new PushedState(this);
}
protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds);
/// <summary>
/// Pushes a matrix post-transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix);
/// <summary>
/// Pushes a matrix pre-transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform);
/// <summary>
/// Sets the current matrix transformation.
/// </summary>
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushSetTransform(Matrix matrix)
public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
var oldMatrix = CurrentTransform;
CurrentTransform = matrix;
return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix);
PushBitmapBlendMode(blendingMode);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode));
return new PushedState(this);
}
/// <summary>
/// Pushes a new transform context.
/// </summary>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushTransformContainer()
{
if (_transformContainers is null)
throw new ObjectDisposedException(nameof(DrawingContext));
_transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform));
_currentContainerTransform = CurrentTransform * _currentContainerTransform;
_currentTransform = Matrix.Identity;
return new PushedState(this, PushedState.PushedStateType.MatrixContainer);
}
protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode);
/// <summary>
/// Disposes of any resources held by the <see cref="DrawingContext"/>.
/// Pushes a matrix transformation.
/// </summary>
public void Dispose()
/// <param name="matrix">The matrix</param>
/// <returns>A disposable used to undo the transformation.</returns>
public PushedState PushTransform(Matrix matrix)
{
if (_states is null || _transformContainers is null)
throw new ObjectDisposedException(nameof(DrawingContext));
while (_states.Count != 0)
_states.Peek().Dispose();
StateStackPool.Return(_states);
_states = null;
if (_transformContainers.Count != 0)
throw new InvalidOperationException("Transform container stack is non-empty");
TransformStackPool.Return(_transformContainers);
_transformContainers = null;
if (_ownsImpl)
PlatformImpl.Dispose();
PushTransformCore(matrix);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform));
return new PushedState(this);
}
[Obsolete("Use PushTransform")]
public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform")]
public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform")]
public PushedState PushTransformContainer() => PushTransform(Matrix.Identity);
protected abstract void PushTransformCore(Matrix matrix);
protected abstract void PopClipCore();
protected abstract void PopGeometryClipCore();
protected abstract void PopOpacityCore();
protected abstract void PopOpacityMaskCore();
protected abstract void PopBitmapBlendModeCore();
protected abstract void PopTransformCore();
private static bool PenIsVisible(IPen? pen)
{
return pen?.Brush != null && pen.Thickness > 0;

141
src/Avalonia.Base/Media/DrawingGroup.cs

@ -67,10 +67,7 @@ namespace Avalonia.Media
}
}
public DrawingContext Open()
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public DrawingContext Open() => new DrawingGroupDrawingContext(this);
public override void Draw(DrawingContext context)
{
@ -105,13 +102,11 @@ namespace Avalonia.Media
return rect;
}
private class DrawingGroupDrawingContext : IDrawingContextImpl
private sealed class DrawingGroupDrawingContext : DrawingContext
{
private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
private Matrix _transform;
private bool _disposed;
// Root drawing created by this DrawingContext.
@ -135,17 +130,7 @@ namespace Avalonia.Media
_drawingGroup = drawingGroup;
}
public Matrix Transform
{
get => _transform;
set
{
_transform = value;
PushTransform(new MatrixTransform(value));
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
if ((brush == null) && (pen == null))
{
@ -159,7 +144,7 @@ namespace Avalonia.Media
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
if ((brush == null) && (pen == null))
{
@ -169,7 +154,7 @@ namespace Avalonia.Media
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{
if (foreground == null)
{
@ -179,124 +164,70 @@ namespace Avalonia.Media
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{
Foreground = foreground,
GlyphRun = new GlyphRun(glyphRun)
GlyphRun = glyphRun
};
// Add Drawing to the Drawing graph
AddDrawing(glyphRunDrawing);
}
public void DrawLine(IPen? pen, Point p1, Point p2)
{
if (pen == null)
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void Clear(Color color)
{
throw new NotImplementedException();
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotImplementedException();
}
public void Custom(ICustomDrawOperation custom)
{
throw new NotImplementedException();
}
public object? GetFeature(Type t) => null;
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
protected override void PushClipCore(RoundedRect rect)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
protected override void PushClipCore(Rect rect)
{
throw new NotImplementedException();
}
public void PopBitmapBlendMode()
protected override void PushGeometryClipCore(Geometry clip)
{
throw new NotImplementedException();
}
public void PopClip()
protected override void PushOpacityCore(double opacity, Rect bounds)
{
throw new NotImplementedException();
}
public void PopGeometryClip()
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
{
throw new NotImplementedException();
}
public void PopOpacity()
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
public void PopOpacityMask()
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
throw new NotImplementedException();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
protected override void DrawLineCore(IPen pen, Point p1, Point p2)
{
throw new NotImplementedException();
}
public void PushClip(Rect clip)
{
throw new NotImplementedException();
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
public void PushClip(RoundedRect clip)
{
throw new NotImplementedException();
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void PushGeometryClip(IGeometryImpl clip)
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default)
{
throw new NotImplementedException();
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect);
public void PushOpacity(double opacity, Rect bounds)
{
throw new NotImplementedException();
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void PushOpacityMask(IBrush mask, Rect bounds)
{
throw new NotImplementedException();
}
public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException();
public void Dispose()
protected override void DisposeCore()
{
// Dispose may be called multiple times without throwing
// an exception.
@ -366,22 +297,34 @@ namespace Avalonia.Media
// Restore the previous value of the current drawing group
_currentDrawingGroup = _previousDrawingGroupStack.Pop();
}
/// <summary>
/// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop.
/// </summary>
/// <param name="transform"> The Transform to push. </param>
private void PushTransform(Transform transform)
/// <param name="matrix"> The transform to push. </param>
protected override void PushTransformCore(Matrix matrix)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the transform on the new DrawingGroup
drawingGroup.Transform = transform;
drawingGroup.Transform = new MatrixTransform(matrix);
}
protected override void PopClipCore() => Pop();
protected override void PopGeometryClipCore() => Pop();
protected override void PopOpacityCore() => Pop();
protected override void PopOpacityMaskCore() => Pop();
protected override void PopBitmapBlendModeCore() => Pop();
protected override void PopTransformCore() => Pop();
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,

2
src/Avalonia.Base/Media/DrawingImage.cs

@ -62,7 +62,7 @@ namespace Avalonia.Media
-sourceRect.Y + destRect.Y - bounds.Y);
using (context.PushClip(destRect))
using (context.PushPreTransform(translate * scale))
using (context.PushTransform(translate * scale))
{
Drawing?.Draw(context);
}

4
src/Avalonia.Base/Media/GeometryDrawing.cs

@ -27,8 +27,8 @@ namespace Avalonia.Media
/// <summary>
/// Defines the <see cref="Pen"/> property.
/// </summary>
public static readonly StyledProperty<Pen?> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, Pen?>(nameof(Pen));
public static readonly StyledProperty<IPen?> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, IPen?>(nameof(Pen));
/// <summary>
/// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>.

31
src/Avalonia.Base/Media/ISceneBrush.cs

@ -0,0 +1,31 @@
using System;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media
{
[NotClientImplementable]
public interface ISceneBrush : ITileBrush
{
ISceneBrushContent? CreateContent();
}
[NotClientImplementable]
public interface ISceneBrushContent : IImmutableBrush, IDisposable
{
ITileBrush Brush { get; }
Rect Rect { get; }
void Render(IDrawingContextImpl context, Matrix? transform);
internal bool UseScalableRasterization { get; }
}
internal class ImmutableSceneBrush : ImmutableTileBrush
{
public ImmutableSceneBrush(ITileBrush source) : base(source)
{
}
}
}

16
src/Avalonia.Base/Media/IVisualBrush.cs

@ -1,16 +0,0 @@
using Avalonia.Metadata;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
[NotClientImplementable]
public interface IVisualBrush : ITileBrush
{
/// <summary>
/// Gets the visual to draw.
/// </summary>
Visual? Visual { get; }
}
}

2
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging
Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode)
{
context.PlatformImpl.DrawBitmap(
context.DrawBitmap(
PlatformImpl,
1,
sourceRect,

18
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// A bitmap that holds the rendering of a <see cref="Visual"/>.
/// </summary>
public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget
public class RenderTargetBitmap : Bitmap, IDisposable
{
/// <summary>
/// Initializes a new instance of the <see cref="RenderTargetBitmap"/> class.
@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging
/// Renders a visual to the <see cref="RenderTargetBitmap"/>.
/// </summary>
/// <param name="visual">The visual to render.</param>
public void Render(Visual visual) => ImmediateRenderer.Render(visual, this);
public void Render(Visual visual)
{
using (var ctx = CreateDrawingContext())
ImmediateRenderer.Render(visual, ctx);
}
/// <summary>
/// Creates a platform-specific implementation for a <see cref="RenderTargetBitmap"/>.
@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging
return factory.CreateRenderTargetBitmap(size, dpi);
}
/// <inheritdoc/>
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr);
bool IRenderTarget.IsCorrupted => false;
public DrawingContext CreateDrawingContext()
{
var platform = PlatformImpl.Item.CreateDrawingContext();
platform.Clear(Colors.Transparent);
return new PlatformDrawingContext(platform);
}
}
}

6
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -354,12 +354,10 @@ namespace Avalonia.Media
throw new ObjectDisposedException(nameof(DrawingContext));
while (_states.Count != 0)
_states.Peek().Dispose();
StateStackPool.Return(_states);
_states = null;
StateStackPool.ReturnAndSetNull(ref _states);
if (_transformContainers.Count != 0)
throw new InvalidOperationException("Transform container stack is non-empty");
TransformStackPool.Return(_transformContainers);
_transformContainers = null;
TransformStackPool.ReturnAndSetNull(ref _transformContainers);
if (_ownsImpl)
PlatformImpl.Dispose();
}

66
src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs

@ -1,66 +0,0 @@
using Avalonia.Media.Imaging;
namespace Avalonia.Media.Immutable
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush
{
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableImageBrush"/> class.
/// </summary>
/// <param name="visual">The visual to draw.</param>
/// <param name="alignmentX">The horizontal alignment of a tile in the destination.</param>
/// <param name="alignmentY">The vertical alignment of a tile in the destination.</param>
/// <param name="destinationRect">The rectangle on the destination in which to paint a tile.</param>
/// <param name="opacity">The opacity of the brush.</param>
/// <param name="transform">The transform of the brush.</param>
/// <param name="transformOrigin">The transform origin of the brush</param>
/// <param name="sourceRect">The rectangle of the source image that will be displayed.</param>
/// <param name="stretch">
/// How the source rectangle will be stretched to fill the destination rect.
/// </param>
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">Controls the quality of interpolation.</param>
public ImmutableVisualBrush(
Visual? visual,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
double opacity = 1,
ImmutableTransform? transform = null,
RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
: base(
alignmentX,
alignmentY,
destinationRect ?? RelativeRect.Fill,
opacity,
transform,
transformOrigin,
sourceRect ?? RelativeRect.Fill,
stretch,
tileMode,
bitmapInterpolationMode)
{
Visual = visual;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableVisualBrush"/> class.
/// </summary>
/// <param name="source">The brush from which this brush's properties should be copied.</param>
public ImmutableVisualBrush(IVisualBrush source)
: base(source)
{
Visual = source.Visual;
}
/// <inheritdoc/>
public Visual? Visual { get; }
}
}

112
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Media;
internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private readonly IDrawingContextImpl _impl;
private readonly bool _ownsImpl;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
private Stack<Matrix>? _transforms;
public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true)
{
_impl = impl;
_ownsImpl = ownsImpl;
}
protected override void DrawLineCore(IPen pen, Point p1, Point p2) =>
_impl.DrawLine(pen, p1, p2);
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) =>
_impl.DrawGeometry(brush, pen, geometry);
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect,
BoxShadows boxShadows = default) =>
_impl.DrawRectangle(brush, pen, rrect, boxShadows);
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect);
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) =>
_impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode);
public override void Custom(ICustomDrawOperation custom) =>
custom.Render(_impl);
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
if (foreground != null)
_impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
}
protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect);
protected override void PushClipCore(Rect rect) => _impl.PushClip(rect);
protected override void PushGeometryClipCore(Geometry clip) =>
_impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException());
protected override void PushOpacityCore(double opacity, Rect bounds) =>
_impl.PushOpacity(opacity, bounds);
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) =>
_impl.PushOpacityMask(mask, bounds);
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) =>
_impl.PushBitmapBlendMode(blendingMode);
protected override void PushTransformCore(Matrix matrix)
{
_transforms ??= TransformStackPool.Get();
var current = _impl.Transform;
_transforms.Push(current);
_impl.Transform = matrix * current;
}
protected override void PopClipCore() => _impl.PopClip();
protected override void PopGeometryClipCore() => _impl.PopGeometryClip();
protected override void PopOpacityCore() => _impl.PopOpacity();
protected override void PopOpacityMaskCore() => _impl.PopOpacityMask();
protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode();
protected override void PopTransformCore() =>
_impl.Transform =
(_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();
protected override void DisposeCore()
{
if (_ownsImpl)
_impl.Dispose();
if (_transforms != null)
{
if (_transforms.Count != 0)
throw new InvalidOperationException("Not all states are disposed");
TransformStackPool.ReturnAndSetNull(ref _transforms);
}
}
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
if (_impl is IDrawingContextWithAcrylicLikeSupport idc)
idc.DrawRectangle(material, rect);
else
DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect);
}
}

3
src/Avalonia.Base/Media/PolyLineSegment.cs

@ -1,5 +1,4 @@
using System.Collections.Generic;
using Avalonia.Collections;
namespace Avalonia.Media
{
@ -20,7 +19,7 @@ namespace Avalonia.Media
/// <value>
/// The points.
/// </value>
public AvaloniaList<Point> Points
public Points Points
{
get => GetValue(PointsProperty);
set => SetValue(PointsProperty, value);

6
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -89,13 +89,13 @@ namespace Avalonia.Media.TextFormatting
var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster);
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset);
var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
var glyphInfo = shapedBuffer[glyphIndex];
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,
shapedBuffer[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,
glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
}
glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;
glyphRun.GlyphInfos = shapedBuffer;
}
currentPosition += textRun.Length;

98
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -2,6 +2,7 @@
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@ -9,12 +10,13 @@ namespace Avalonia.Media.TextFormatting
public sealed class ShapedBuffer : IReadOnlyList<GlyphInfo>, IDisposable
{
private GlyphInfo[]? _rentedBuffer;
private ArraySlice<GlyphInfo> _glyphInfos;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
_rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
Text = text;
GlyphInfos = new ArraySlice<GlyphInfo>(_rentedBuffer, 0, bufferLength);
_rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
_glyphInfos = new ArraySlice<GlyphInfo>(_rentedBuffer, 0, bufferLength);
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
@ -23,27 +25,70 @@ namespace Avalonia.Media.TextFormatting
internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
Text = text;
GlyphInfos = glyphInfos;
_glyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; private set; }
public int Length
=> GlyphInfos.Length;
/// <summary>
/// The buffer's length.
/// </summary>
public int Length => _glyphInfos.Length;
/// <summary>
/// The buffer's glyph typeface.
/// </summary>
public IGlyphTypeface GlyphTypeface { get; }
/// <summary>
/// The buffers font rendering em size.
/// </summary>
public double FontRenderingEmSize { get; }
/// <summary>
/// The buffer's bidi level.
/// </summary>
public sbyte BidiLevel { get; }
/// <summary>
/// The buffer's reading direction.
/// </summary>
public bool IsLeftToRight => (BidiLevel & 1) == 0;
/// <summary>
/// The text that is represended by this buffer.
/// </summary>
public ReadOnlyMemory<char> Text { get; }
/// <summary>
/// Reverses the buffer.
/// </summary>
public void Reverse()
{
_glyphInfos.Span.Reverse();
}
public void Dispose()
{
if (_rentedBuffer is not null)
{
ArrayPool<GlyphInfo>.Shared.Return(_rentedBuffer);
_rentedBuffer = null;
_glyphInfos = ArraySlice<GlyphInfo>.Empty; // ensure we don't misuse the returned array
}
}
public GlyphInfo this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _glyphInfos[index];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _glyphInfos[index] = value;
}
public IEnumerator<GlyphInfo> GetEnumerator() => _glyphInfos.GetEnumerator();
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -53,20 +98,19 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
private int FindGlyphIndex(int characterIndex)
{
if (characterIndex < GlyphInfos[0].GlyphCluster)
if (characterIndex < _glyphInfos[0].GlyphCluster)
{
return 0;
}
if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster)
if (characterIndex > _glyphInfos[_glyphInfos.Length - 1].GlyphCluster)
{
return GlyphInfos.Length - 1;
return _glyphInfos.Length - 1;
}
var comparer = GlyphInfo.ClusterAscendingComparer;
var glyphInfos = GlyphInfos.Span;
var glyphInfos = _glyphInfos.Span;
var searchValue = new GlyphInfo(default, characterIndex, default);
@ -109,42 +153,24 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(this, null);
}
var firstCluster = GlyphInfos[0].GlyphCluster;
var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster;
var firstCluster = _glyphInfos[0].GlyphCluster;
var lastCluster = _glyphInfos[_glyphInfos.Length - 1].GlyphCluster;
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(Text.Slice(0, length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
_glyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(Text.Slice(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
_glyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
}
int IReadOnlyCollection<GlyphInfo>.Count => GlyphInfos.Length;
public GlyphInfo this[int index]
{
get => GlyphInfos[index];
set => GlyphInfos[index] = value;
}
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator();
int IReadOnlyCollection<GlyphInfo>.Count => _glyphInfos.Length;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
if (_rentedBuffer is not null)
{
ArrayPool<GlyphInfo>.Shared.Return(_rentedBuffer);
_rentedBuffer = null;
GlyphInfos = ArraySlice<GlyphInfo>.Empty; // ensure we don't misuse the returned array
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

6
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -85,7 +85,7 @@ namespace Avalonia.Media.TextFormatting
{
_glyphRun = null;
ShapedBuffer.GlyphInfos.Span.Reverse();
ShapedBuffer.Reverse();
IsReversed = !IsReversed;
}
@ -106,7 +106,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < ShapedBuffer.Length; i++)
{
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
var advance = ShapedBuffer[i].GlyphAdvance;
if (currentWidth + advance > availableWidth)
{
@ -130,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
var advance = ShapedBuffer[i].GlyphAdvance;
if (width + advance > availableWidth)
{

2
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -583,7 +583,7 @@ namespace Avalonia.Media.TextFormatting
{
if (shapedTextCharacters.ShapedBuffer.Length > 0)
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster;
var lastCluster = firstCluster;
for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)

24
src/Avalonia.Base/Media/VisualBrush.cs

@ -1,11 +1,14 @@
using Avalonia.Media.Immutable;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush
public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender
{
/// <summary>
/// Defines the <see cref="Visual"/> property.
@ -43,10 +46,23 @@ namespace Avalonia.Media
set { SetValue(VisualProperty, value); }
}
/// <inheritdoc/>
IImmutableBrush IMutableBrush.ToImmutable()
ISceneBrushContent? ISceneBrush.CreateContent()
{
return new ImmutableVisualBrush(this);
if (Visual == null)
return null;
if (Visual is IVisualBrushInitialize initialize)
initialize.EnsureInitialized();
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
ImmediateRenderer.Render(recorder, Visual, Visual.Bounds);
var drawList = recorder.EndUpdate();
if (drawList == null)
return null;
return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList,
new(Visual.Bounds.Size), false);
}
}
}

6
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -14,11 +14,7 @@ namespace Avalonia.Platform
/// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary>
/// <param name="visualBrushRenderer">
/// A render to be used to render visual brushes. May be null if no visual brushes are
/// to be drawn.
/// </param>
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
IDrawingContextImpl CreateDrawingContext();
/// <summary>
/// Indicates if the render target is no longer usable and needs to be recreated

76
src/Avalonia.Base/PropertyStore/BindingEntryBase.cs

@ -16,27 +16,37 @@ namespace Avalonia.PropertyStore
private IDisposable? _subscription;
private bool _hasValue;
private TValue? _value;
private TValue? _defaultValue;
private bool _isDefaultValueInitialized;
private UncommonFields? _uncommon;
protected BindingEntryBase(
AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable<BindingValue<TSource>> source)
: this(target, frame, property, (object)source)
{
Frame = frame;
Source = source;
Property = property;
}
protected BindingEntryBase(
AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable<TSource> source)
: this(target, frame, property, (object)source)
{
}
private BindingEntryBase(
AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
object source)
{
Frame = frame;
Source = source;
Property = property;
Source = source;
if (property.GetMetadata(target.GetType()).EnableDataValidation == true)
_uncommon = new() { _hasDataValidation = true };
}
public bool HasValue
@ -68,6 +78,20 @@ namespace Avalonia.PropertyStore
return _value!;
}
public bool GetDataValidationState(out BindingValueType state, out Exception? error)
{
if (_uncommon?._hasDataValidation == true)
{
state = _uncommon._dataValidationState;
error = _uncommon._dataValidationError;
return true;
}
state = BindingValueType.Value;
error = null;
return false;
}
public void Start() => Start(true);
public void OnCompleted() => BindingCompleted();
@ -111,16 +135,28 @@ namespace Avalonia.PropertyStore
{
static void Execute(BindingEntryBase<TValue, TSource> instance, BindingValue<TValue> value)
{
if (instance.Frame.Owner is null)
if (instance.Frame.Owner is not { } valueStore)
return;
LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value);
var owner = valueStore.Owner;
var property = instance.Property;
var originalType = value.Type;
LoggingUtils.LogIfNecessary(owner, property, value);
var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue();
if (!value.HasValue && value.Type != BindingValueType.DataValidationError)
value = value.WithValue(instance.GetCachedDefaultValue());
if (!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, effectiveValue))
if (instance._uncommon?._hasDataValidation == true)
{
instance._value = effectiveValue;
instance._uncommon._dataValidationState = value.Type;
instance._uncommon._dataValidationError = value.Error;
}
if (value.HasValue &&
(!instance._hasValue || !EqualityComparer<TValue>.Default.Equals(instance._value, value.Value)))
{
instance._value = value.Value;
instance._hasValue = true;
if (instance._subscription is not null && instance._subscription != s_creatingQuiet)
instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority);
@ -152,13 +188,23 @@ namespace Avalonia.PropertyStore
private TValue GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
if (_uncommon?._isDefaultValueInitialized != true)
{
_defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
_isDefaultValueInitialized = true;
_uncommon ??= new();
_uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType());
_uncommon._isDefaultValueInitialized = true;
}
return _defaultValue!;
return _uncommon._defaultValue!;
}
private class UncommonFields
{
public TValue? _defaultValue;
public bool _isDefaultValueInitialized;
public bool _hasDataValidation;
public BindingValueType _dataValidationState;
public Exception? _dataValidationError;
}
}
}

11
src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs

@ -9,11 +9,13 @@ namespace Avalonia.PropertyStore
IDisposable
{
private readonly ValueStore _owner;
private readonly bool _hasDataValidation;
private IDisposable? _subscription;
public DirectBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
Property = property;
}
@ -33,10 +35,17 @@ namespace Avalonia.PropertyStore
{
_subscription?.Dispose();
_subscription = null;
OnCompleted();
}
public void OnCompleted()
{
_owner.OnLocalValueBindingCompleted(Property, this);
if (_hasDataValidation)
_owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)

5
src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs

@ -10,11 +10,13 @@ namespace Avalonia.PropertyStore
IDisposable
{
private readonly ValueStore _owner;
private readonly bool _hasDataValidation;
private IDisposable? _subscription;
public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase<T> property)
{
_owner = owner;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false;
Property = property;
}
@ -30,6 +32,9 @@ namespace Avalonia.PropertyStore
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
if (_hasDataValidation)
_owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);

51
src/Avalonia.Base/PropertyStore/EffectiveValue.cs

@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore
/// </remarks>
internal abstract class EffectiveValue
{
private IValueEntry? _valueEntry;
private IValueEntry? _baseValueEntry;
/// <summary>
/// Gets the current effective value as a boxed value.
/// </summary>
@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore
/// </summary>
public BindingPriority BasePriority { get; protected set; }
/// <summary>
/// Gets the active value entry for the current effective value.
/// </summary>
public IValueEntry? ValueEntry { get; private set; }
/// <summary>
/// Gets the active value entry for the current base value.
/// </summary>
public IValueEntry? BaseValueEntry { get; private set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Value"/> was overridden by a call to
/// <see cref="AvaloniaObject.SetCurrentValue{T}"/>.
@ -63,14 +70,14 @@ namespace Avalonia.PropertyStore
{
if (Priority == BindingPriority.Unset)
{
_valueEntry?.Unsubscribe();
_valueEntry = null;
ValueEntry?.Unsubscribe();
ValueEntry = null;
}
if (BasePriority == BindingPriority.Unset)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = null;
BaseValueEntry?.Unsubscribe();
BaseValueEntry = null;
}
}
@ -135,40 +142,34 @@ namespace Avalonia.PropertyStore
// value, then the current entry becomes our base entry.
if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited)
{
Debug.Assert(_valueEntry is not null);
_baseValueEntry = _valueEntry;
_valueEntry = null;
Debug.Assert(ValueEntry is not null);
BaseValueEntry = ValueEntry;
ValueEntry = null;
}
if (_valueEntry != entry)
if (ValueEntry != entry)
{
_valueEntry?.Unsubscribe();
_valueEntry = entry;
ValueEntry?.Unsubscribe();
ValueEntry = entry;
}
}
else if (Priority <= BindingPriority.Animation)
{
// We've received a non-animation value and have an active animation value, so the
// new entry becomes our base entry.
if (_baseValueEntry != entry)
if (BaseValueEntry != entry)
{
_baseValueEntry?.Unsubscribe();
_baseValueEntry = entry;
BaseValueEntry?.Unsubscribe();
BaseValueEntry = entry;
}
}
else if (_valueEntry != entry)
else if (ValueEntry != entry)
{
// Both the current value and the new value are non-animation values, so the new
// entry replaces the existing entry.
_valueEntry?.Unsubscribe();
_valueEntry = entry;
ValueEntry?.Unsubscribe();
ValueEntry = entry;
}
}
protected void UnsubscribeValueEntries()
{
_valueEntry?.Unsubscribe();
_baseValueEntry?.Unsubscribe();
}
}
}

26
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot;
namespace Avalonia.PropertyStore
{
@ -61,6 +62,12 @@ namespace Avalonia.PropertyStore
UpdateValueEntry(value, priority);
SetAndRaiseCore(owner, (StyledProperty<T>)value.Property, GetValue(value), priority, false);
if (priority > BindingPriority.LocalValue &&
value.GetDataValidationState(out var state, out var error))
{
owner.Owner.OnUpdateDataValidation(value.Property, state, error);
}
}
public void SetLocalValueAndRaise(
@ -128,12 +135,10 @@ namespace Avalonia.PropertyStore
public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property)
{
UnsubscribeValueEntries();
DisposeAndRaiseUnset(owner, (StyledProperty<T>)property);
}
ValueEntry?.Unsubscribe();
BaseValueEntry?.Unsubscribe();
public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty<T> property)
{
var p = (StyledProperty<T>)property;
BindingPriority priority;
T oldValue;
@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore
if (!EqualityComparer<T>.Default.Equals(oldValue, Value))
{
owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true);
owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true);
if (property.Inherits)
owner.OnInheritedEffectiveValueDisposed(property, Value);
owner.OnInheritedEffectiveValueDisposed(p, Value);
}
if (ValueEntry?.GetDataValidationState(out _, out _) ??
BaseValueEntry?.GetDataValidationState(out _, out _) ??
false)
{
owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null);
}
}

11
src/Avalonia.Base/PropertyStore/IValueEntry.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
@ -22,6 +23,16 @@ namespace Avalonia.PropertyStore
/// </exception>
object? GetValue();
/// <summary>
/// Gets the data validation state if supported.
/// </summary>
/// <param name="state">The binding validation state.</param>
/// <param name="error">The current binding error, if any.</param>
/// <returns>
/// True if the entry supports data validation, otherwise false.
/// </returns>
bool GetDataValidationState(out BindingValueType state, out Exception? error);
/// <summary>
/// Called when the value entry is removed from the value store.
/// </summary>

8
src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Data;
namespace Avalonia.PropertyStore
{
@ -27,5 +28,12 @@ namespace Avalonia.PropertyStore
object? IValueEntry.GetValue() => _value;
T IValueEntry<T>.GetValue() => _value;
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
{
state = BindingValueType.Value;
error = null;
return false;
}
}
}

6
src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs

@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
IObservable<BindingValue<T>> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
var e = new TypedBindingEntry<T>(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
IObservable<T> source)
{
var e = new TypedBindingEntry<T>(this, property, source);
var e = new TypedBindingEntry<T>(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore
StyledProperty<T> property,
IObservable<object?> source)
{
var e = new SourceUntypedBindingEntry<T>(this, property, source);
var e = new SourceUntypedBindingEntry<T>(Owner!.Owner, this, property, source);
Add(e);
return e;
}

114
src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs

@ -1,121 +1,25 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class LocalValueBindingObserver<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
internal class LocalValueBindingObserver<T> : LocalValueBindingObserverBase<T>,
IObserver<object?>
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property)
: base(owner, property)
{
_owner = owner;
Property = property;
}
public StyledProperty<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<object?> source) => _subscription = source.Subscribe(this);
public void Dispose()
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
public void OnNext(object? value)
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
static void Execute(LocalValueBindingObserver<T> instance, T value)
{
var owner = instance._owner;
var property = instance.Property;
if (property.ValidateValue?.Invoke(value) == false)
value = instance.GetCachedDefaultValue();
owner.SetValue(property, value, BindingPriority.LocalValue);
}
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
public void OnNext(BindingValue<T> value)
{
static void Execute(LocalValueBindingObserver<T> instance, BindingValue<T> value)
{
var owner = instance._owner;
var property = instance.Property;
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
if (value.HasValue)
{
var effectiveValue = value.Value;
if (property.ValidateValue?.Invoke(effectiveValue) == false)
effectiveValue = instance.GetCachedDefaultValue();
owner.SetValue(property, effectiveValue, BindingPriority.LocalValue);
}
else
{
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
}
}
if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError)
if (value == BindingOperations.DoNothing)
return;
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
private T GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
base.OnNext(BindingValue<T>.FromUntyped(value, Property.PropertyType));
}
}
}

133
src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs

@ -0,0 +1,133 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class LocalValueBindingObserverBase<T> : IObserver<T>,
IObserver<BindingValue<T>>,
IDisposable
{
private readonly ValueStore _owner;
private readonly bool _hasDataValidation;
protected IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty<T> property)
{
_owner = owner;
Property = property;
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false;
}
public StyledProperty<T> Property { get;}
public void Start(IObservable<T> source)
{
_subscription = source.Subscribe(this);
}
public void Start(IObservable<BindingValue<T>> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
OnCompleted();
}
public void OnCompleted()
{
if (_hasDataValidation)
_owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null);
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnError(Exception error) => OnCompleted();
public void OnNext(T value)
{
static void Execute(LocalValueBindingObserverBase<T> instance, T value)
{
var owner = instance._owner;
var property = instance.Property;
if (property.ValidateValue?.Invoke(value) == false)
value = instance.GetCachedDefaultValue();
owner.SetLocalValue(property, value);
if (instance._hasDataValidation)
owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null);
}
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
public void OnNext(BindingValue<T> value)
{
static void Execute(LocalValueBindingObserverBase<T> instance, BindingValue<T> value)
{
var owner = instance._owner;
var property = instance.Property;
var originalType = value.Type;
LoggingUtils.LogIfNecessary(owner.Owner, property, value);
// Revert to the default value if the binding value fails validation, or if
// there was no value (though not if there was a data validation error).
if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) ||
(!value.HasValue && value.Type != BindingValueType.DataValidationError))
value = value.WithValue(instance.GetCachedDefaultValue());
if (value.HasValue)
owner.SetLocalValue(property, value.Value);
if (instance._hasDataValidation)
owner.Owner.OnUpdateDataValidation(property, originalType, value.Error);
}
if (value.Type is BindingValueType.DoNothing)
return;
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
private T GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
}
}
}

94
src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs

@ -1,94 +0,0 @@
using System;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.PropertyStore
{
internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>,
IDisposable
{
private readonly ValueStore _owner;
private IDisposable? _subscription;
private T? _defaultValue;
private bool _isDefaultValueInitialized;
public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty<T> property)
{
_owner = owner;
Property = property;
}
public StyledProperty<T> Property { get; }
public void Start(IObservable<object?> source)
{
_subscription = source.Subscribe(this);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_owner.OnLocalValueBindingCompleted(Property, this);
}
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this);
public void OnError(Exception error) => OnCompleted();
public void OnNext(object? value)
{
static void Execute(LocalValueUntypedBindingObserver<T> instance, object? value)
{
var owner = instance._owner;
var property = instance.Property;
if (value is BindingNotification n)
{
value = n.Value;
LoggingUtils.LogIfNecessary(owner.Owner, property, n);
}
if (value == AvaloniaProperty.UnsetValue)
{
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
}
else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue))
{
owner.SetValue(property, typedValue, BindingPriority.LocalValue);
}
else
{
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue);
LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value);
}
}
if (value == BindingOperations.DoNothing)
return;
if (Dispatcher.UIThread.CheckAccess())
{
Execute(this, value);
}
else if (value != BindingOperations.DoNothing)
{
// To avoid allocating closure in the outer scope we need to capture variables
// locally. This allows us to skip most of the allocations when on UI thread.
var instance = this;
var newValue = value;
Dispatcher.UIThread.Post(() => Execute(instance, newValue));
}
}
private T GetCachedDefaultValue()
{
if (!_isDefaultValueInitialized)
{
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType());
_isDefaultValueInitialized = true;
}
return _defaultValue!;
}
}
}

3
src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs

@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore
private readonly Func<TTarget, bool>? _validate;
public SourceUntypedBindingEntry(
AvaloniaObject target,
ValueFrame frame,
StyledProperty<TTarget> property,
IObservable<object?> source)
: base(frame, property, source)
: base(target, frame, property, source)
{
_validate = property.ValidateValue;
}

6
src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs

@ -10,18 +10,20 @@ namespace Avalonia.PropertyStore
internal sealed class TypedBindingEntry<T> : BindingEntryBase<T, T>
{
public TypedBindingEntry(
AvaloniaObject target,
ValueFrame frame,
StyledProperty<T> property,
IObservable<T> source)
: base(frame, property, source)
: base(target, frame, property, source)
{
}
public TypedBindingEntry(
AvaloniaObject target,
ValueFrame frame,
StyledProperty<T> property,
IObservable<BindingValue<T>> source)
: base(frame, property, source)
: base(target, frame, property, source)
{
}

3
src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs

@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore
private readonly Func<object?, bool>? _validate;
public UntypedBindingEntry(
AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable<object?> source)
: base(frame, property, source)
: base(target, frame, property, source)
{
_validate = ((IStyledPropertyAccessor)property).ValidateValue;
}

37
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -104,7 +104,7 @@ namespace Avalonia.PropertyStore
{
if (priority == BindingPriority.LocalValue)
{
var observer = new LocalValueUntypedBindingObserver<T>(this, property);
var observer = new LocalValueBindingObserver<T>(this, property);
DisposeExistingLocalValueBinding(property);
_localValueBindings ??= new();
_localValueBindings[property.Id] = observer;
@ -193,18 +193,7 @@ namespace Avalonia.PropertyStore
}
else
{
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
SetLocalValue(property, value);
return null;
}
}
@ -223,6 +212,21 @@ namespace Avalonia.PropertyStore
}
}
public void SetLocalValue<T>(StyledProperty<T> property, T value)
{
if (TryGetEffectiveValue(property, out var existing))
{
var effective = (EffectiveValue<T>)existing;
effective.SetLocalValueAndRaise(this, property, value);
}
else
{
var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value);
}
}
public object? GetValue(AvaloniaProperty property)
{
if (_effectiveValues.TryGetValue(property, out var v))
@ -834,8 +838,6 @@ namespace Avalonia.PropertyStore
break;
}
current?.EndReevaluation();
if (current?.Priority == BindingPriority.Unset)
{
if (current.BasePriority == BindingPriority.Unset)
@ -848,6 +850,8 @@ namespace Avalonia.PropertyStore
current.RemoveAnimationAndRaise(this, property);
}
}
current?.EndReevaluation();
}
finally
{
@ -919,7 +923,6 @@ namespace Avalonia.PropertyStore
for (var i = _effectiveValues.Count - 1; i >= 0; --i)
{
_effectiveValues.GetKeyValue(i, out var key, out var e);
e.EndReevaluation();
if (e.Priority == BindingPriority.Unset)
{
@ -929,6 +932,8 @@ namespace Avalonia.PropertyStore
if (i > _effectiveValues.Count)
break;
}
e.EndReevaluation();
}
}
finally

4
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor
{
_root = root;
_compositor = compositor;
_recordingContext = new DrawingContext(_recorder);
_recordingContext = _recorder;
CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update;
@ -225,7 +225,7 @@ public class CompositingRenderer : IRendererWithCompositor
sortedChildren.Dispose();
}
else
foreach (var ch in v.GetVisualChildren())
foreach (var ch in visualChildren)
{
var compositionChild = ch.CompositionVisual;
if (compositionChild != null)

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Collections.Pooled;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing;
/// </summary>
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
{
public Size? Size { get; set; }
public CompositionDrawList()
{
@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
public CompositionDrawList Clone()
{
var clone = new CompositionDrawList(Count) { Size = Size };
var clone = new CompositionDrawList(Count);
foreach (var r in this)
clone.Add(r.Clone());
return clone;
}
public void Render(CompositorDrawingContextProxy canvas)
public void Render(IDrawingContextImpl canvas)
{
foreach (var cmd in this)
{
if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform;
cmd.Item.Render(canvas);
}
}
public void Render(IDrawingContextImpl canvas, Matrix transform)
{
foreach (var cmd in this)
{
canvas.VisualBrushDrawList = (cmd.Item as BrushDrawOperation)?.Aux as CompositionDrawList;
if (cmd.Item is IDrawOperationWithTransform hasTransform)
canvas.Transform = hasTransform.Transform * transform;
cmd.Item.Render(canvas);
}
}
canvas.VisualBrushDrawList = null;
public Rect CalculateBounds()
{
var rect = default(Rect);
foreach (var cmd in this)
rect = rect.Union(cmd.Item.Bounds);
return rect;
}
public bool HitTest(Point pt)
{
foreach (var op in this)
if (op.Item.HitTest(pt))
return true;
return false;
}
}

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs

@ -0,0 +1,37 @@
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Drawing;
internal class CompositionDrawListSceneBrushContent : ISceneBrushContent
{
private readonly CompositionDrawList _drawList;
public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization)
{
Brush = brush;
Rect = rect;
UseScalableRasterization = useScalableRasterization;
_drawList = drawList;
}
public ITileBrush Brush { get; }
public Rect Rect { get; }
public double Opacity => Brush.Opacity;
public ITransform? Transform => Brush.Transform;
public RelativePoint TransformOrigin => Brush.TransformOrigin;
public void Dispose() => _drawList.Dispose();
public void Render(IDrawingContextImpl context, Matrix? transform)
{
if (transform.HasValue)
_drawList.Render(context, transform.Value);
else
_drawList.Render(context);
}
public bool UseScalableRasterization { get; }
}

183
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@ -7,7 +8,7 @@ using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Threading;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition;
/// <summary>
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
/// </summary>
internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
/// <inheritdoc/>
public Matrix Transform { get; set; } = Matrix.Identity;
private Stack<Matrix>? _transforms;
/// <inheritdoc/>
public void Clear(Color color)
{
// Cannot clear a deferred scene.
}
private static ThreadSafeObjectPool<Stack<bool>> OpacityMaskPopStackPool { get; } =
ThreadSafeObjectPool<Stack<bool>>.Default;
/// <inheritdoc/>
public void Dispose()
{
// Nothing to do here since we allocate no unmanaged resources.
}
private Stack<bool>? _needsToPopOpacityMask;
public Matrix Transform { get; set; } = Matrix.Identity;
public void BeginUpdate(CompositionDrawList? list)
{
_builder.Reset(list);
_drawOperationIndex = 0;
}
public CompositionDrawList EndUpdate()
public CompositionDrawList? EndUpdate()
{
// Make sure that any pending pop operations are completed
Dispose();
_builder.TrimTo(_drawOperationIndex);
return _builder.DrawOperations!;
return _builder.DrawOperations;
}
protected override void DisposeCore()
{
if (_transforms != null)
{
_transforms.Clear();
TransformStackPool.ReturnAndSetNull(ref _transforms);
}
/// <inheritdoc/>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
if (_needsToPopOpacityMask != null)
{
_needsToPopOpacityMask.Clear();
_needsToPopOpacityMask = null;
}
}
protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry));
}
else
{
@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode)
internal override void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect,
BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
var next = NextDrawAs<ImageNode>();
@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(IPen? pen, Point p1, Point p2)
protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
{
if (pen is null)
{
@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
Add(new LineNode(Transform, pen, p1, p2));
}
else
{
@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
/// <inheritdoc/>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows));
}
else
{
@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush)));
Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect));
}
else
{
++_drawOperationIndex;
}
}
public void Custom(ICustomDrawOperation custom)
public override void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom))
@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
++_drawOperationIndex;
}
public object? GetFeature(Type t) => null;
/// <inheritdoc/>
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
{
if (foreground is null)
{
@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl))
{
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground)));
Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl));
}
else
@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
public IDrawingContextLayerImpl CreateLayer(Size size)
protected override void PushTransformCore(Matrix matrix)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
_transforms ??= TransformStackPool.Get();
_transforms.Push(Transform);
Transform = matrix * Transform;
}
protected override void PopTransformCore() =>
Transform = (_transforms ?? throw new InvalidOperationException()).Pop();
/// <inheritdoc/>
public void PopClip()
protected override void PopClipCore()
{
var next = NextDrawAs<ClipNode>();
@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
/// <inheritdoc/>
public void PopGeometryClip()
protected override void PopGeometryClipCore()
{
var next = NextDrawAs<GeometryClipNode>();
@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PopBitmapBlendMode()
protected override void PopBitmapBlendModeCore()
{
var next = NextDrawAs<BitmapBlendModeNode>();
@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PopOpacity()
protected override void PopOpacityCore()
{
var next = NextDrawAs<OpacityNode>();
@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PopOpacityMask()
protected override void PopOpacityMaskCore()
{
if (!_needsToPopOpacityMask!.Pop())
return;
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null))
{
Add(new OpacityMaskNode());
Add(new OpacityMaskPopNode());
}
else
{
@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PushClip(Rect clip)
protected override void PushClipCore(Rect clip)
{
var next = NextDrawAs<ClipNode>();
@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc />
public void PushClip(RoundedRect clip)
protected override void PushClipCore(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PushGeometryClip(IGeometryImpl? clip)
protected override void PushGeometryClipCore(Geometry clip)
{
if (clip is null)
if (clip.PlatformImpl is null)
return;
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl))
{
Add(new GeometryClipNode(Transform, clip));
Add(new GeometryClipNode(Transform, clip.PlatformImpl));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void PushOpacity(double opacity, Rect bounds)
protected override void PushOpacityCore(double opacity, Rect bounds)
{
var next = NextDrawAs<OpacityNode>();
@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
/// <inheritdoc/>
public void PushOpacityMask(IBrush mask, Rect bounds)
protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
{
var next = NextDrawAs<OpacityMaskNode>();
bool needsToPop = true;
if (next == null || !next.Item.Equals(mask, bounds))
{
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
var immutableMask = ConvertBrush(mask);
if (immutableMask != null)
Add(new OpacityMaskNode(immutableMask, bounds));
else
needsToPop = false;
}
else
{
++_drawOperationIndex;
}
_needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get();
_needsToPopOpacityMask.Push(needsToPop);
}
/// <inheritdoc/>
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{
var next = NextDrawAs<BitmapBlendModeNode>();
@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
: null;
}
private static IDisposable? CreateChildScene(IBrush? brush)
private IImmutableBrush? ConvertBrush(IBrush? brush)
{
if (brush is VisualBrush visualBrush)
{
var visual = visualBrush.Visual;
if (visual != null)
{
// TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer
// We should directly reference the corresponding CompositionVisual (which should
// be attached to the same composition target) like UWP does.
// Render-able visuals shouldn't be dangling unattached
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var recorder = new CompositionDrawingContext();
recorder.BeginUpdate(null);
ImmediateRenderer.Render(visual, new DrawingContext(recorder));
var drawList = recorder.EndUpdate();
drawList.Size = visual.Bounds.Size;
return drawList;
}
}
return null;
if (brush is IMutableBrush mutable)
return mutable.ToImmutable();
if (brush is ISceneBrush sceneBrush)
return sceneBrush.CreateContent();
return (IImmutableBrush?)brush;
}
}

30
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server;
internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private IDrawingContextImpl _impl;
private readonly VisualBrushRenderer _visualBrushRenderer;
public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer)
public CompositorDrawingContextProxy(IDrawingContextImpl impl)
{
_impl = impl;
_visualBrushRenderer = visualBrushRenderer;
}
// This is a hack to make it work with the current way of handling visual brushes
public CompositionDrawList? VisualBrushDrawList
{
get => _visualBrushRenderer.VisualBrushDrawList;
set => _visualBrushRenderer.VisualBrushDrawList = value;
}
public Matrix PostTransform { get; set; } = Matrix.Identity;
@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
}
public object? GetFeature(Type t) => _impl.GetFeature(t);
public class VisualBrushRenderer : IVisualBrushRenderer
{
public CompositionDrawList? VisualBrushDrawList { get; set; }
public Size GetRenderTargetSize(IVisualBrush brush)
{
return VisualBrushDrawList?.Size ?? default;
}
public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
if (VisualBrushDrawList != null)
{
foreach (var cmd in VisualBrushDrawList)
cmd.Item.Render(context);
}
}
}
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{

7
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server
Readback.CompleteWrite(Revision);
_redrawRequested = false;
using (var targetContext = _renderTarget.CreateDrawingContext(null))
using (var targetContext = _renderTarget.CreateDrawingContext())
{
var layerSize = Size * Scaling;
if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server
if (!_dirtyRect.IsDefault)
{
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
using (var context = _layer.CreateDrawingContext(visualBrushHelper))
using (var context = _layer.CreateDrawingContext())
{
context.PushClip(_dirtyRect);
context.Clear(Colors.Transparent);
Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect);
Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect);
context.PopClip();
}
}

29
src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs

@ -1,29 +0,0 @@
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines a renderer used to render a visual brush to a bitmap.
/// </summary>
[Unstable]
public interface IVisualBrushRenderer
{
/// <summary>
/// Gets the size of the intermediate render target to which the visual brush should be
/// drawn.
/// </summary>
/// <param name="brush">The visual brush.</param>
/// <returns>The size of the intermediate render target to create.</returns>
Size GetRenderTargetSize(IVisualBrush brush);
/// <summary>
/// Renders a visual brush to a bitmap.
/// </summary>
/// <param name="context">The drawing context to render to.</param>
/// <param name="brush">The visual brush.</param>
/// <returns>A bitmap containing the rendered brush.</returns>
void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush);
}
}

37
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -14,19 +14,8 @@ namespace Avalonia.Rendering
/// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// </summary>
internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer
internal class ImmediateRenderer
{
/// <summary>
/// Renders a visual to a render target.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="target">The render target.</param>
public static void Render(Visual visual, IRenderTarget target)
{
using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
Render(context, visual, visual.Bounds);
}
/// <summary>
/// Renders a visual to a drawing context.
/// </summary>
@ -36,28 +25,6 @@ namespace Avalonia.Rendering
{
Render(context, visual, visual.Bounds);
}
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
(brush.Visual as IVisualBrushInitialize)?.EnsureInitialized();
return brush.Visual?.Bounds.Size ?? default;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
if (brush.Visual is { } visual)
{
Render(new DrawingContext(context), visual, visual.Bounds);
}
}
internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
{
Render(context, visual, visual.Bounds);
}
private static Rect GetTransformedBounds(Visual visual)
{
@ -75,7 +42,7 @@ namespace Avalonia.Rendering
}
private static void Render(DrawingContext context, Visual visual, Rect clipRect)
public static void Render(DrawingContext context, Visual visual, Rect clipRect)
{
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;

15
src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs

@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Base class for draw operations that can use a brush.
/// </summary>
internal abstract class BrushDrawOperation : DrawOperation
internal abstract class BrushDrawOperation : DrawOperationWithTransform
{
public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux)
public IImmutableBrush? Brush { get; }
public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush)
: base(bounds, transform)
{
Aux = aux;
Brush = brush;
}
/// <summary>
/// Auxiliary data required to draw the brush
/// </summary>
public IDisposable? Aux { get; }
public override void Dispose()
{
Aux?.Dispose();
(Brush as ISceneBrushContent)?.Dispose();
base.Dispose();
}
}

4
src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs

@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the scene graph which represents a clip push or pop.
/// </summary>
internal class ClipNode : IDrawOperation
internal class ClipNode : IDrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if (Clip.HasValue)
{
context.PushClip(Clip.Value);

39
src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs

@ -4,30 +4,19 @@ using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
internal sealed class CustomDrawOperation : DrawOperation
internal sealed class CustomDrawOperation : DrawOperationWithTransform
{
public Matrix Transform { get; }
public ICustomDrawOperation Custom { get; }
public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform)
: base(custom.Bounds, transform)
{
Transform = transform;
Custom = custom;
}
public override bool HitTest(Point p)
{
if (Transform.HasInverse)
{
return Custom.HitTest(p * Transform.Invert());
}
return false;
}
public override bool HitTest(Point p) => Custom.HitTest(p);
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
Custom.Render(context);
}
@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph
Transform == transform && Custom?.Equals(custom) == true;
}
public interface ICustomDrawOperation : IDrawOperation, IEquatable<ICustomDrawOperation>
public interface ICustomDrawOperation : IEquatable<ICustomDrawOperation>, IDisposable
{
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
}
}

10
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph
{
}
}
internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform
{
protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform)
{
Transform = transform;
}
public Matrix Transform { get; }
}
}

37
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph
{
public EllipseNode(
Matrix transform,
IBrush? brush,
IImmutableBrush? brush,
IPen? pen,
Rect rect,
IDisposable? aux = null)
: base(rect.Inflate(pen?.Thickness ?? 0), transform, aux)
Rect rect)
: base(rect.Inflate(pen?.Thickness ?? 0), transform, brush)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
}
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
public ImmutablePen? Pen { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the rect of the ellipse to draw.
/// </summary>
@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph
rect.Equals(Rect);
}
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawEllipse(Brush, Pen, Rect);
}
public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect);
public override bool HitTest(Point p)
{
if (!Transform.TryInvert(out Matrix inverted))
{
return false;
}
p *= inverted;
var center = Rect.Center;
var strokeThickness = Pen?.Thickness ?? 0;
@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph
return false;
}
public override void Dispose()
{
(Brush as ISceneBrushContent)?.Dispose();
}
}
}

24
src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the scene graph which represents a rectangle draw.
/// </summary>
internal class ExperimentalAcrylicNode : DrawOperation
internal class ExperimentalAcrylicNode : DrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="RectangleNode"/> class.
@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph
RoundedRect rect)
: base(rect.Rect, transform)
{
Transform = transform;
Material = material.ToImmutable();
Rect = rect;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
public IExperimentalAcrylicMaterial Material { get; }
/// <summary>
@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if(context is IDrawingContextWithAcrylicLikeSupport idc)
{
idc.DrawRectangle(Material, Rect);
@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override bool HitTest(Point p)
{
// TODO: This doesn't respect CornerRadius yet.
if (Transform.HasInverse)
{
p *= Transform.Invert();
var rect = Rect.Rect;
return rect.ContainsExclusive(p);
}
return false;
}
public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p);
}
}

4
src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs

@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the scene graph which represents a geometry clip push or pop.
/// </summary>
internal class GeometryClipNode : IDrawOperation
internal class GeometryClipNode : IDrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if (Clip != null)
{
context.PushGeometryClip(Clip);

30
src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs

@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="geometry">The geometry.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public GeometryNode(Matrix transform,
IBrush? brush,
IImmutableBrush? brush,
IPen? pen,
IGeometryImpl geometry,
IDisposable? aux)
: base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux)
IGeometryImpl geometry)
: base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Geometry = geometry;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry);
}
/// <inheritdoc/>
public override bool HitTest(Point p)
{
if (Transform.HasInverse)
{
p *= Transform.Invert();
return (Brush != null && Geometry.FillContains(p)) ||
(Pen != null && Geometry.StrokeContains(Pen, p));
}
return false;
return (Brush != null && Geometry.FillContains(p)) ||
(Pen != null && Geometry.StrokeContains(Pen, p));
}
}
}

33
src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public GlyphRunNode(
Matrix transform,
IBrush foreground,
IRef<IGlyphRunImpl> glyphRun,
IDisposable? aux = null)
: base(new Rect(glyphRun.Item.Size), transform, aux)
IImmutableBrush foreground,
IRef<IGlyphRunImpl> glyphRun)
: base(new Rect(glyphRun.Item.Size), transform, foreground)
{
Transform = transform;
Foreground = foreground.ToImmutable();
GlyphRun = glyphRun.Clone();
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary>
/// Gets the glyph run to draw.
/// </summary>
public IRef<IGlyphRunImpl> GlyphRun { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGlyphRun(Foreground, GlyphRun);
}
public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun);
/// <summary>
/// Determines if this draw operation equals another.
@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph
internal bool Equals(Matrix transform, IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
{
return transform == Transform &&
Equals(foreground, Foreground) &&
Equals(foreground, Brush) &&
Equals(glyphRun.Item, GlyphRun.Item);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p);
public override void Dispose()
{
GlyphRun?.Dispose();
base.Dispose();
}
}
}

10
src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs

@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Represents a node in the low-level scene graph that represents geometry.
/// </summary>
public interface IDrawOperation : IDisposable
internal interface IDrawOperation : IDisposable
{
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
}
internal interface IDrawOperationWithTransform : IDrawOperation
{
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
Matrix Transform { get; }
}
}

21
src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs

@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the scene graph which represents an image draw.
/// </summary>
internal class ImageNode : DrawOperation
internal class ImageNode : DrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageNode"/> class.
@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph
public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform)
{
Transform = transform;
Source = source.Clone();
Opacity = opacity;
SourceRect = sourceRect;
DestRect = destRect;
BitmapInterpolationMode = bitmapInterpolationMode;
SourceVersion = Source.Item.Version;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
}
/// <summary>
/// Gets the image to draw.
@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph
/// </value>
public BitmapInterpolationMode BitmapInterpolationMode { get; }
/// <summary>
/// The bitmap blending mode.
/// </summary>
/// <value>
/// The blending mode.
/// </value>
public BitmapBlendingMode BitmapBlendingMode { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
public override void Dispose()
{

19
src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs

@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the scene graph which represents a line draw.
/// </summary>
internal class LineNode : BrushDrawOperation
internal class LineNode : DrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="GeometryNode"/> class.
@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph
Matrix transform,
IPen pen,
Point p1,
Point p2,
IDisposable? aux = null)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux)
Point p2)
: base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
{
Transform = transform;
Pen = pen.ToImmutable();
P1 = p1;
P2 = p2;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawLine(Pen, P1, P2);
}
public override bool HitTest(Point p)
{
if (!Transform.HasInverse)
return false;
p *= Transform.Invert();
var halfThickness = Pen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness;
var maxX = Math.Max(P1.X, P2.X) + halfThickness;

41
src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs

@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null)
: base(default, Matrix.Identity, aux)
public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
: base(default, Matrix.Identity, mask)
{
Mask = mask.ToImmutable();
MaskBounds = bounds;
}
/// <summary>
/// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
/// opacity mask pop.
/// </summary>
public OpacityMaskNode()
: base(default, Matrix.Identity, null)
{
}
/// <summary>
/// Gets the mask to be pushed or null if the operation represents a pop.
/// </summary>
public IBrush? Mask { get; }
/// <summary>
/// Gets the bounds of the opacity mask or null if the operation represents a pop.
/// </summary>
@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds;
public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds;
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
if (Mask != null)
{
context.PushOpacityMask(Mask, MaskBounds!.Value);
}
else
{
context.PopOpacityMask();
}
context.PushOpacityMask(Brush!, MaskBounds!.Value);
}
}
internal class OpacityMaskPopNode : DrawOperation
{
public OpacityMaskPopNode() : base(default, Matrix.Identity)
{
}
public override bool HitTest(Point p) => false;
public override void Render(IDrawingContextImpl context) => context.PopOpacityMask();
}
}

52
src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs

@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="aux">Auxiliary data required to draw the brush.</param>
public RectangleNode(
Matrix transform,
IBrush? brush,
IImmutableBrush? brush,
IPen? pen,
RoundedRect rect,
BoxShadows boxShadows,
IDisposable? aux = null)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux)
BoxShadows boxShadows)
: base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
BoxShadows = boxShadows;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush? Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
}
public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
/// <inheritdoc/>
public override bool HitTest(Point p)
{
// TODO: This doesn't respect CornerRadius yet.
if (Transform.HasInverse)
if (Brush != null)
{
p *= Transform.Invert();
if (Brush != null)
{
var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
return rect.ContainsExclusive(p);
}
else
{
var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
}
var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
return rect.ContainsExclusive(p);
}
else
{
var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
}
return false;
}
}
}

22
src/Avalonia.Base/StyledElement.cs

@ -46,6 +46,7 @@ namespace Avalonia
defaultBindingMode: BindingMode.OneWay,
validate: null,
coerce: null,
enableDataValidation: false,
notifying: DataContextNotifying);
/// <summary>
@ -802,8 +803,11 @@ namespace Avalonia
if (theme.HasChildren)
{
foreach (var child in theme.Children)
ApplyStyle(child, null, type);
var children = theme.Children;
for (var i = 0; i < children.Count; i++)
{
ApplyStyle(children[i], null, type);
}
}
}
@ -815,8 +819,11 @@ namespace Avalonia
if (host.IsStylesInitialized)
{
foreach (var style in host.Styles)
ApplyStyle(style, host, FrameType.Style);
var styles = host.Styles;
for (var i = 0; i < styles.Count; ++i)
{
ApplyStyle(styles[i], host, FrameType.Style);
}
}
}
@ -825,8 +832,11 @@ namespace Avalonia
if (style is Style s)
s.TryAttach(this, host, type);
foreach (var child in style.Children)
ApplyStyle(child, host, type);
var children = style.Children;
for (var i = 0; i < children.Count; i++)
{
ApplyStyle(children[i], host, type);
}
}
private void ReevaluateImplicitTheme()

6
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -16,11 +16,13 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="enableDataValidation">Whether the property is interested in data validation.</param>
public StyledPropertyMetadata(
Optional<TValue> defaultValue = default,
BindingMode defaultBindingMode = BindingMode.Default,
Func<AvaloniaObject, TValue, TValue>? coerce = null)
: base(defaultBindingMode)
Func<AvaloniaObject, TValue, TValue>? coerce = null,
bool enableDataValidation = false)
: base(defaultBindingMode, enableDataValidation)
{
_defaultValue = defaultValue;
CoerceValue = coerce;

47
src/Avalonia.Base/Styling/Activators/NthChildActivator.cs

@ -1,4 +1,5 @@
#nullable enable
using System;
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
private int _index = -1;
public NthChildActivator(
ILogical control,
@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive()
{
return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}
protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
protected override void Initialize()
{
_provider.ChildIndexChanged += ChildIndexChanged;
}
protected override void Deinitialize()
{
_provider.ChildIndexChanged -= ChildIndexChanged;
}
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
// 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
// 2. e.Child is null, when all children indices were changed.
// 3. Subscribed child index was changed.
if (_reversed
|| e.Child is null
|| e.Child == _control)
// 1. Subscribed child index was changed
// 2. Child indexes were reset
// 3. We're a reversed (nth-last-child) selector and total count has changed
if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) ||
(_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged))
{
// We're using the _index field to pass the index of the child to EvaluateIsActive
// *only* when the active state is re-evaluated via this event handler. The docs
// for EvaluateIsActive say:
//
// > This method should read directly from its inputs and not rely on any
// > subscriptions to fire in order to be up-to-date.
//
// Which is good advice in general, however in this case we need to break the rule
// and use the value from the event subscription instead of calling
// IChildIndexProvider.GetChildIndex. This is because this event can be fired during
// the process of realizing an element of a virtualized list; in this case calling
// GetChildIndex may not return the correct index as the element isn't yet realized.
_index = e.Index;
ReevaluateIsActive();
_index = -1;
}
}
private void TotalCountChanged(object? sender, EventArgs e)
{
if (_reversed)
ReevaluateIsActive();
}
}
}

5
src/Avalonia.Base/Styling/NthChildSelector.cs

@ -61,7 +61,7 @@ namespace Avalonia.Styling
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
: Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
: Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed);
}
else
{
@ -70,10 +70,9 @@ namespace Avalonia.Styling
}
internal static SelectorMatch Evaluate(
ILogical logical, IChildIndexProvider childIndexProvider,
int index, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;

13
src/Avalonia.Base/Styling/OrSelector.cs

@ -71,9 +71,9 @@ namespace Avalonia.Styling
var activators = new OrActivatorBuilder();
var neverThisInstance = false;
foreach (var selector in _selectors)
for (var i = 0; i < _selectors.Count; i++)
{
var match = selector.Match(control, parent, subscribe);
var match = _selectors[i].Match(control, parent, subscribe);
switch (match.Result)
{
@ -108,16 +108,19 @@ namespace Avalonia.Styling
internal override void ValidateNestingSelector(bool inControlTheme)
{
foreach (var selector in _selectors)
selector.ValidateNestingSelector(inControlTheme);
for (var i = 0; i < _selectors.Count; i++)
{
_selectors[i].ValidateNestingSelector(inControlTheme);
}
}
private Type? EvaluateTargetType()
{
Type? result = null;
foreach (var selector in _selectors)
for (var i = 0; i < _selectors.Count; i++)
{
var selector = _selectors[i];
if (selector.TargetType == null)
{
return null;

2
src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs

@ -15,7 +15,7 @@ namespace Avalonia.Styling
AvaloniaProperty property,
BindingMode mode,
IObservable<object?> source)
: base(instance, property, source)
: base(target, instance, property, source)
{
_target = target;
_mode = mode;

8
src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
@ -19,6 +20,13 @@ namespace Avalonia.Styling
public object? GetValue() => _value ??= _template.Build();
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
{
state = BindingValueType.Value;
error = null;
return false;
}
void IValueEntry.Unsubscribe() { }
}
}

10
src/Avalonia.Base/Styling/Setter.cs

@ -90,6 +90,13 @@ namespace Avalonia.Styling
object? IValueEntry.GetValue() => Value;
bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
{
state = BindingValueType.Value;
error = null;
return false;
}
private AvaloniaProperty EnsureProperty()
{
return Property ?? throw new InvalidOperationException("Setter.Property must be set.");
@ -99,7 +106,8 @@ namespace Avalonia.Styling
{
if (!Property!.IsDirect)
{
var i = binding.Initiate(target, Property)!;
var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false;
var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!;
var mode = i.Mode;
if (mode == BindingMode.Default)

15
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Styling.Activators;
using Avalonia.Utilities;
@ -15,7 +14,7 @@ namespace Avalonia.Styling
internal class TypeNameAndClassSelector : Selector
{
private readonly Selector? _previous;
private readonly Lazy<List<string>> _classes = new Lazy<List<string>>(() => new List<string>());
private List<string>? _classes;
private Type? _targetType;
private string? _selectorString;
@ -81,7 +80,7 @@ namespace Avalonia.Styling
/// <summary>
/// The style classes which the selector matches.
/// </summary>
public IList<string> Classes => _classes.Value;
public IList<string> Classes => _classes ??= new();
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -122,16 +121,16 @@ namespace Avalonia.Styling
return SelectorMatch.NeverThisInstance;
}
if (_classes.IsValueCreated && _classes.Value.Count > 0)
if (_classes is { Count: > 0 })
{
if (subscribe)
{
var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
var observable = new StyleClassActivator(control.Classes, _classes);
return new SelectorMatch(observable);
}
if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes))
if (!StyleClassActivator.AreClassesMatching(control.Classes, _classes))
{
return SelectorMatch.NeverThisInstance;
}
@ -172,9 +171,9 @@ namespace Avalonia.Styling
builder.Append(Name);
}
if (_classes.IsValueCreated && _classes.Value.Count > 0)
if (_classes is { Count: > 0 })
{
foreach (var c in Classes)
foreach (var c in _classes)
{
if (!c.StartsWith(":"))
{

7
src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs

@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace Avalonia.Threading
{
public class ThreadSafeObjectPool<T> where T : class, new()
internal class ThreadSafeObjectPool<T> where T : class, new()
{
private Stack<T> _stack = new Stack<T>();
public static ThreadSafeObjectPool<T> Default { get; } = new ThreadSafeObjectPool<T>();
@ -17,11 +17,14 @@ namespace Avalonia.Threading
}
}
public void Return(T obj)
public void ReturnAndSetNull(ref T? obj)
{
if (obj == null)
return;
lock (_stack)
{
_stack.Push(obj);
obj = null;
}
}
}

10
src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs

@ -304,13 +304,9 @@ namespace Avalonia.Utilities
{
if (_entryCount == _entries!.Length)
{
const double growthFactor = 1.2;
var newSize = (int)(_entryCount * growthFactor);
if (newSize == _entryCount)
{
newSize++;
}
var newSize = _entryCount == DefaultInitialCapacity ?
DefaultInitialCapacity * 2 :
(int)(_entryCount * 1.5);
var destEntries = new Entry[newSize];

17
src/Avalonia.Base/Visual.cs

@ -732,6 +732,23 @@ namespace Avalonia
}
}
internal override void OnTemplatedParentControlThemeChanged()
{
base.OnTemplatedParentControlThemeChanged();
var count = VisualChildren.Count;
var templatedParent = TemplatedParent;
for (var i = 0; i < count; ++i)
{
if (VisualChildren[i] is StyledElement child &&
child.TemplatedParent == templatedParent)
{
child.OnTemplatedParentControlThemeChanged();
}
}
}
/// <summary>
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>

2
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)

2
src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs

@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
}
}

2
src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs

@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex(DataGridRow row)
{
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index));
}
/// <summary>

2
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@ -202,7 +202,7 @@
<Style Selector="^ /template/ Rectangle#BackgroundRectangle">
<Setter Property="IsVisible" Value="False" />
<Setter Property="Fill" Value="{DynamicResource HighlightBrush}" />
<Setter Property="Fill" Value="{DynamicResource HighlightBrush2}" />
</Style>
<Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">

9
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@ -536,11 +536,12 @@ namespace Avalonia.Controls
internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
var index = virtInfo.Index;
_viewportManager.OnElementPrepared(element, virtInfo);
if (ElementPrepared != null)
{
var index = virtInfo.Index;
if (_elementPreparedArgs == null)
{
@ -554,7 +555,7 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index));
}
internal void OnElementClearing(Control element)
@ -573,7 +574,7 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1));
}
internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
@ -592,7 +593,7 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex));
}
private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue)

2
src/Avalonia.Controls.ItemsRepeater/Layout/StackLayout.cs

@ -26,7 +26,7 @@ namespace Avalonia.Layout
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<StackPanel>();
StackPanel.OrientationProperty.AddOwner<StackLayout>();
/// <summary>
/// Defines the <see cref="Spacing"/> property.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save