Browse Source

Merge branch 'master' into simple-theme-fixes

pull/10459/head
Luis von der Eltz 3 years ago
committed by GitHub
parent
commit
e1e04bc87f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Avalonia.Desktop.slnf
  2. 15
      Avalonia.sln
  3. 5
      build/DevAnalyzers.props
  4. 8
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  5. 104
      nukebuild/BuildTasksPatcher.cs
  6. 41
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  7. 3
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  8. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  9. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  10. 1
      samples/Directory.Build.props
  11. 13
      samples/IntegrationTestApp/MainWindow.axaml
  12. 20
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  13. 5
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  14. 4
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  15. 15
      src/Avalonia.Base/AvaloniaObject.cs
  16. 4
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  17. 13
      src/Avalonia.Base/AvaloniaProperty.cs
  18. 18
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  19. 39
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  20. 4
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  21. 4
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  22. 27
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  23. 10
      src/Avalonia.Base/Input/DataFormats.cs
  24. 25
      src/Avalonia.Base/Input/DataObject.cs
  25. 50
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  26. 17
      src/Avalonia.Base/Input/IDataObject.cs
  27. 1
      src/Avalonia.Base/Input/MouseDevice.cs
  28. 1
      src/Avalonia.Base/Input/PenDevice.cs
  29. 6
      src/Avalonia.Base/Layout/Layoutable.cs
  30. 69
      src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
  31. 2
      src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
  32. 66
      src/Avalonia.Base/Media/DrawingBrush.cs
  33. 406
      src/Avalonia.Base/Media/DrawingContext.cs
  34. 145
      src/Avalonia.Base/Media/DrawingGroup.cs
  35. 2
      src/Avalonia.Base/Media/DrawingImage.cs
  36. 2
      src/Avalonia.Base/Media/FormattedText.cs
  37. 2
      src/Avalonia.Base/Media/IImageBrush.cs
  38. 31
      src/Avalonia.Base/Media/ISceneBrush.cs
  39. 16
      src/Avalonia.Base/Media/IVisualBrush.cs
  40. 8
      src/Avalonia.Base/Media/ImageBrush.cs
  41. 2
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  42. 18
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  43. 11
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  44. 6
      src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
  45. 66
      src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
  46. 112
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  47. 4
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  48. 13
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  49. 21
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  50. 9
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  51. 191
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  52. 25
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  53. 7
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  54. 47
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  55. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  56. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  57. 2
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  58. 4
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  59. 24
      src/Avalonia.Base/Media/VisualBrush.cs
  60. 2
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  61. 6
      src/Avalonia.Base/Platform/IGeometryImpl.cs
  62. 6
      src/Avalonia.Base/Platform/IRenderTarget.cs
  63. 5
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  64. 9
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  65. 17
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  66. 2
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  67. 12
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  68. 76
      src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
  69. 11
      src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
  70. 5
      src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
  71. 51
      src/Avalonia.Base/PropertyStore/EffectiveValue.cs
  72. 26
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  73. 11
      src/Avalonia.Base/PropertyStore/IValueEntry.cs
  74. 8
      src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
  75. 6
      src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
  76. 114
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs
  77. 133
      src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs
  78. 94
      src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs
  79. 3
      src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs
  80. 6
      src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs
  81. 3
      src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs
  82. 37
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  83. 2
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  84. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  85. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  86. 187
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  87. 34
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  88. 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  89. 18
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  90. 29
      src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
  91. 39
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  92. 15
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  93. 4
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  94. 39
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  95. 10
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  96. 37
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
  97. 24
      src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
  98. 4
      src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
  99. 30
      src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
  100. 33
      src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs

3
Avalonia.Desktop.slnf

@ -42,6 +42,7 @@
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
@ -61,4 +62,4 @@
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
]
}
}
}

15
Avalonia.sln

@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject
@ -548,7 +555,6 @@ 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
@ -560,6 +566,10 @@ 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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -626,6 +636,7 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

5
build/DevAnalyzers.props

