diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index 741570061b..4a7a329fc6 100644
--- a/Avalonia.Desktop.slnf
+++ b/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"
]
}
-}
\ No newline at end of file
+}
diff --git a/Avalonia.sln b/Avalonia.sln
index 7d9c5243e1..e66b73de0e 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -233,6 +233,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
@@ -554,6 +561,10 @@ Global
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -631,6 +642,7 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
index 14e4f6a563..7d021d051f 100644
--- a/build/DevAnalyzers.props
+++ b/build/DevAnalyzers.props
@@ -5,5 +5,10 @@
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
+
diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm
index 23abf1d53f..b1fb915e04 100644
--- a/native/Avalonia.Native/src/OSX/AvnWindow.mm
+++ b/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(_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{
}
diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs
index 5fd331035a..f2dd217657 100644
--- a/nukebuild/BuildTasksPatcher.cs
+++ b/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
{
+ ///
+ /// This helper class, avoid argument null exception
+ /// when cecil write AssemblyNameDefinition on MemoryStream.
+ ///
+ 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()?.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(),
+ 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
}
}
}
-}
\ No newline at end of file
+}
diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
index e24860e3e1..e5f29abb68 100644
--- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs
+++ b/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.Shared.Rent(length);
- try
- {
- var charsRead = await reader.ReadAsync(buffer, 0, length);
- resultText += new string(buffer, 0, charsRead);
- }
- finally
- {
- ArrayPool.Shared.Return(buffer);
- }
+ resultText += await ReadTextFromFile(file, 10000);
}
openedFileContent.Text = resultText;
@@ -354,6 +337,28 @@ namespace ControlCatalog.Pages
}
}
+ public static async Task 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.Shared.Rent(length);
+ try
+ {
+ var charsRead = await reader.ReadAsync(buffer, 0, length);
+ return new string(buffer, 0, charsRead);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
index 3f8a023060..390fa32b9c 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml
@@ -25,7 +25,6 @@
BorderThickness="2">
Drag Me (custom)
-
+
+
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
index e384db88b3..26430b4b61 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
+++ b/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("DropState");
+ _dropState = this.Get("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 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();
+ 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());
+ {
+ var files = e.Data.GetFileNames();
+ _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty());
+ }
+#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;
diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml
index dc0eaf0a51..b1b1b99c9c 100644
--- a/samples/ControlCatalog/Pages/ListBoxPage.xaml
+++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml
@@ -10,7 +10,7 @@
diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props
index 3b14f0ce12..ac78d9c739 100644
--- a/samples/Directory.Build.props
+++ b/samples/Directory.Build.props
@@ -6,4 +6,5 @@
11
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index 353e01dca7..090cf23b33 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -25,6 +25,7 @@
WindowState:
+
@@ -56,6 +57,16 @@
+
+
+ Sample RadioButton
+
+ Three States: Option 1
+ Three States: Option 2
+
+
+
+
Unchecked
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 087f25666b..19eb1d64b0 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -1,19 +1,17 @@
-using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Interactivity;
-using Avalonia.Media;
using Avalonia.Markup.Xaml;
+using Avalonia.Media;
using Avalonia.VisualTree;
using Microsoft.CodeAnalysis;
-using Avalonia.Controls.Primitives;
-using Avalonia.Threading;
-using Avalonia.Controls.Primitives.PopupPositioning;
namespace IntegrationTestApp
{
@@ -25,6 +23,10 @@ namespace IntegrationTestApp
InitializeViewMenu();
InitializeGesturesTab();
this.AttachDevTools();
+
+ var overlayPopups = this.Get("AppOverlayPopups");
+ overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups";
+
AddHandler(Button.ClickEvent, OnButtonClick);
ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList();
DataContext = this;
diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs
index c09b249cfa..6603450b85 100644
--- a/samples/IntegrationTestApp/Program.cs
+++ b/samples/IntegrationTestApp/Program.cs
@@ -1,17 +1,31 @@
using System;
+using System.Linq;
using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
namespace IntegrationTestApp
{
class Program
{
+ public static bool OverlayPopups { get; private set; }
+
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
- public static void Main(string[] args) => BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
+ public static void Main(string[] args)
+ {
+ OverlayPopups = args.Contains("--overlayPopups");
+
+ BuildAvaloniaApp()
+ .With(new Win32PlatformOptions
+ {
+ OverlayPopups = OverlayPopups,
+ })
+ .With(new AvaloniaNativePlatformOptions
+ {
+ OverlayPopups = OverlayPopups,
+ })
+ .StartWithClassicDesktopLifetime(args);
+ }
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs
index bf27747154..4a3e20ff5b 100644
--- a/samples/RenderDemo/Pages/CustomSkiaPage.cs
+++ b/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();
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);
}
}
diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs
index cc5125609c..2fe57165b3 100644
--- a/samples/RenderDemo/Pages/PathMeasurementPage.cs
+++ b/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())
diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
index f365b59c20..b88dded39b 100644
--- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
+++ b/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));
}
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index d89d6f3690..f3a046ef80 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -784,6 +784,11 @@ namespace Avalonia
}
}
+ internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
+ {
+ UpdateDataValidation(property, state, error);
+ }
+
///
/// Gets a description of an observable that van be used in logs.
///
diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs
index 6231483ff8..9fbf680a5c 100644
--- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs
+++ b/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)
{
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index 45ab293a89..24244c5068 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -227,6 +227,7 @@ namespace Avalonia
/// The default binding mode for the property.
/// A value validation callback.
/// A value coercion callback.
+ /// Whether the property is interested in data validation.
/// A
public static StyledProperty Register(
string name,
@@ -234,7 +235,8 @@ namespace Avalonia
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func? validate = null,
- Func? coerce = null)
+ Func? coerce = null,
+ bool enableDataValidation = false)
where TOwner : AvaloniaObject
{
_ = name ?? throw new ArgumentNullException(nameof(name));
@@ -242,7 +244,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata(
defaultValue,
defaultBindingMode: defaultBindingMode,
- coerce: coerce);
+ coerce: coerce,
+ enableDataValidation: enableDataValidation);
var result = new StyledProperty(
name,
@@ -253,7 +256,7 @@ namespace Avalonia
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}
-
+
///
///
/// A method that gets called before and after the property starts being notified on an
@@ -267,6 +270,7 @@ namespace Avalonia
BindingMode defaultBindingMode,
Func? validate,
Func? coerce,
+ bool enableDataValidation,
Action? notifying)
where TOwner : AvaloniaObject
{
@@ -275,7 +279,8 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata(
defaultValue,
defaultBindingMode: defaultBindingMode,
- coerce: coerce);
+ coerce: coerce,
+ enableDataValidation: enableDataValidation);
var result = new StyledProperty(
name,
diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
index 2963567b14..62bb65351f 100644
--- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
+++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs
@@ -13,10 +13,13 @@ namespace Avalonia
/// Initializes a new instance of the class.
///
/// The default binding mode.
+ /// Whether the property is interested in data validation.
public AvaloniaPropertyMetadata(
- BindingMode defaultBindingMode = BindingMode.Default)
+ BindingMode defaultBindingMode = BindingMode.Default,
+ bool? enableDataValidation = null)
{
_defaultBindingMode = defaultBindingMode;
+ EnableDataValidation = enableDataValidation;
}
///
@@ -31,6 +34,17 @@ namespace Avalonia
}
}
+ ///
+ /// Gets a value indicating whether the property is interested in data validation.
+ ///
+ ///
+ /// 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.
+ ///
+ public bool? EnableDataValidation { get; private set; }
+
///
/// Merges the metadata with the base metadata.
///
@@ -44,6 +58,8 @@ namespace Avalonia
{
_defaultBindingMode = baseMetadata.DefaultBindingMode;
}
+
+ EnableDataValidation ??= baseMetadata.EnableDataValidation;
}
}
}
diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
index 4b0bab0c92..8aed1545a5 100644
--- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
+++ b/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;
}
diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs
index fe1cdd0e65..451ff6ce00 100644
--- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs
+++ b/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;
}
///
@@ -32,16 +31,6 @@ namespace Avalonia
///
public TValue UnsetValue { get; private set; }
- ///
- /// Gets a value indicating whether the property is interested in data validation.
- ///
- ///
- /// 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.
- ///
- public bool? EnableDataValidation { get; private set; }
///
object? IDirectPropertyMetadata.UnsetValue => UnsetValue;
@@ -51,19 +40,9 @@ namespace Avalonia
{
base.Merge(baseMetadata, property);
- var src = baseMetadata as DirectPropertyMetadata;
-
- if (src != null)
+ if (baseMetadata is DirectPropertyMetadata src)
{
- if (UnsetValue == null)
- {
- UnsetValue = src.UnsetValue;
- }
-
- if (EnableDataValidation == null)
- {
- EnableDataValidation = src.EnableDataValidation;
- }
+ UnsetValue ??= src.UnsetValue;
}
}
}
diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs
index 59c66ed505..13ca140565 100644
--- a/src/Avalonia.Base/Input/AccessKeyHandler.cs
+++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs
@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using Avalonia.Interactivity;
-using Avalonia.VisualTree;
+using Avalonia.LogicalTree;
namespace Avalonia.Input
{
@@ -190,7 +189,7 @@ namespace Avalonia.Input
// If the menu is open, only match controls in the menu's visual tree.
if (menuIsOpen)
{
- matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsVisualAncestorOf((Visual)x));
+ matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x));
}
var match = matches.FirstOrDefault();
diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs
index cf5a6592e1..35d50e669a 100644
--- a/src/Avalonia.Base/Input/DataFormats.cs
+++ b/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 @@
///
public static readonly string Text = nameof(Text);
+ ///
+ /// Dataformat for one or more files.
+ ///
+ public static readonly string Files = nameof(Files);
+
///
/// Dataformat for one or more filenames
///
+ [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")]
public static readonly string FileNames = nameof(FileNames);
}
}
diff --git a/src/Avalonia.Base/Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs
index 688f5f9cc8..93a6baa03c 100644
--- a/src/Avalonia.Base/Input/DataObject.cs
+++ b/src/Avalonia.Base/Input/DataObject.cs
@@ -2,37 +2,34 @@
namespace Avalonia.Input
{
+ ///
+ /// Specific and mutable implementation of the IDataObject interface.
+ ///
public class DataObject : IDataObject
{
- private readonly Dictionary _items = new Dictionary();
+ private readonly Dictionary _items = new();
+ ///
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
+ ///
public object? Get(string dataFormat)
{
- if (_items.ContainsKey(dataFormat))
- return _items[dataFormat];
- return null;
+ return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
+ ///
public IEnumerable GetDataFormats()
{
return _items.Keys;
}
- public IEnumerable? GetFileNames()
- {
- return Get(DataFormats.FileNames) as IEnumerable;
- }
-
- public string? GetText()
- {
- return Get(DataFormats.Text) as string;
- }
-
+ ///
+ /// Sets a value to the internal store of the data object with as a key.
+ ///
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;
diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs
new file mode 100644
index 0000000000..6af531b0d8
--- /dev/null
+++ b/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
+ {
+ ///
+ /// Returns a list of files if the DataObject contains files or filenames.
+ /// .
+ ///
+ ///
+ /// Collection of storage items - files or folders. If format isn't available, returns null.
+ ///
+ public static IEnumerable? GetFiles(this IDataObject dataObject)
+ {
+ return dataObject.Get(DataFormats.Files) as IEnumerable;
+ }
+
+ ///
+ /// Returns a list of filenames if the DataObject contains filenames.
+ ///
+ ///
+ ///
+ /// Collection of file names. If format isn't available, returns null.
+ ///
+ [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")]
+ public static IEnumerable? GetFileNames(this IDataObject dataObject)
+ {
+ return (dataObject.Get(DataFormats.FileNames) as IEnumerable)
+ ?? dataObject.GetFiles()?
+ .Select(f => f.TryGetLocalPath())
+ .Where(p => !string.IsNullOrEmpty(p))
+ .OfType();
+ }
+
+ ///
+ /// Returns the dragged text if the DataObject contains any text.
+ ///
+ ///
+ ///
+ /// A text string. If format isn't available, returns null.
+ ///
+ public static string? GetText(this IDataObject dataObject)
+ {
+ return dataObject.Get(DataFormats.Text) as string;
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs
index 1db008aa3a..6ccd0a8499 100644
--- a/src/Avalonia.Base/Input/IDataObject.cs
+++ b/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
///
bool Contains(string dataFormat);
- ///
- /// Returns the dragged text if the DataObject contains any text.
- ///
- ///
- string? GetText();
-
- ///
- /// Returns a list of filenames if the DataObject contains filenames.
- ///
- ///
- IEnumerable? GetFileNames();
-
///
/// Tries to get the data of the given DataFormat.
///
+ ///
+ /// Object data. If format isn't available, returns null.
+ ///
object? Get(string dataFormat);
}
}
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index e1c42c4ead..50980f1c3d 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -184,6 +184,7 @@ namespace Avalonia.Input
source?.RaiseEvent(e);
_pointer.Capture(null);
+ _lastMouseDownButton = default;
return e.Handled;
}
diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
index 98da83c1ce..285249a5f8 100644
--- a/src/Avalonia.Base/Input/PenDevice.cs
+++ b/src/Avalonia.Base/Input/PenDevice.cs
@@ -131,6 +131,7 @@ namespace Avalonia.Input
source?.RaiseEvent(e);
pointer.Capture(null);
+ _lastMouseDownButton = default;
return e.Handled;
}
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index 4a273b0291..ea84dc84bd 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/src/Avalonia.Base/Layout/Layoutable.cs
@@ -798,6 +798,12 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
+ internal override void OnTemplatedParentControlThemeChanged()
+ {
+ base.OnTemplatedParentControlThemeChanged();
+ InvalidateMeasure();
+ }
+
///
/// Called when the layout manager raises a LayoutUpdated event.
///
diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
index 497596fcc1..6b41c1c66c 100644
--- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
+++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs
@@ -1,28 +1,79 @@
-#nullable enable
-using System;
+using System;
+
+#nullable enable
namespace Avalonia.LogicalTree
{
+ ///
+ /// Describes the action that caused a event.
+ ///
+ public enum ChildIndexChangedAction
+ {
+ ///
+ /// The index of a single child changed.
+ ///
+ ChildIndexChanged,
+
+ ///
+ /// The index of multiple children changed and all children should be re-evaluated.
+ ///
+ ChildIndexesReset,
+
+ ///
+ /// The total number of children changed.
+ ///
+ TotalCountChanged,
+ }
+
///
/// Event args for event.
///
public class ChildIndexChangedEventArgs : EventArgs
{
- public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs();
-
- private ChildIndexChangedEventArgs()
+ ///
+ /// Initializes a new instance of the class with
+ /// an action of .
+ ///
+ /// The child whose index was changed.
+ /// The new index of the child.
+ 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;
}
///
- /// 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.
+ ///
+ public ChildIndexChangedAction Action { get; }
+
+ ///
+ /// Gets the logical child whose index was changed or null if all children should be re-evaluated.
///
public ILogical? Child { get; }
+
+ ///
+ /// Gets the new index of or -1 if all children should be re-evaluated.
+ ///
+ public int Index { get; }
+
+ ///
+ /// Gets an instance of the with an action of
+ /// .
+ ///
+ public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset);
+
+ ///
+ /// Gets an instance of the with an action of
+ /// .
+ ///
+ public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged);
}
}
diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
index 7fcd73273c..186c9527f2 100644
--- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
+++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs
@@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree
bool TryGetTotalCount(out int count);
///
- /// Notifies subscriber when child's index or total count was changed.
+ /// Notifies subscriber when a child's index was changed.
///
event EventHandler? ChildIndexChanged;
}
diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs
new file mode 100644
index 0000000000..2825628948
--- /dev/null
+++ b/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
+{
+ ///
+ /// Paints an area with an .
+ ///
+ public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty DrawingProperty =
+ AvaloniaProperty.Register(nameof(Drawing));
+
+ static DrawingBrush()
+ {
+ AffectsRender(DrawingProperty);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DrawingBrush()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The visual to draw.
+ public DrawingBrush(Drawing visual)
+ {
+ Drawing = visual;
+ }
+
+ ///
+ /// Gets or sets the visual to draw.
+ ///
+ 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);
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs
index 622181dba0..31a16dc69c 100644
--- a/src/Avalonia.Base/Media/DrawingContext.cs
+++ b/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> StateStackPool { get; } =
+ ThreadSafeObjectPool>.Default;
+ private Stack? _states;
- private static ThreadSafeObjectPool> StateStackPool { get; } =
- ThreadSafeObjectPool>.Default;
-
- private static ThreadSafeObjectPool> TransformStackPool { get; } =
- ThreadSafeObjectPool>.Default;
-
- private Stack? _states = StateStackPool.Get();
-
- private Stack? _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;
-
- ///
- /// Gets the current transform of the drawing context.
- ///
- 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();
+
///
/// Draws an image.
///
/// The image.
/// The rect in the output to draw to.
- 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);
}
+
///
/// Draws an image.
///
@@ -92,12 +54,22 @@ namespace Avalonia.Media
/// The rect in the image to draw.
/// The rect in the output to draw to.
/// The bitmap interpolation mode.
- 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);
}
+
+ ///
+ /// Draws a platform-specific bitmap impl.
+ ///
+ /// The bitmap image.
+ /// The opacity to draw with.
+ /// The rect in the image to draw.
+ /// The rect in the output to draw to.
+ /// The bitmap interpolation mode.
+ internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default);
///
/// 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);
+
///
/// Draws a geometry.
///
@@ -121,10 +93,10 @@ namespace Avalonia.Media
/// The geometry.
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);
}
-
+
///
/// Draws a geometry.
///
@@ -133,14 +105,12 @@ namespace Avalonia.Media
/// The geometry.
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);
+
///
/// Draws a rectangle with the specified Brush and Pen.
///
@@ -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.
///
- 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);
+ }
+
+ ///
+ /// Draws a rectangle with the specified Brush and Pen.
+ ///
+ /// The brush used to fill the rectangle, or null for no fill.
+ /// The pen used to stroke the rectangle, or null for no stroke.
+ /// The rectangle bounds.
+ /// Box shadow effect parameters
+ ///
+ /// 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.
+ ///
+ 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);
+
///
/// Draws the outline of a rectangle.
///
/// The pen.
/// The rectangle bounds.
/// The corner radius.
- 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);
- }
+
+ ///
+ /// Draws a filled rectangle.
+ ///
+ /// The brush.
+ /// The rectangle bounds.
+ /// The corner radius.
+ public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) =>
+ DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
///
/// Draws an ellipse with the specified Brush and Pen.
@@ -204,35 +200,50 @@ namespace Avalonia.Media
///
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));
+ }
+
+ ///
+ /// Draws an ellipse with the specified Brush and Pen.
+ ///
+ /// The brush used to fill the ellipse, or null for no fill.
+ /// The pen used to stroke the ellipse, or null for no stroke.
+ /// The bounding rect.
+ ///
+ /// 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.
+ ///
+ 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);
+
///
/// Draws a custom drawing operation
///
/// custom operation
- public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom);
+ public abstract void Custom(ICustomDrawOperation custom);
///
/// Draws text.
///
/// The upper-left corner of the text.
/// The text.
- 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);
}
///
@@ -240,30 +251,31 @@ namespace Avalonia.Media
///
/// The foreground brush.
/// The glyph run.
- 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;
}
- }
- ///
- /// Draws a filled rectangle.
- ///
- /// The brush.
- /// The rectangle bounds.
- /// The corner radius.
- 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();
}
}
-
+ ///
+ /// Pushes a clip rectangle.
+ ///
+ /// The clip rectangle.
+ /// A disposable used to undo the clip rectangle.
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);
+
///
/// Pushes a clip rectangle.
///
@@ -334,9 +340,13 @@ namespace Avalonia.Media
/// A disposable used to undo the clip rectangle.
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);
///
/// Pushes a clip geometry.
@@ -345,29 +355,28 @@ namespace Avalonia.Media
/// A disposable used to undo the clip geometry.
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);
///
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- 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);
///
/// Pushes an opacity mask.
@@ -379,70 +388,53 @@ namespace Avalonia.Media
/// A disposable to undo the opacity mask.
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);
- ///
- /// Pushes a matrix post-transformation.
- ///
- /// The matrix
- /// A disposable used to undo the transformation.
- public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix);
-
- ///
- /// Pushes a matrix pre-transformation.
- ///
- /// The matrix
- /// A disposable used to undo the transformation.
- public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform);
-
- ///
- /// Sets the current matrix transformation.
- ///
- /// The matrix
- /// A disposable used to undo the transformation.
- 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);
}
- ///
- /// Pushes a new transform context.
- ///
- /// A disposable used to undo the transformation.
- 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);
///
- /// Disposes of any resources held by the .
+ /// Pushes a matrix transformation.
///
- public void Dispose()
+ /// The matrix
+ /// A disposable used to undo the transformation.
+ 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;
diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs
index b7abda2c61..812d315912 100644
--- a/src/Avalonia.Base/Media/DrawingGroup.cs
+++ b/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();
@@ -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 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 source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
+ protected override void PushClipCore(RoundedRect rect)
{
throw new NotImplementedException();
}
- public void DrawBitmap(IRef 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 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();
}
-
+
///
/// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop.
///
- /// The Transform to push.
- private void PushTransform(Transform transform)
+ /// The transform to push.
+ 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();
+
///
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,
diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs
index 1b22a1ee69..52fbd87db7 100644
--- a/src/Avalonia.Base/Media/DrawingImage.cs
+++ b/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);
}
diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs
index 3b63a98720..d4640390d7 100644
--- a/src/Avalonia.Base/Media/FormattedText.cs
+++ b/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);
diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs
index 732f1957d0..07fd2d56fa 100644
--- a/src/Avalonia.Base/Media/IImageBrush.cs
+++ b/src/Avalonia.Base/Media/IImageBrush.cs
@@ -12,6 +12,6 @@ namespace Avalonia.Media
///
/// Gets the image to draw.
///
- IBitmap Source { get; }
+ IBitmap? Source { get; }
}
}
diff --git a/src/Avalonia.Base/Media/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs
new file mode 100644
index 0000000000..df72dd1ace
--- /dev/null
+++ b/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)
+ {
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs
deleted file mode 100644
index a7d3e4da10..0000000000
--- a/src/Avalonia.Base/Media/IVisualBrush.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Avalonia.Metadata;
-
-namespace Avalonia.Media
-{
- ///
- /// Paints an area with an .
- ///
- [NotClientImplementable]
- public interface IVisualBrush : ITileBrush
- {
- ///
- /// Gets the visual to draw.
- ///
- Visual? Visual { get; }
- }
-}
diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs
index 2f2a0fb627..718ebf1686 100644
--- a/src/Avalonia.Base/Media/ImageBrush.cs
+++ b/src/Avalonia.Base/Media/ImageBrush.cs
@@ -11,8 +11,8 @@ namespace Avalonia.Media
///
/// Defines the property.
///
- public static readonly StyledProperty SourceProperty =
- AvaloniaProperty.Register(nameof(Source));
+ public static readonly StyledProperty SourceProperty =
+ AvaloniaProperty.Register(nameof(Source));
static ImageBrush()
{
@@ -30,7 +30,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the class.
///
/// The image to draw.
- public ImageBrush(IBitmap source)
+ public ImageBrush(IBitmap? source)
{
Source = source;
}
@@ -38,7 +38,7 @@ namespace Avalonia.Media
///
/// Gets or sets the image to draw.
///
- public IBitmap Source
+ public IBitmap? Source
{
get { return GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
index 6577532891..c4720d772e 100644
--- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs
+++ b/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,
diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
index 88e5e627ee..e77dd9d1ab 100644
--- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
@@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging
///
/// A bitmap that holds the rendering of a .
///
- public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget
+ public class RenderTargetBitmap : Bitmap, IDisposable
{
///
/// Initializes a new instance of the class.
@@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging
/// Renders a visual to the .
///
/// The visual to render.
- public void Render(Visual visual) => ImmediateRenderer.Render(visual, this);
+ public void Render(Visual visual)
+ {
+ using (var ctx = CreateDrawingContext())
+ ImmediateRenderer.Render(visual, ctx);
+ }
///
/// Creates a platform-specific implementation for a .
@@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging
return factory.CreateRenderTargetBitmap(size, dpi);
}
- ///
- 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);
+ }
}
}
diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
index 7d9534c414..58b153482d 100644
--- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
+++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs
@@ -281,11 +281,12 @@ namespace Avalonia.Media
/// Pushes an opacity value.
///
/// The opacity.
+ /// The bounds.
/// A disposable used to undo the opacity.
- public PushedState PushOpacity(double opacity)
+ public PushedState PushOpacity(double opacity, Rect bounds)
//TODO: Eliminate platform-specific push opacity call
{
- PlatformImpl.PushOpacity(opacity);
+ PlatformImpl.PushOpacity(opacity, bounds);
return new PushedState(this, PushedState.PushedStateType.Opacity);
}
@@ -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();
}
diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
index f9892bf60c..668a907fdf 100644
--- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
+++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
@@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable
/// The tile mode.
/// The bitmap interpolation mode.
public ImmutableImageBrush(
- IBitmap source,
+ IBitmap? source,
AlignmentX alignmentX = AlignmentX.Center,
AlignmentY alignmentY = AlignmentY.Center,
RelativeRect? destinationRect = null,
double opacity = 1,
ImmutableTransform? transform = null,
- RelativePoint transformOrigin = new RelativePoint(),
+ RelativePoint transformOrigin = default,
RelativeRect? sourceRect = null,
Stretch stretch = Stretch.Uniform,
TileMode tileMode = TileMode.None,
@@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable
}
///
- public IBitmap Source { get; }
+ public IBitmap? Source { get; }
}
}
diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
deleted file mode 100644
index 0b625080e3..0000000000
--- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Avalonia.Media.Imaging;
-
-namespace Avalonia.Media.Immutable
-{
- ///
- /// Paints an area with an .
- ///
- internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The visual to draw.
- /// The horizontal alignment of a tile in the destination.
- /// The vertical alignment of a tile in the destination.
- /// The rectangle on the destination in which to paint a tile.
- /// The opacity of the brush.
- /// The transform of the brush.
- /// The transform origin of the brush
- /// The rectangle of the source image that will be displayed.
- ///
- /// How the source rectangle will be stretched to fill the destination rect.
- ///
- /// The tile mode.
- /// Controls the quality of interpolation.
- 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;
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The brush from which this brush's properties should be copied.
- public ImmutableVisualBrush(IVisualBrush source)
- : base(source)
- {
- Visual = source.Visual;
- }
-
- ///
- public Visual? Visual { get; }
- }
-}
diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs
new file mode 100644
index 0000000000..eb8a93722c
--- /dev/null
+++ b/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> TransformStackPool { get; } =
+ ThreadSafeObjectPool>.Default;
+
+ private Stack? _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 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);
+ }
+}
diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
index 40ba613717..9b7bf3f74c 100644
--- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
+++ b/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;
}
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
index 0d85f3e7c5..c1b9b77401 100644
--- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
+++ b/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();
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,
diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
index 7f23ac98b4..568148e15c 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
@@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting
internal SplitResult 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(second, first);
+ }
+
return new SplitResult(first, second);
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
index 72882df0b5..7cdf81ecc9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
@@ -1,4 +1,6 @@
-namespace Avalonia.Media.TextFormatting
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
{
///
/// Properties of text collapsing.
@@ -15,6 +17,11 @@
///
public abstract TextRun Symbol { get; }
+ ///
+ /// Gets the flow direction that is used for collapsing.
+ ///
+ public abstract FlowDirection FlowDirection { get; }
+
///
/// Collapses given text line.
///
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
index 4c93a1d851..6422f23dcd 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/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 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
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index 4dbc472133..a382416b8a 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/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
/// The .
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()
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
index 2e85b1e187..a21a5d45e9 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting
/// Length of leading prefix.
/// width in which collapsing is constrained to
/// text run properties of ellipsis symbol
+ /// the flow direction of the collapes line.
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;
}
///
@@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting
///
public override TextRun Symbol { get; }
+ public override FlowDirection FlowDirection { get; }
+
///
public override TextRun[]? Collapse(TextLine textLine)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 187b3154ad..b3321d4d9f 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/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;
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index ccae99cc75..8a6607bce2 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -12,10 +12,13 @@
/// Text used as collapsing symbol.
/// Width in which collapsing is constrained to.
/// Text run properties of ellipsis symbol.
- public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
+ /// The flow direction of the collapsed line.
+ public TextTrailingCharacterEllipsis(string ellipsis, double width,
+ TextRunProperties textRunProperties, FlowDirection flowDirection)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
+ FlowDirection = flowDirection;
}
///
@@ -24,6 +27,8 @@
///
public override TextRun Symbol { get; }
+ public override FlowDirection FlowDirection { get; }
+
///
public override TextRun[]? Collapse(TextLine textLine)
{
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
index c622c76a60..5252766382 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -12,14 +12,17 @@
/// Text used as collapsing symbol.
/// width in which collapsing is constrained to.
/// text run properties of ellipsis symbol.
+ /// flow direction of the collapsed line.
public TextTrailingWordEllipsis(
string ellipsis,
double width,
- TextRunProperties textRunProperties
+ TextRunProperties textRunProperties,
+ FlowDirection flowDirection
)
{
Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties);
+ FlowDirection = flowDirection;
}
///
@@ -28,6 +31,8 @@
///
public override TextRun Symbol { get; }
+ public override FlowDirection FlowDirection { get; }
+
///
public override TextRun[]? Collapse(TextLine textLine)
{
diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
index 7ba25eb005..19e6a70357 100644
--- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
+++ b/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()
diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs
index 2edbaabbc6..8a3c5aa397 100644
--- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs
+++ b/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()
diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs
index 2be3e9a94e..6bfe20271f 100644
--- a/src/Avalonia.Base/Media/VisualBrush.cs
+++ b/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
{
///
/// Paints an area with an .
///
- public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush
+ public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender
{
///
/// Defines the property.
@@ -43,10 +46,23 @@ namespace Avalonia.Media
set { SetValue(VisualProperty, value); }
}
- ///
- 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);
}
}
}
diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
index 8509067cd0..8962bc1586 100644
--- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
+++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs
@@ -128,7 +128,7 @@ namespace Avalonia.Platform
/// Pushes an opacity value.
///
/// The opacity.
- void PushOpacity(double opacity);
+ void PushOpacity(double opacity, Rect bounds);
///
/// Pops the latest pushed opacity value.
diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs
index 5826cfb2ff..d1964bf07e 100644
--- a/src/Avalonia.Base/Platform/IGeometryImpl.cs
+++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Metadata;
@@ -47,7 +48,7 @@ namespace Avalonia.Platform
/// The stroke to use.
/// The point.
/// true if the geometry contains the point; otherwise, false.
- bool StrokeContains(IPen pen, Point point);
+ bool StrokeContains(IPen? pen, Point point);
///
/// Makes a clone of the geometry with the specified transform.
@@ -87,6 +88,7 @@ namespace Avalonia.Platform
/// If ture, the resulting snipped path will start with a BeginFigure call.
/// The resulting snipped path.
/// If the snipping operation is successful.
- bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry);
+ bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
+ [NotNullWhen(true)] out IGeometryImpl? segmentGeometry);
}
}
diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs
index 73e9e58da4..31ad84341d 100644
--- a/src/Avalonia.Base/Platform/IRenderTarget.cs
+++ b/src/Avalonia.Base/Platform/IRenderTarget.cs
@@ -14,11 +14,7 @@ namespace Avalonia.Platform
///
/// Creates an for a rendering session.
///
- ///
- /// A render to be used to render visual brushes. May be null if no visual brushes are
- /// to be drawn.
- ///
- IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
+ IDrawingContextImpl CreateDrawingContext();
///
/// Indicates if the render target is no longer usable and needs to be recreated
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
index 5bf9ff9d9a..543fb0ab74 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
+++ b/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));
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
index 1e21c197bb..d8e3d91f75 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
+++ b/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));
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
index 55e84ee937..a8cbffb417 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
+++ b/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)
diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs
index 6f97916a26..ed061aa2d5 100644
--- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs
+++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs
@@ -12,6 +12,8 @@ public class PickerOptions
///
/// 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
+ /// or .
///
public IStorageFolder? SuggestedStartLocation { get; set; }
}
diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
index 6f8b945cd6..1febb4506a 100644
--- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
+++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
@@ -11,12 +11,24 @@ public static class StorageProviderExtensions
///
public static Task 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));
}
///
public static Task 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));
}
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
index e1ff0970c2..a841803ee1 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs
+++ b/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> source)
+ : this(target, frame, property, (object)source)
{
- Frame = frame;
- Source = source;
- Property = property;
}
protected BindingEntryBase(
+ AvaloniaObject target,
ValueFrame frame,
AvaloniaProperty property,
IObservable 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 instance, BindingValue 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.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.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;
}
}
}
diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
index cbe2435953..4bf98e3f7b 100644
--- a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs
+++ b/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 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)
diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
index 5d60b44bef..1cf108df9b 100644
--- a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs
+++ b/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 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);
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
index 78f0ad46b7..11a4dd7893 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
+++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs
@@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore
///
internal abstract class EffectiveValue
{
- private IValueEntry? _valueEntry;
- private IValueEntry? _baseValueEntry;
-
///
/// Gets the current effective value as a boxed value.
///
@@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore
///
public BindingPriority BasePriority { get; protected set; }
+ ///
+ /// Gets the active value entry for the current effective value.
+ ///
+ public IValueEntry? ValueEntry { get; private set; }
+
+ ///
+ /// Gets the active value entry for the current base value.
+ ///
+ public IValueEntry? BaseValueEntry { get; private set; }
+
///
/// Gets a value indicating whether the was overridden by a call to
/// .
@@ -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();
- }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
index c469034f9b..0788b39459 100644
--- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
+++ b/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)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)property);
- }
+ ValueEntry?.Unsubscribe();
+ BaseValueEntry?.Unsubscribe();
- public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property)
- {
+ var p = (StyledProperty)property;
BindingPriority priority;
T oldValue;
@@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore
if (!EqualityComparer.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);
}
}
diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs
index 271d85f8bc..5898bef491 100644
--- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs
+++ b/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
///
object? GetValue();
+ ///
+ /// Gets the data validation state if supported.
+ ///
+ /// The binding validation state.
+ /// The current binding error, if any.
+ ///
+ /// True if the entry supports data validation, otherwise false.
+ ///
+ bool GetDataValidationState(out BindingValueType state, out Exception? error);
+
///
/// Called when the value entry is removed from the value store.
///
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
index d8a353dc70..16b96eff5d 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs
+++ b/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.GetValue() => _value;
+
+ bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ state = BindingValueType.Value;
+ error = null;
+ return false;
+ }
}
}
diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
index 7e9f3ab312..222d857aa3 100644
--- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
+++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs
@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable> source)
{
- var e = new TypedBindingEntry(this, property, source);
+ var e = new TypedBindingEntry(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable source)
{
- var e = new TypedBindingEntry(this, property, source);
+ var e = new TypedBindingEntry(Owner!.Owner, this, property, source);
Add(e);
return e;
}
@@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore
StyledProperty property,
IObservable
internal class CompositionDrawList : PooledList>
{
- public Size? Size { get; set; }
-
public CompositionDrawList()
{
@@ -34,21 +33,47 @@ internal class CompositionDrawList : PooledList>
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;
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
new file mode 100644
index 0000000000..85bb156475
--- /dev/null
+++ b/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; }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
index b75d080cfd..f81cc5a1a0 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
+++ b/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 License.md
@@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition;
///
/// An IDrawingContextImpl implementation that builds
///
-internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
+internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex;
+
+ private static ThreadSafeObjectPool> TransformStackPool { get; } =
+ ThreadSafeObjectPool>.Default;
- ///
- public Matrix Transform { get; set; } = Matrix.Identity;
+ private Stack? _transforms;
- ///
- public void Clear(Color color)
- {
- // Cannot clear a deferred scene.
- }
+ private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } =
+ ThreadSafeObjectPool>.Default;
- ///
- public void Dispose()
- {
- // Nothing to do here since we allocate no unmanaged resources.
- }
+ private Stack? _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);
+ }
- ///
- 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();
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
}
}
- ///
- public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect,
- BitmapInterpolationMode bitmapInterpolationMode)
+ internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect,
+ BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
var next = NextDrawAs();
@@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
///
- public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
- {
- // This method is currently only used to composite layers so shouldn't be called here.
- throw new NotSupportedException();
- }
-
- ///
- public void DrawLine(IPen? pen, Point p1, Point p2)
+ 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
}
///
- 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();
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();
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();
if (next == null || !next.Item.Equals(Transform, custom))
@@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
++_drawOperationIndex;
}
- public object? GetFeature(Type t) => null;
-
- ///
- public void DrawGlyphRun(IBrush? foreground, IRef 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();
- 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();
- ///
- public void PopClip()
+ protected override void PopClipCore()
{
var next = NextDrawAs();
@@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
///
- public void PopGeometryClip()
+ protected override void PopGeometryClipCore()
{
var next = NextDrawAs();
@@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PopBitmapBlendMode()
+ protected override void PopBitmapBlendModeCore()
{
var next = NextDrawAs();
@@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PopOpacity()
+ protected override void PopOpacityCore()
{
var next = NextDrawAs();
@@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PopOpacityMask()
+ protected override void PopOpacityMaskCore()
{
+ if (!_needsToPopOpacityMask!.Pop())
+ return;
+
var next = NextDrawAs();
if (next == null || !next.Item.Equals(null, null))
{
- Add(new OpacityMaskNode());
+ Add(new OpacityMaskPopNode());
}
else
{
@@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PushClip(Rect clip)
+
+ protected override void PushClipCore(Rect clip)
{
var next = NextDrawAs();
@@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PushClip(RoundedRect clip)
+ protected override void PushClipCore(RoundedRect clip)
{
var next = NextDrawAs();
@@ -294,32 +301,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PushGeometryClip(IGeometryImpl? clip)
+ protected override void PushGeometryClipCore(Geometry clip)
{
- if (clip is null)
+ if (clip.PlatformImpl is null)
return;
var next = NextDrawAs();
- 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;
}
}
-
- ///
- public void PushOpacity(double opacity)
+
+ protected override void PushOpacityCore(double opacity, Rect bounds)
{
var next = NextDrawAs();
- if (next == null || !next.Item.Equals(opacity))
+ if (next == null || !next.Item.Equals(opacity, bounds))
{
- Add(new OpacityNode(opacity));
+ Add(new OpacityNode(opacity, bounds));
}
else
{
@@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW
}
}
- ///
- public void PushOpacityMask(IBrush mask, Rect bounds)
+ protected override void PushOpacityMaskCore(IBrush mask, Rect bounds)
{
var next = NextDrawAs();
+ 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);
}
///
- public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
+ protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode)
{
var next = NextDrawAs();
@@ -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;
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
index 50df8bd32b..eaa9a70ca0 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
+++ b/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)
{
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
index 63ec8d756b..977acd8470 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
+++ b/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();
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 98be861afa..f9492d0015 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
@@ -41,9 +41,9 @@ 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;
@@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server
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
diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
deleted file mode 100644
index f5312ad39b..0000000000
--- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Avalonia.Media;
-using Avalonia.Metadata;
-using Avalonia.Platform;
-
-namespace Avalonia.Rendering
-{
- ///
- /// Defines a renderer used to render a visual brush to a bitmap.
- ///
- [Unstable]
- public interface IVisualBrushRenderer
- {
- ///
- /// Gets the size of the intermediate render target to which the visual brush should be
- /// drawn.
- ///
- /// The visual brush.
- /// The size of the intermediate render target to create.
- Size GetRenderTargetSize(IVisualBrush brush);
-
- ///
- /// Renders a visual brush to a bitmap.
- ///
- /// The drawing context to render to.
- /// The visual brush.
- /// A bitmap containing the rendered brush.
- void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush);
- }
-}
diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
index 8e5dc38317..4a12e78817 100644
--- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs
+++ b/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
///
- internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer
+ internal class ImmediateRenderer
{
- ///
- /// Renders a visual to a render target.
- ///
- /// The visual.
- /// The render target.
- public static void Render(Visual visual, IRenderTarget target)
- {
- using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
- Render(context, visual, visual.Bounds);
- }
-
///
/// Renders a visual to a drawing context.
///
@@ -36,28 +25,6 @@ namespace Avalonia.Rendering
{
Render(context, visual, visual.Bounds);
}
-
-
- ///
- Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
- {
- (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized();
- return brush.Visual?.Bounds.Size ?? default;
- }
-
- ///
- 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
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
index e81966ce81..62fc73db44 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
@@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Base class for draw operations that can use a brush.
///
- 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;
}
- ///
- /// Auxiliary data required to draw the brush
- ///
- public IDisposable? Aux { get; }
-
public override void Dispose()
{
- Aux?.Dispose();
+ (Brush as ISceneBrushContent)?.Dispose();
base.Dispose();
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
index e1bfaa4aa3..782e287989 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
@@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// A node in the scene graph which represents a clip push or pop.
///
- internal class ClipNode : IDrawOperation
+ internal class ClipNode : IDrawOperationWithTransform
{
///
/// Initializes a new instance of the class that represents a
@@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph
///
public void Render(IDrawingContextImpl context)
{
- context.Transform = Transform;
-
if (Clip.HasValue)
{
context.PushClip(Clip.Value);
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
index b7311936d3..ff2616bfe4 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
+++ b/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
+ public interface ICustomDrawOperation : IEquatable, IDisposable
{
-
+ ///
+ /// Gets the bounds of the visible content in the node in global coordinates.
+ ///
+ Rect Bounds { get; }
+
+ ///
+ /// Hit test the geometry in this node.
+ ///
+ /// The point in global coordinates.
+ /// True if the point hits the node's geometry; otherwise false.
+ ///
+ /// This method does not recurse to childs, if you want
+ /// to hit test children they must be hit tested manually.
+ ///
+ bool HitTest(Point p);
+
+ ///
+ /// Renders the node to a drawing context.
+ ///
+ /// The drawing context.
+ void Render(IDrawingContextImpl context);
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
index c49e7705e0..5b93cd8cfc 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
+++ b/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; }
+ }
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
index 4600653b9d..d5f0270cb2 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs
+++ b/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;
}
- ///
- /// Gets the fill brush.
- ///
- public IBrush? Brush { get; }
-
///
/// Gets the stroke pen.
///
public ImmutablePen? Pen { get; }
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
///
/// Gets the rect of the ellipse to draw.
///
@@ -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();
+ }
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
index 82f8fc2d56..e1f79e0e10 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// A node in the scene graph which represents a rectangle draw.
///
- internal class ExperimentalAcrylicNode : DrawOperation
+ internal class ExperimentalAcrylicNode : DrawOperationWithTransform
{
///
/// Initializes a new instance of the class.
@@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph
RoundedRect rect)
: base(rect.Rect, transform)
{
- Transform = transform;
Material = material.ToImmutable();
Rect = rect;
}
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
public IExperimentalAcrylicMaterial Material { get; }
///
@@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph
///
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
}
///
- 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);
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
index 842edf2bcb..8575e61de4 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs
@@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// A node in the scene graph which represents a geometry clip push or pop.
///
- internal class GeometryClipNode : IDrawOperation
+ internal class GeometryClipNode : IDrawOperationWithTransform
{
///
/// Initializes a new instance of the class that represents a
@@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph
///
public void Render(IDrawingContextImpl context)
{
- context.Transform = Transform;
-
if (Clip != null)
{
context.PushGeometryClip(Clip);
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
index cf53b86fa7..3ab535897a 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs
@@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph
/// The geometry.
/// Auxiliary data required to draw the brush.
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;
}
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
- ///
- /// Gets the fill brush.
- ///
- public IBrush? Brush { get; }
-
///
/// Gets the stroke pen.
///
@@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph
///
public override void Render(IDrawingContextImpl context)
{
- context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry);
}
///
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));
}
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
index a2d914bdd7..4d8759f545 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs
@@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph
/// Auxiliary data required to draw the brush.
public GlyphRunNode(
Matrix transform,
- IBrush foreground,
- IRef glyphRun,
- IDisposable? aux = null)
- : base(new Rect(glyphRun.Item.Size), transform, aux)
+ IImmutableBrush foreground,
+ IRef glyphRun)
+ : base(new Rect(glyphRun.Item.Size), transform, foreground)
{
- Transform = transform;
- Foreground = foreground.ToImmutable();
GlyphRun = glyphRun.Clone();
}
-
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
- ///
- /// Gets the foreground brush.
- ///
- public IBrush Foreground { get; }
-
+
+
///
/// Gets the glyph run to draw.
///
public IRef GlyphRun { get; }
///
- public override void Render(IDrawingContextImpl context)
- {
- context.Transform = Transform;
- context.DrawGlyphRun(Foreground, GlyphRun);
- }
+ public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun);
///
/// Determines if this draw operation equals another.
@@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph
internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun)
{
return transform == Transform &&
- Equals(foreground, Foreground) &&
+ Equals(foreground, Brush) &&
Equals(glyphRun.Item, GlyphRun.Item);
}
///
- 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();
}
}
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
index 2bfd2080c3..6a1aefe6b2 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// Represents a node in the low-level scene graph that represents geometry.
///
- public interface IDrawOperation : IDisposable
+ internal interface IDrawOperation : IDisposable
{
///
/// Gets the bounds of the visible content in the node in global coordinates.
@@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph
/// The drawing context.
void Render(IDrawingContextImpl context);
}
+
+ internal interface IDrawOperationWithTransform : IDrawOperation
+ {
+ ///
+ /// Gets the transform with which the node will be drawn.
+ ///
+ Matrix Transform { get; }
+ }
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
index 339881e675..dd9787e8d1 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs
@@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// A node in the scene graph which represents an image draw.
///
- internal class ImageNode : DrawOperation
+ internal class ImageNode : DrawOperationWithTransform
{
///
/// Initializes a new instance of the class.
@@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph
public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
: base(destRect, transform)
{
- Transform = transform;
Source = source.Clone();
Opacity = opacity;
SourceRect = sourceRect;
DestRect = destRect;
BitmapInterpolationMode = bitmapInterpolationMode;
SourceVersion = Source.Item.Version;
- }
-
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
+ }
///
/// Gets the image to draw.
@@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph
///
public BitmapInterpolationMode BitmapInterpolationMode { get; }
- ///
- /// The bitmap blending mode.
- ///
- ///
- /// The blending mode.
- ///
- public BitmapBlendingMode BitmapBlendingMode { get; }
-
///
/// Determines if this draw operation equals another.
///
@@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph
///
public override void Render(IDrawingContextImpl context)
{
- context.Transform = Transform;
context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode);
}
///
- public override bool HitTest(Point p) => Bounds.ContainsExclusive(p);
+ public override bool HitTest(Point p) => DestRect.ContainsExclusive(p);
public override void Dispose()
{
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
index 0af8ba2752..f21791d038 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph
///
/// A node in the scene graph which represents a line draw.
///
- internal class LineNode : BrushDrawOperation
+ internal class LineNode : DrawOperationWithTransform
{
///
/// Initializes a new instance of the class.
@@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph
Matrix transform,
IPen pen,
Point p1,
- Point p2,
- IDisposable? aux = null)
- : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux)
+ Point p2)
+ : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
{
- Transform = transform;
Pen = pen.ToImmutable();
P1 = p1;
P2 = p2;
}
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
///
/// Gets the stroke pen.
///
@@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context)
{
- context.Transform = Transform;
context.DrawLine(Pen, P1, P2);
}
public override bool HitTest(Point p)
{
- if (!Transform.HasInverse)
- return false;
-
- p *= Transform.Invert();
-
var halfThickness = Pen.Thickness / 2;
var minX = Math.Min(P1.X, P2.X) - halfThickness;
var maxX = Math.Max(P1.X, P2.X) + halfThickness;
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
index 3ecc07fa54..e10d712c2d 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs
@@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph
/// The opacity mask to push.
/// The bounds of the mask.
/// Auxiliary data required to draw the brush.
- public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null)
- : base(default, Matrix.Identity, aux)
+ public OpacityMaskNode(IImmutableBrush mask, Rect bounds)
+ : base(default, Matrix.Identity, mask)
{
- Mask = mask.ToImmutable();
MaskBounds = bounds;
}
- ///
- /// Initializes a new instance of the class that represents an
- /// opacity mask pop.
- ///
- public OpacityMaskNode()
- : base(default, Matrix.Identity, null)
- {
- }
-
- ///
- /// Gets the mask to be pushed or null if the operation represents a pop.
- ///
- public IBrush? Mask { get; }
-
///
/// Gets the bounds of the opacity mask or null if the operation represents a pop.
///
@@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds;
+ public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds;
///
public override void Render(IDrawingContextImpl context)
{
- if (Mask != null)
- {
- context.PushOpacityMask(Mask, MaskBounds!.Value);
- }
- else
- {
- context.PopOpacityMask();
- }
+ context.PushOpacityMask(Brush!, MaskBounds!.Value);
}
}
+
+ internal class OpacityMaskPopNode : DrawOperation
+ {
+ public OpacityMaskPopNode() : base(default, Matrix.Identity)
+ {
+ }
+
+ public override bool HitTest(Point p) => false;
+
+ public override void Render(IDrawingContextImpl context) => context.PopOpacityMask();
+ }
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
index e41e639067..f76a055934 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs
@@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph
/// opacity push.
///
/// The opacity to push.
- public OpacityNode(double opacity)
+ /// The bounds.
+ public OpacityNode(double opacity, Rect bounds)
{
Opacity = opacity;
+ Bounds = bounds;
}
///
@@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public Rect Bounds => default;
+ public Rect Bounds { get; }
///
/// Gets the opacity to be pushed or null if the operation represents a pop.
@@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph
/// Determines if this draw operation equals another.
///
/// The opacity of the other draw operation.
+ /// The bounds of the other draw operation.
/// True if the draw operations are the same, otherwise false.
///
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
///
- public bool Equals(double? opacity) => Opacity == opacity;
+ public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds;
///
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
- context.PushOpacity(Opacity.Value);
+ context.PushOpacity(Opacity.Value, Bounds);
}
else
{
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
index f2ffd7411c..cee9ce9df7 100644
--- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
+++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs
@@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph
/// Auxiliary data required to draw the brush.
public RectangleNode(
Matrix transform,
- IBrush? brush,
+ IImmutableBrush? brush,
IPen? pen,
RoundedRect rect,
- BoxShadows boxShadows,
- IDisposable? aux = null)
- : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux)
+ BoxShadows boxShadows)
+ : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush)
{
- Transform = transform;
- Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
BoxShadows = boxShadows;
}
- ///
- /// Gets the transform with which the node will be drawn.
- ///
- public Matrix Transform { get; }
-
- ///
- /// Gets the fill brush.
- ///
- public IBrush? Brush { get; }
-
///
/// Gets the stroke pen.
///
@@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph
}
///
- public override void Render(IDrawingContextImpl context)
- {
- context.Transform = Transform;
-
- context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
- }
+ public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
///
public override bool HitTest(Point p)
{
- // TODO: This doesn't respect CornerRadius yet.
- if (Transform.HasInverse)
+ if (Brush != null)
{
- p *= Transform.Invert();
-
- if (Brush != null)
- {
- var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
- return rect.ContainsExclusive(p);
- }
- else
- {
- var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
- var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
- return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
- }
+ var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+ return rect.ContainsExclusive(p);
+ }
+ else
+ {
+ var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
+ var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
+ return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p);
}
-
- return false;
}
}
}
diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index 5b8dac2f53..82e948eea8 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -28,7 +28,7 @@ namespace Avalonia
public class StyledElement : Animatable,
IDataContextProvider,
ILogical,
- IResourceHost,
+ IThemeVariantHost,
IStyleHost,
IStyleable,
ISetLogicalParent,
@@ -46,6 +46,7 @@ namespace Avalonia
defaultBindingMode: BindingMode.OneWay,
validate: null,
coerce: null,
+ enableDataValidation: false,
notifying: DataContextNotifying);
///
@@ -75,23 +76,6 @@ namespace Avalonia
public static readonly StyledProperty ThemeProperty =
AvaloniaProperty.Register(nameof(Theme));
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty ActualThemeVariantProperty =
- AvaloniaProperty.Register(
- nameof(ThemeVariant),
- inherits: true,
- defaultValue: ThemeVariant.Light);
-
- ///
- /// Defines the RequestedThemeVariant property.
- ///
- public static readonly StyledProperty RequestedThemeVariantProperty =
- AvaloniaProperty.Register(
- nameof(ThemeVariant),
- defaultValue: ThemeVariant.Default);
-
private static readonly ControlTheme s_invalidTheme = new ControlTheme();
private int _initCount;
private string? _name;
@@ -160,6 +144,9 @@ namespace Avalonia
///
public event EventHandler? ResourcesChanged;
+ ///
+ public event EventHandler? ActualThemeVariantChanged;
+
///
/// Gets or sets the name of the styled element.
///
@@ -278,15 +265,6 @@ namespace Avalonia
set => SetValue(ThemeProperty, value);
}
- ///
- /// Gets the UI theme that is currently used by the element, which might be different than the .
- ///
- ///
- /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned.
- /// Otherwise, current OS theme variant is returned.
- ///
- public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
-
///
/// Gets the styled element's logical children.
///
@@ -325,6 +303,9 @@ namespace Avalonia
///
public StyledElement? Parent { get; private set; }
+ ///
+ public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty);
+
///
/// Gets the styled element's logical parent.
///
@@ -394,7 +375,7 @@ namespace Avalonia
///
public bool ApplyStyling()
{
- if (_initCount == 0 && (!_stylesApplied || !_themeApplied))
+ if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied))
{
GetValueStore().BeginStyling();
@@ -644,13 +625,19 @@ namespace Avalonia
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
+ {
OnControlThemeChanged();
- else if (change.Property == RequestedThemeVariantProperty)
+ }
+ else if (change.Property == ThemeVariant.RequestedThemeVariantProperty)
{
if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default)
- SetValue(ActualThemeVariantProperty, themeVariant);
+ SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant);
else
- ClearValue(ActualThemeVariantProperty);
+ ClearValue(ThemeVariant.ActualThemeVariantProperty);
+ }
+ else if (change.Property == ThemeVariant.ActualThemeVariantProperty)
+ {
+ ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty);
}
}
diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
index c71973fde8..6f10de3651 100644
--- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
@@ -16,11 +16,13 @@ namespace Avalonia
/// The default value of the property.
/// The default binding mode.
/// A value coercion callback.
+ /// Whether the property is interested in data validation.
public StyledPropertyMetadata(
Optional defaultValue = default,
BindingMode defaultBindingMode = BindingMode.Default,
- Func? coerce = null)
- : base(defaultBindingMode)
+ Func? coerce = null,
+ bool enableDataValidation = false)
+ : base(defaultBindingMode, enableDataValidation)
{
_defaultValue = defaultValue;
CoerceValue = coerce;
diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
index 33d4cd0824..8bdcec2e53 100644
--- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
+++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs
@@ -1,4 +1,5 @@
#nullable enable
+using System;
using Avalonia.LogicalTree;
namespace Avalonia.Styling.Activators
@@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators
private readonly int _step;
private readonly int _offset;
private readonly bool _reversed;
+ private int _index = -1;
public NthChildActivator(
ILogical control,
@@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators
protected override bool EvaluateIsActive()
{
- return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
+ var index = _index >= 0 ? _index : _provider.GetChildIndex(_control);
+ return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch;
}
- protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged;
- protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged;
+ protected override void Initialize()
+ {
+ _provider.ChildIndexChanged += ChildIndexChanged;
+ }
+
+ protected override void Deinitialize()
+ {
+ _provider.ChildIndexChanged -= ChildIndexChanged;
+ }
private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e)
{
// Run matching again if:
- // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
- // 2. e.Child is null, when all children indices were changed.
- // 3. Subscribed child index was changed.
- if (_reversed
- || e.Child is null
- || e.Child == _control)
+ // 1. Subscribed child index was changed
+ // 2. Child indexes were reset
+ // 3. We're a reversed (nth-last-child) selector and total count has changed
+ if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) ||
+ (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged))
{
+ // We're using the _index field to pass the index of the child to EvaluateIsActive
+ // *only* when the active state is re-evaluated via this event handler. The docs
+ // for EvaluateIsActive say:
+ //
+ // > This method should read directly from its inputs and not rely on any
+ // > subscriptions to fire in order to be up-to-date.
+ //
+ // Which is good advice in general, however in this case we need to break the rule
+ // and use the value from the event subscription instead of calling
+ // IChildIndexProvider.GetChildIndex. This is because this event can be fired during
+ // the process of realizing an element of a virtualized list; in this case calling
+ // GetChildIndex may not return the correct index as the element isn't yet realized.
+ _index = e.Index;
ReevaluateIsActive();
+ _index = -1;
}
}
+
+ private void TotalCountChanged(object? sender, EventArgs e)
+ {
+ if (_reversed)
+ ReevaluateIsActive();
+ }
}
}
diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
deleted file mode 100644
index 2467d99b3b..0000000000
--- a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using Avalonia.Controls;
-using Avalonia.Metadata;
-
-namespace Avalonia.Styling;
-
-///
-/// Interface for an application host element with a root theme variant.
-///
-[Unstable]
-public interface IGlobalThemeVariantProvider : IResourceHost
-{
- ///
- /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination.
- ///
- ThemeVariant ActualThemeVariant { get; }
-
- ///
- /// Raised when the theme variant is changed on the element or an ancestor of the element.
- ///
- event EventHandler? ActualThemeVariantChanged;
-}
diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs
new file mode 100644
index 0000000000..01583148a8
--- /dev/null
+++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs
@@ -0,0 +1,26 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Metadata;
+
+namespace Avalonia.Styling;
+
+///
+/// Interface for the host element with a theme variant.
+///
+[Unstable]
+public interface IThemeVariantHost : IResourceHost
+{
+ ///
+ /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty.
+ ///
+ ///
+ /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned.
+ /// Otherwise, current OS theme variant is returned.
+ ///
+ ThemeVariant ActualThemeVariant { get; }
+
+ ///
+ /// Raised when the theme variant is changed on the element or an ancestor of the element.
+ ///
+ event EventHandler? ActualThemeVariantChanged;
+}
diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs
index ccfc2c781d..532179bb2c 100644
--- a/src/Avalonia.Base/Styling/NthChildSelector.cs
+++ b/src/Avalonia.Base/Styling/NthChildSelector.cs
@@ -61,7 +61,7 @@ namespace Avalonia.Styling
{
return subscribe
? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
- : Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
+ : Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed);
}
else
{
@@ -70,10 +70,9 @@ namespace Avalonia.Styling
}
internal static SelectorMatch Evaluate(
- ILogical logical, IChildIndexProvider childIndexProvider,
+ int index, IChildIndexProvider childIndexProvider,
int step, int offset, bool reversed)
{
- var index = childIndexProvider.GetChildIndex(logical);
if (index < 0)
{
return SelectorMatch.NeverThisInstance;
diff --git a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
index 826b45582d..be5a999771 100644
--- a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
+++ b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Styling
AvaloniaProperty property,
BindingMode mode,
IObservable source)
- : base(instance, property, source)
+ : base(target, instance, property, source)
{
_target = target;
_mode = mode;
diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
index 7a39407ba2..7604c26244 100644
--- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
+++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Data;
using Avalonia.PropertyStore;
namespace Avalonia.Styling
@@ -19,6 +20,13 @@ namespace Avalonia.Styling
public object? GetValue() => _value ??= _template.Build();
+ bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ state = BindingValueType.Value;
+ error = null;
+ return false;
+ }
+
void IValueEntry.Unsubscribe() { }
}
}
diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs
index 093597c6a0..9b009be6d2 100644
--- a/src/Avalonia.Base/Styling/Setter.cs
+++ b/src/Avalonia.Base/Styling/Setter.cs
@@ -90,6 +90,13 @@ namespace Avalonia.Styling
object? IValueEntry.GetValue() => Value;
+ bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error)
+ {
+ state = BindingValueType.Value;
+ error = null;
+ return false;
+ }
+
private AvaloniaProperty EnsureProperty()
{
return Property ?? throw new InvalidOperationException("Setter.Property must be set.");
@@ -99,7 +106,8 @@ namespace Avalonia.Styling
{
if (!Property!.IsDirect)
{
- var i = binding.Initiate(target, Property)!;
+ var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false;
+ var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!;
var mode = i.Mode;
if (mode == BindingMode.Default)
diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs
index 8218533f4f..389136b0f5 100644
--- a/src/Avalonia.Base/Styling/ThemeVariant.cs
+++ b/src/Avalonia.Base/Styling/ThemeVariant.cs
@@ -6,11 +6,26 @@ using Avalonia.Platform;
namespace Avalonia.Styling;
///
-/// Specifies a UI theme variant that should be used for the
+/// Specifies a UI theme variant that should be used for the Control and Application types.
///
[TypeConverter(typeof(ThemeVariantTypeConverter))]
public sealed record ThemeVariant
{
+ ///
+ /// Defines the ActualThemeVariant property.
+ ///
+ internal static readonly StyledProperty ActualThemeVariantProperty =
+ AvaloniaProperty.Register(
+ "ActualThemeVariant",
+ inherits: true);
+
+ ///
+ /// Defines the RequestedThemeVariant property.
+ ///
+ internal static readonly StyledProperty RequestedThemeVariantProperty =
+ AvaloniaProperty.Register(
+ "RequestedThemeVariant", defaultValue: Default);
+
///
/// Creates a new instance of the
///
diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
index 827a02334a..30b7738409 100644
--- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
+++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs
@@ -2,7 +2,7 @@ using System.Collections.Generic;
namespace Avalonia.Threading
{
- public class ThreadSafeObjectPool where T : class, new()
+ internal class ThreadSafeObjectPool where T : class, new()
{
private Stack _stack = new Stack();
public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool();
@@ -17,11 +17,14 @@ namespace Avalonia.Threading
}
}
- public void Return(T obj)
+ public void ReturnAndSetNull(ref T? obj)
{
+ if (obj == null)
+ return;
lock (_stack)
{
_stack.Push(obj);
+ obj = null;
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
index 06a77f0894..f5db7c0855 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
@@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge)
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
index f9b84793c6..fcf72385b2 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
@@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex()
{
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
index d906cd359c..5a4ddd36f4 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
+++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
@@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives
internal void InvalidateChildIndex(DataGridRow row)
{
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index));
}
///
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
index dd8575c989..ca516c8918 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
@@ -1,6 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0.6
0.8
@@ -9,19 +51,6 @@
M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z
M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z
-
-
-
-
-
-
-
-
-
-
-
@@ -29,23 +58,10 @@
-
+
-
-
-
-
-
-
-
-
-
@@ -565,5 +581,6 @@
+
diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
index 951e60c25b..3d3d01e06e 100644
--- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
+++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs
@@ -536,11 +536,12 @@ namespace Avalonia.Controls
internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo)
{
+ var index = virtInfo.Index;
+
_viewportManager.OnElementPrepared(element, virtInfo);
if (ElementPrepared != null)
{
- var index = virtInfo.Index;
if (_elementPreparedArgs == null)
{
@@ -554,7 +555,7 @@ namespace Avalonia.Controls
ElementPrepared(this, _elementPreparedArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index));
}
internal void OnElementClearing(Control element)
@@ -573,7 +574,7 @@ namespace Avalonia.Controls
ElementClearing(this, _elementClearingArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1));
}
internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex)
@@ -592,7 +593,7 @@ namespace Avalonia.Controls
ElementIndexChanged(this, _elementIndexChangedArgs);
}
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex));
}
private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue)
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 6d3ba3cf8a..6d9a6bd493 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -29,7 +29,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
///
- public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents
+ public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents
{
///
/// The application-global data templates.
@@ -50,13 +50,13 @@ namespace Avalonia
public static readonly StyledProperty DataContextProperty =
StyledElement.DataContextProperty.AddOwner();
- ///
+ ///
public static readonly StyledProperty ActualThemeVariantProperty =
- StyledElement.ActualThemeVariantProperty.AddOwner();
+ ThemeVariantScope.ActualThemeVariantProperty.AddOwner();
- ///
+ ///
public static readonly StyledProperty RequestedThemeVariantProperty =
- StyledElement.RequestedThemeVariantProperty.AddOwner();
+ ThemeVariantScope.RequestedThemeVariantProperty.AddOwner();
///
public event EventHandler? ResourcesChanged;
@@ -95,11 +95,8 @@ namespace Avalonia
set => SetValue(RequestedThemeVariantProperty, value);
}
- ///
- public ThemeVariant ActualThemeVariant
- {
- get => GetValue(ActualThemeVariantProperty);
- }
+ ///
+ public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty);
///
/// Gets the current instance of the class.
@@ -256,7 +253,7 @@ namespace Avalonia
.Bind().ToTransient()
.Bind().ToConstant(this)
.Bind().ToConstant(this)
- .Bind().ToConstant(this)
+ .Bind().ToConstant(this)
.Bind().ToConstant(FocusManager)
.Bind().ToConstant(InputManager)
.Bind().ToTransient()
diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs
new file mode 100644
index 0000000000..9cc0f17818
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs
@@ -0,0 +1,21 @@
+using Avalonia.Automation.Peers;
+
+namespace Avalonia.Controls.Automation.Peers
+{
+ public class ImageAutomationPeer : ControlAutomationPeer
+ {
+ public ImageAutomationPeer(Control owner) : base(owner)
+ {
+ }
+
+ override protected string GetClassNameCore()
+ {
+ return "Image";
+ }
+
+ override protected AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.Image;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs
new file mode 100644
index 0000000000..b0f83c1f2a
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs
@@ -0,0 +1,63 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Controls.Automation.Peers
+{
+ public class RadioButtonAutomationPeer : ToggleButtonAutomationPeer, ISelectionItemProvider
+ {
+ public RadioButtonAutomationPeer(RadioButton owner) : base(owner)
+ {
+ owner.PropertyChanged += (a, e) =>
+ {
+ if (e.Property == RadioButton.IsCheckedProperty)
+ {
+ RaiseToggleStatePropertyChangedEvent((bool?)e.OldValue, (bool?)e.NewValue);
+ }
+ };
+ }
+
+ override protected string GetClassNameCore()
+ {
+ return "RadioButton";
+ }
+
+ override protected AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.RadioButton;
+ }
+
+ public bool IsSelected => ((RadioButton)Owner).IsChecked == true;
+
+ public ISelectionProvider? SelectionContainer => null;
+
+ public void AddToSelection()
+ {
+ if (((RadioButton)Owner).IsChecked != true)
+ throw new InvalidOperationException("Operation cannot be performed");
+ }
+
+ public void RemoveFromSelection()
+ {
+ if (((RadioButton)Owner).IsChecked == true)
+ throw new InvalidOperationException("Operation cannot be performed");
+ }
+
+ public void Select()
+ {
+ if (!IsEnabled())
+ throw new InvalidOperationException("Element is disabled thus it cannot be selected");
+
+ ((RadioButton)Owner).IsChecked = true;
+ }
+
+ internal virtual void RaiseToggleStatePropertyChangedEvent(bool? oldValue, bool? newValue)
+ {
+ RaisePropertyChangedEvent(
+ SelectionItemPatternIdentifiers.IsSelectedProperty,
+ oldValue == true,
+ newValue == true);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs
new file mode 100644
index 0000000000..418ae1f1fe
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs
@@ -0,0 +1,16 @@
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Automation
+{
+ ///
+ /// Contains values used as identifiers by .
+ ///
+ public static class SelectionItemPatternIdentifiers
+ {
+ /// Indicates the element is currently selected.
+ public static AutomationProperty IsSelectedProperty { get; } = new AutomationProperty();
+
+ /// Indicates the element is currently selected.
+ public static AutomationProperty SelectionContainerProperty { get; } = new AutomationProperty();
+ }
+}
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index ab7c9948c4..325593508c 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -161,7 +161,7 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
-
+
///
/// Occurs when the user has completed a context input gesture, such as a right-click.
///
@@ -403,7 +403,7 @@ namespace Avalonia.Controls
{
if (_focusAdorner == null)
{
- var template = GetValue(FocusAdornerProperty);
+ var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
if (template != null)
{
diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs
index 2ad6a58d38..668de5bca9 100644
--- a/src/Avalonia.Controls/Expander.cs
+++ b/src/Avalonia.Controls/Expander.cs
@@ -191,7 +191,7 @@ namespace Avalonia.Controls
///
/// Invoked just before the event.
///
- protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
+ protected virtual void OnCollapsing(CancelRoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
@@ -207,7 +207,7 @@ namespace Avalonia.Controls
///
/// Invoked just before the event.
///
- protected virtual void OnExpanding(RoutedEventArgs eventArgs)
+ protected virtual void OnExpanding(CancelRoutedEventArgs eventArgs)
{
RaiseEvent(eventArgs);
}
diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
index e1f840672d..dbffb803a3 100644
--- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
+++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs
@@ -82,7 +82,7 @@ namespace Avalonia.Controls
public sealed override void Render(DrawingContext context)
{
- if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc)
+ if (context is IDrawingContextWithAcrylicLikeSupport idc)
{
var cornerRadius = CornerRadius;
diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs
index 3e76835e92..b14cc78e60 100644
--- a/src/Avalonia.Controls/Image.cs
+++ b/src/Avalonia.Controls/Image.cs
@@ -1,5 +1,6 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
+using Avalonia.Controls.Automation.Peers;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
@@ -130,5 +131,10 @@ namespace Avalonia.Controls
return new Size();
}
}
+
+ protected override AutomationPeer OnCreateAutomationPeer()
+ {
+ return new ImageAutomationPeer(this);
+ }
}
}
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index ce12d5f2bf..9483f98881 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -102,7 +102,6 @@ namespace Avalonia.Controls
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler? _childIndexChanged;
private IDataTemplate? _displayMemberItemTemplate;
- private Tuple? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
private ItemsPresenter? _itemsPresenter;
@@ -218,7 +217,6 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
-
///
public event EventHandler HorizontalSnapPointsChanged
{
@@ -495,6 +493,7 @@ namespace Avalonia.Controls
else if (change.Property == ItemCountProperty)
{
UpdatePseudoClasses(change.GetNewValue());
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
@@ -579,7 +578,7 @@ namespace Avalonia.Controls
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
- _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty);
+ _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
internal void PrepareItemContainer(Control container, object? item, int index)
@@ -601,17 +600,14 @@ namespace Avalonia.Controls
internal void ItemContainerPrepared(Control container, object? item, int index)
{
- _containerBeingPrepared = new(index, container);
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
- _containerBeingPrepared = null;
-
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
_scrollViewer?.RegisterAnchorCandidate(container);
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
- _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container));
+ _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
}
internal void ClearItemContainer(Control container)
@@ -742,9 +738,6 @@ namespace Avalonia.Controls
int IChildIndexProvider.GetChildIndex(ILogical child)
{
- if (_containerBeingPrepared?.Item2 == child)
- return _containerBeingPrepared.Item1;
-
return child is Control container ? IndexFromContainer(container) : -1;
}
diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index 5588bde7c0..1670e496b4 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -13,6 +13,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
+using Avalonia.Layout;
namespace Avalonia.Controls
{
@@ -85,16 +86,16 @@ namespace Avalonia.Controls
///
/// Defines the event.
///
- public static readonly RoutedEvent PointerEnteredItemEvent =
- RoutedEvent.Register