@ -5,5 +5,10 @@
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
</ItemGroup>
</Project>

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
}
}
}
}
}

41
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -306,25 +306,8 @@ namespace ControlCatalog.Pages
resultText += @$"
Content:
";
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
resultText += await ReadTextFromFile(file, 10000);
}
openedFileContent.Text = resultText;
@ -354,6 +337,28 @@ namespace ControlCatalog.Pages
}
}
public static async Task<string> ReadTextFromFile(IStorageFile file, int length)
{
#if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync();
#else
using var stream = await file.OpenReadAsync();
#endif
using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory.
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
var charsRead = await reader.ReadAsync(buffer, 0, length);
return new string(buffer, 0, charsRead);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);

3
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -25,7 +25,6 @@
BorderThickness="2">
<TextBlock Name="DragStateCustom" TextWrapping="Wrap">Drag Me (custom)</TextBlock>
</Border>
<TextBlock Name="DropState" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Margin="8"
@ -47,5 +46,7 @@
</Border>
</StackPanel>
</WrapPanel>
<TextBlock x:Name="DropState" TextWrapping="Wrap" />
</StackPanel>
</UserControl>

48
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -1,27 +1,29 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
TextBlock _DropState;
private readonly TextBlock _dropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Get<TextBlock>("DropState");
_dropState = this.Get<TextBlock>("DropState");
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move);
SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy);
}
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects)
@ -68,12 +70,12 @@ namespace ControlCatalog.Pages
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(DataFormats.Files)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
void Drop(object? sender, DragEventArgs e)
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
@ -85,11 +87,41 @@ namespace ControlCatalog.Pages
}
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
{
_dropState.Text = e.Data.GetText();
}
else if (e.Data.Contains(DataFormats.Files))
{
var files = e.Data.GetFiles() ?? Array.Empty<IStorageItem>();
var contentStr = "";
foreach (var item in files)
{
if (item is IStorageFile file)
{
var content = await DialogsPage.ReadTextFromFile(file, 1000);
contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}";
}
else if (item is IStorageFolder folder)
{
var items = await folder.GetItemsAsync();
contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}";
}
}
_dropState.Text = contentStr;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty<string>());
{
var files = e.Data.GetFileNames();
_dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty<string>());
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
{
_dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
}
dragMe.PointerPressed += DoDrag;

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>

1
samples/Directory.Build.props

@ -6,4 +6,5 @@
<LangVersion>11</LangVersion>
</PropertyGroup>
<Import Project="..\build\SharedVersion.props" />
<Import Project="..\build\DevAnalyzers.props" />
</Project>

13
samples/IntegrationTestApp/MainWindow.axaml

@ -56,6 +56,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>
@ -153,6 +163,9 @@
</StackPanel>
</Grid>
</TabItem>
<TabItem Header="SliderTab">
<Slider VerticalAlignment="Top" Name="Slider" Value="30"/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

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));
}

15
src/Avalonia.Base/AvaloniaObject.cs

@ -664,14 +664,12 @@ namespace Avalonia
/// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged<T>(
DirectPropertyBase<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority = BindingPriority.LocalValue)
T oldValue,
T newValue)
{
RaisePropertyChanged(property, oldValue, newValue, priority, true);
RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true);
}
/// <summary>
@ -720,7 +718,7 @@ namespace Avalonia
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
protected bool SetAndRaise<T>(DirectPropertyBase<T> property, ref T field, T value)
{
VerifyAccess();
@ -786,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;
}
}
}

39
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@ -138,18 +138,18 @@ namespace Avalonia.Controls
protected override void Initialize()
{
_target.ResourcesChanged += ResourcesChanged;
if (_target is StyledElement themeStyleable)
if (_target is IThemeVariantHost themeVariantHost)
{
themeStyleable.PropertyChanged += PropertyChanged;
themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged;
}
}
protected override void Deinitialize()
{
_target.ResourcesChanged -= ResourcesChanged;
if (_target is StyledElement themeStyleable)
if (_target is IThemeVariantHost themeVariantHost)
{
themeStyleable.PropertyChanged -= PropertyChanged;
themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged;
}
}
@ -163,18 +163,15 @@ namespace Avalonia.Controls
PublishNext(GetValue());
}
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void ActualThemeVariantChanged(object? sender, EventArgs e)
{
if (e.Property == StyledElement.ActualThemeVariantProperty)
{
PublishNext(GetValue());
}
PublishNext(GetValue());
}
private object? GetValue()
{
if (_target is not StyledElement themeStyleable
|| !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
if (_target is not IThemeVariantHost themeVariantHost
|| !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value))
{
value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
}
@ -236,9 +233,9 @@ namespace Avalonia.Controls
{
_owner.ResourcesChanged -= ResourcesChanged;
}
if (_owner is StyledElement styleable)
if (_owner is IThemeVariantHost themeVariantHost)
{
styleable.PropertyChanged += PropertyChanged;
themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged;
}
_owner = _target.Owner;
@ -247,20 +244,18 @@ namespace Avalonia.Controls
{
_owner.ResourcesChanged += ResourcesChanged;
}
if (_owner is StyledElement styleable2)
if (_owner is IThemeVariantHost themeVariantHost2)
{
styleable2.PropertyChanged += PropertyChanged;
themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged;
}
PublishNext();
}
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void ActualThemeVariantChanged(object? sender, EventArgs e)
{
if (e.Property == StyledElement.ActualThemeVariantProperty)
{
PublishNext();
}
PublishNext();
}
private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
@ -270,8 +265,8 @@ namespace Avalonia.Controls
private object? GetValue()
{
if (!(_target.Owner is StyledElement themeStyleable)
|| !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
if (!(_target.Owner is IThemeVariantHost themeVariantHost)
|| !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value))
{
value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
}

4
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -22,7 +22,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc)
{
WeakEvents.PropertyChanged.Subscribe(inpc, this);
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
ValueChanged(GetValue(target));
@ -39,7 +39,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc)
{
WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
}
}

4
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
WeakEvents.PropertyChanged.Unsubscribe(inpc, this);
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
private object? GetReferenceTarget()
@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null)
WeakEvents.PropertyChanged.Subscribe(inpc, this);
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
}
}

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;
}
}
}

10
src/Avalonia.Base/Input/DataFormats.cs

@ -1,4 +1,6 @@
namespace Avalonia.Input
using System;
namespace Avalonia.Input
{
public static class DataFormats
{
@ -7,9 +9,15 @@
/// </summary>
public static readonly string Text = nameof(Text);
/// <summary>
/// Dataformat for one or more files.
/// </summary>
public static readonly string Files = nameof(Files);
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
[Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")]
public static readonly string FileNames = nameof(FileNames);
}
}

25
src/Avalonia.Base/Input/DataObject.cs

@ -2,37 +2,34 @@
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new Dictionary<string, object>();
private readonly Dictionary<string, object> _items = new();
/// <inheritdoc />
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
/// <inheritdoc />
public object? Get(string dataFormat)
{
if (_items.ContainsKey(dataFormat))
return _items[dataFormat];
return null;
return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
/// <inheritdoc />
public IEnumerable<string> GetDataFormats()
{
return _items.Keys;
}
public IEnumerable<string>? GetFileNames()
{
return Get(DataFormats.FileNames) as IEnumerable<string>;
}
public string? GetText()
{
return Get(DataFormats.Text) as string;
}
/// <summary>
/// Sets a value to the internal store of the data object with <see cref="DataFormats"/> as a key.
/// </summary>
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;

50
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
public static class DataObjectExtensions
{
/// <summary>
/// Returns a list of files if the DataObject contains files or filenames.
/// <seealso cref="DataFormats.Files"/>.
/// </summary>
/// <returns>
/// Collection of storage items - files or folders. If format isn't available, returns null.
/// </returns>
public static IEnumerable<IStorageItem>? GetFiles(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Files) as IEnumerable<IStorageItem>;
}
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
/// <returns>
/// Collection of file names. If format isn't available, returns null.
/// </returns>
[System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")]
public static IEnumerable<string>? GetFileNames(this IDataObject dataObject)
{
return (dataObject.Get(DataFormats.FileNames) as IEnumerable<string>)
?? dataObject.GetFiles()?
.Select(f => f.TryGetLocalPath())
.Where(p => !string.IsNullOrEmpty(p))
.OfType<string>();
}
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
/// <returns>
/// A text string. If format isn't available, returns null.
/// </returns>
public static string? GetText(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Text) as string;
}
}
}

17
src/Avalonia.Base/Input/IDataObject.cs

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input
{
@ -19,21 +21,12 @@ namespace Avalonia.Input
/// </summary>
bool Contains(string dataFormat);
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
string? GetText();
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
IEnumerable<string>? GetFileNames();
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
/// <returns>
/// Object data. If format isn't available, returns null.
/// </returns>
object? Get(string dataFormat);
}
}

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;
}

6
src/Avalonia.Base/Layout/Layoutable.cs

@ -798,6 +798,12 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
internal override void OnTemplatedParentControlThemeChanged()
{
base.OnTemplatedParentControlThemeChanged();
InvalidateMeasure();
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>

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);
}
}
}

406
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,30 +251,31 @@ 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;
@ -271,62 +283,56 @@ namespace Avalonia.Media
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 +340,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,29 +355,28 @@ 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.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity)
//TODO: Eliminate platform-specific push opacity call
public PushedState PushOpacity(double opacity, Rect bounds)
{
PlatformImpl.PushOpacity(opacity);
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.
@ -379,70 +388,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;

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

@ -67,17 +67,16 @@ namespace Avalonia.Media
}
}
public DrawingContext Open()
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public DrawingContext Open() => new DrawingGroupDrawingContext(this);
public override void Draw(DrawingContext context)
{
var bounds = GetBounds();
using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity))
using (context.PushOpacity(Opacity))
using (context.PushOpacity(Opacity, bounds))
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
{
foreach (var drawing in Children)
{
@ -103,7 +102,7 @@ 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>();
@ -133,17 +132,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))
{
@ -157,7 +146,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))
{
@ -167,7 +156,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)
{
@ -177,124 +166,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)
{
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.
@ -364,22 +299,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);
}

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

@ -877,7 +877,7 @@ namespace Avalonia.Media
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps));
TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection));
var collapsedLine = line.Collapse(collapsingProperties);

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

@ -12,6 +12,6 @@ namespace Avalonia.Media
/// <summary>
/// Gets the image to draw.
/// </summary>
IBitmap Source { get; }
IBitmap? Source { get; }
}
}

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; }
}
}

8
src/Avalonia.Base/Media/ImageBrush.cs

@ -11,8 +11,8 @@ namespace Avalonia.Media
/// <summary>
/// Defines the <see cref="Visual"/> property.
/// </summary>
public static readonly StyledProperty<IBitmap> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap>(nameof(Source));
public static readonly StyledProperty<IBitmap?> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap?>(nameof(Source));
static ImageBrush()
{
@ -30,7 +30,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="source">The image to draw.</param>
public ImageBrush(IBitmap source)
public ImageBrush(IBitmap? source)
{
Source = source;
}
@ -38,7 +38,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the image to draw.
/// </summary>
public IBitmap Source
public IBitmap? Source
{
get { return GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }

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);
}
}
}

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

@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <param name="bounds">The bounds.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public PushedState PushOpacity(double opacity)
public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
PlatformImpl.PushOpacity(opacity);
PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
@ -353,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();
}

6
src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs

@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable
/// <param name="tileMode">The tile mode.</param>
/// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
public ImmutableImageBrush(
IBitmap source,
IBitmap? source,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
double opacity = 1,
ImmutableTransform? transform = null,
RelativePoint transformOrigin = new RelativePoint(),
RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable
}
/// <inheritdoc/>
public IBitmap Source { get; }
public IBitmap? Source { get; }
}
}

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);
}
}

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

@ -6,11 +6,13 @@ namespace Avalonia.Media
{
public readonly double Width;
public readonly TextRunProperties TextRunProperties;
public readonly FlowDirection FlowDirection;
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties)
public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
TextRunProperties = textRunProperties;
FlowDirection = flowDirection;
}
}
}

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

@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting
return;
}
if (lineImpl.NewLineLength > 0)
{
return;
}
if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false })
{
return;
}
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting
continue;
}
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex);
var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster);
var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset);
var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,

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

@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting
internal SplitResult<ShapedTextRun> Split(int length)
{
if (IsReversed)
var isReversed = IsReversed;
if (isReversed)
{
Reverse();
}
length = Length - length;
}
#if DEBUG
if(length == 0)
if (length == 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
}
#endif
#endif
var splitBuffer = ShapedBuffer.Split(length);
var first = new ShapedTextRun(splitBuffer.First, Properties);
#if DEBUG
#if DEBUG
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
var second = new ShapedTextRun(splitBuffer.Second!, Properties);
if (isReversed)
{
return new SplitResult<ShapedTextRun>(second, first);
}
return new SplitResult<ShapedTextRun>(first, second);
}

9
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -1,4 +1,6 @@
namespace Avalonia.Media.TextFormatting
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing.
@ -15,6 +17,11 @@
/// </summary>
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the flow direction that is used for collapsing.
/// </summary>
public abstract FlowDirection FlowDirection { get; }
/// <summary>
/// Collapses given text line.
/// </summary>

191
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = properties.Width - shapedSymbol.Size.Width;
while (runIndex < textRuns.Count)
if(properties.FlowDirection== FlowDirection.LeftToRight)
{
var currentRun = textRuns[runIndex];
switch (currentRun)
while (runIndex < textRuns.Count)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
var currentRun = textRuns[runIndex];
if (currentWidth > availableWidth)
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
if (isWordEllipsis && measuredLength < textLine.Length)
{
var nextBreakPosition = lineBreak.PositionMeasure;
var currentBreakPosition = 0;
if (nextBreakPosition == 0)
{
break;
}
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
if (nextBreakPosition >= measuredLength)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
break;
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
collapsedLength += measuredLength;
availableWidth -= shapedRun.Size.Width;
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
availableWidth -= shapedRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol);
}
break;
}
availableWidth -= drawableRun.Size.Width;
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
break;
}
}
availableWidth -= drawableRun.Size.Width;
collapsedLength += currentRun.Length;
break;
}
runIndex++;
}
}
else
{
runIndex = textRuns.Count - 1;
while (runIndex >= 0)
{
var currentRun = textRuns[runIndex];
collapsedLength += currentRun.Length;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
currentWidth += shapedRun.Size.Width;
runIndex++;
}
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= shapedRun.Size.Width;
break;
}
case DrawableTextRun drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol);
}
availableWidth -= drawableRun.Size.Width;
break;
}
}
collapsedLength += currentRun.Length;
runIndex--;
}
}
return null;
}
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength,
FlowDirection flowDirection, TextRun shapedSymbol)
{
var textRuns = textLine.TextRuns;
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
if(flowDirection == FlowDirection.RightToLeft)
{
collapsedLength = textLine.Length - collapsedLength;
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
try
{
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
if (flowDirection == FlowDirection.RightToLeft)
{
var collapsedRuns = new TextRun[postSplitRuns!.Count + 1];
postSplitRuns.CopyTo(collapsedRuns, 1);
collapsedRuns[0] = shapedSymbol;
return collapsedRuns;
}
else
{
var collapsedRuns = new TextRun[preSplitRuns!.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
}
finally
{

25
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting
var lastTrailingIndex = 0;
if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting
{
lastTrailingIndex += textEndOfLine.Length;
}
}
}
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
var justificationWidth = MaxWidth;
for (var i = 0; i < textLines.Count; i++)
if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap)
{
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
justificationWidth = width;
}
var justificationWidth = width - whitespaceWidth;
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
for (var i = 0; i < textLines.Count - 1; i++)
for (var i = 0; i < textLines.Count; i++)
{
var line = textLines[i];
@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
private TextCollapsingProperties? GetCollapsingProperties(double width)
{
if(_textTrimming == TextTrimming.None)
if (_textTrimming == TextTrimming.None)
{
return null;
}
return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties));
return _textTrimming.CreateCollapsingProperties(
new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection));
}
public void Dispose()

7
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting
/// <param name="prefixLength">Length of leading prefix.</param>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param>
/// <param name="flowDirection">the flow direction of the collapes line.</param>
public TextLeadingPrefixCharacterEllipsis(
string ellipsis,
int prefixLength,
double width,
TextRunProperties textRunProperties)
TextRunProperties textRunProperties,
FlowDirection flowDirection)
{
if (_prefixLength < 0)
{
@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting
_prefixLength = prefixLength;
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

47
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
currentX += drawableTextRun.Size.Width;
}
if(lastRunIndex - 1 < 0)
if (lastRunIndex - 1 < 0)
{
break;
}
@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting
directionalWidth -= drawableTextRun.Size.Width;
}
if(firstRunIndex + 1 == _textRuns.Length)
if (firstRunIndex + 1 == _textRuns.Length)
{
break;
}
@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting
var runWidth = endX - startX;
return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
}
public override void Dispose()
@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting
}
}
if (index == lastRunIndex)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.Size.Width;
break;
@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting
{
widthIncludingWhitespace += drawableTextRun.Size.Width;
if (index == lastRunIndex)
{
width = widthIncludingWhitespace;
trailingWhitespaceLength = 0;
}
if (drawableTextRun.Size.Height > height)
{
height = drawableTextRun.Size.Height;
@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting
}
}
width = widthIncludingWhitespace;
for (var i = _textRuns.Length - 1; i >= 0; i--)
{
var currentRun = _textRuns[i];
if(currentRun is ShapedTextRun shapedText)
{
var glyphRun = shapedText.GlyphRun;
var glyphRunMetrics = glyphRun.Metrics;
newLineLength += glyphRunMetrics.NewLineLength;
if (glyphRunMetrics.TrailingWhitespaceLength == 0)
{
break;
}
trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength;
var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width;
width -= whitespaceWidth;
}
}
var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting
return Math.Max(0, start);
case TextAlignment.Right:
return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace);
return Math.Max(0, _paragraphWidth - width);
default:
return 0;

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -12,10 +12,13 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
/// <param name="flowDirection">The flow direction of the collapsed line.</param>
public TextTrailingCharacterEllipsis(string ellipsis, double width,
TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -24,6 +27,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -12,14 +12,17 @@
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
/// <param name="flowDirection">flow direction of the collapsed line.</param>
public TextTrailingWordEllipsis(
string ellipsis,
double width,
TextRunProperties textRunProperties
TextRunProperties textRunProperties,
FlowDirection flowDirection
)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
FlowDirection = flowDirection;
}
/// <inheritdoc/>
@ -28,6 +31,8 @@
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override FlowDirection FlowDirection { get; }
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{

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

@ -15,7 +15,7 @@ namespace Avalonia.Media
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)
{
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties);
return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

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

@ -17,10 +17,10 @@ namespace Avalonia.Media
{
if (_isWordBased)
{
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties);
return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection);
}
public override string ToString()

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);
}
}
}

2
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
void PushOpacity(double opacity);
void PushOpacity(double opacity, Rect bounds);
/// <summary>
/// Pops the latest pushed opacity value.

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

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Metadata;
@ -47,7 +48,7 @@ namespace Avalonia.Platform
/// <param name="pen">The stroke to use.</param>
/// <param name="point">The point.</param>
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
bool StrokeContains(IPen pen, Point point);
bool StrokeContains(IPen? pen, Point point);
/// <summary>
/// Makes a clone of the geometry with the specified transform.
@ -87,6 +88,7 @@ namespace Avalonia.Platform
/// <param name="startOnBeginFigure">If ture, the resulting snipped path will start with a BeginFigure call.</param>
/// <param name="segmentGeometry">The resulting snipped path.</param>
/// <returns>If the snipping operation is successful.</returns>
bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry);
bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
[NotNullWhen(true)] out IGeometryImpl? segmentGeometry);
}
}

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

5
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFile : IStorageBookmarkFile
{
public BclStorageFile(string fileName)
{
FileInfo = new FileInfo(fileName);
}
public BclStorageFile(FileInfo fileInfo)
{
FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));

9
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO;
internal class BclStorageFolder : IStorageBookmarkFolder
{
public BclStorageFolder(string path)
{
DirectoryInfo = new DirectoryInfo(path);
if (!DirectoryInfo.Exists)
{
throw new ArgumentException("Directory must exist");
}
}
public BclStorageFolder(DirectoryInfo directoryInfo)
{
DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));

17
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO;
internal static class StorageProviderHelpers
{
public static IStorageItem? TryCreateBclStorageItem(string path)
{
var directory = new DirectoryInfo(path);
if (directory.Exists)
{
return new BclStorageFolder(directory);
}
var file = new FileInfo(path);
if (file.Exists)
{
return new BclStorageFile(file);
}
return null;
}
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)

2
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -12,6 +12,8 @@ public class PickerOptions
/// <summary>
/// Gets or sets the initial location where the file open picker looks for files to present to the user.
/// Can be obtained from previously picked folder or using <see cref="IStorageProvider.TryGetFolderFromPathAsync"/>
/// or <see cref="IStorageProvider.TryGetWellKnownFolderAsync"/>.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
}

12
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -11,12 +11,24 @@ public static class StorageProviderExtensions
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile);
}
return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{
// We can avoid double escaping of the path by checking for BclStorageProvider.
if (provider is BclStorageProvider)
{
return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder);
}
return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
}

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

2
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;

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; }
}

187
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,32 +301,30 @@ 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)
protected override void PushOpacityCore(double opacity, Rect bounds)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(opacity))
if (next == null || !next.Item.Equals(opacity, bounds))
{
Add(new OpacityNode(opacity));
Add(new OpacityNode(opacity, bounds));
}
else
{
@ -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;
}
}

34
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;
@ -111,9 +102,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont
_impl.PopClip();
}
public void PushOpacity(double opacity)
public void PushOpacity(double opacity, Rect bounds)
{
_impl.PushOpacity(opacity);
_impl.PushOpacity(opacity, bounds);
}
public void PopOpacity()
@ -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();
}
}

18
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -41,27 +41,29 @@ namespace Avalonia.Rendering.Composition.Server
return;
Root!.RenderedVisuals++;
if (Opacity != 1)
canvas.PushOpacity(Opacity);
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (AdornedVisual != null)
{
canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity;
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
if (AdornerIsClipped)
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
}
var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform);
canvas.Transform = Matrix.Identity;
var boundsRect = new Rect(new Size(Size.X, Size.Y));
if (Opacity != 1)
canvas.PushOpacity(Opacity, boundsRect);
if (ClipToBounds && !HandlesClipToBounds)
canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
if (Clip != null)
canvas.PushGeometryClip(Clip);
if(OpacityMaskBrush != null)
canvas.PushOpacityMask(OpacityMaskBrush, boundsRect);
RenderCore(canvas, currentTransformedClip);
// Hack to force invalidation of SKMatrix
@ -74,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip();
if (AdornedVisual != null)
if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip();
if(Opacity != 1)
canvas.PopOpacity();

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);
}
}

39
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;
@ -117,7 +84,7 @@ namespace Avalonia.Rendering
}
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (context.PushOpacity(opacity, bounds))
using (clipToBounds
#pragma warning disable CS0618 // Type or member is obsolete
? visual is IVisualWithRoundRectClip roundClipVisual

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();
}
}
}

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

Loading…
Cancel
Save