diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 4a7a329fc6..477aaec6a8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -21,6 +21,7 @@ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj", "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", + "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", "src\\Avalonia.Headless\\Avalonia.Headless.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 5988e3e35c..1eb4a8b615 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -248,6 +248,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -559,9 +561,14 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -570,10 +577,6 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -582,6 +585,10 @@ Global {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -647,10 +654,10 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 5735da19ab..7221fe4657 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -1,11 +1,3 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - -trigger: -- master - jobs: - job: Mac pool: @@ -25,6 +17,7 @@ jobs: - script: system_profiler SPDisplaysDataType |grep Resolution - script: | + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium & pkill IntegrationTestApp 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/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index a9876df189..5125b42426 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -30,6 +30,7 @@ + diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 938c45a4e2..db74be7c08 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -16,10 +16,10 @@ namespace ControlCatalog.Pages private Point _cursorPoint; - public StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); + public static readonly StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } - public StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation)); + public static readonly StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation)); /// /// Rotation, measured in Radians! /// @@ -33,10 +33,10 @@ namespace ControlCatalog.Pages } } - public StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); + public static readonly StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); public double ViewportCenterY { get => GetValue(ViewportCenterYProperty); set => SetValue(ViewportCenterYProperty, value); } - public StyledProperty ViewportCenterXProperty = AvaloniaProperty.Register(nameof(ViewportCenterX), 0.0d); + public static readonly StyledProperty ViewportCenterXProperty = AvaloniaProperty.Register(nameof(ViewportCenterX), 0.0d); public double ViewportCenterX { get => GetValue(ViewportCenterXProperty); set => SetValue(ViewportCenterXProperty, value); } private IPen _pen; 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/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index da1ff5442d..32e46af9dd 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -93,7 +93,7 @@ public class PointerCanvas : Control } private int _threadSleep; - public static DirectProperty ThreadSleepProperty = + public static readonly DirectProperty ThreadSleepProperty = AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); public int ThreadSleep @@ -103,7 +103,7 @@ public class PointerCanvas : Control } private bool _drawOnlyPoints; - public static DirectProperty DrawOnlyPointsProperty = + public static readonly DirectProperty DrawOnlyPointsProperty = AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); public bool DrawOnlyPoints @@ -113,8 +113,8 @@ public class PointerCanvas : Control } private string? _status; - public static DirectProperty StatusProperty = - AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + public static readonly DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public string? Status diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 0a761d70ba..1356eeb526 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -19,6 +19,7 @@ + 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/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/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/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 a37fa6fd32..f2106f2f86 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,93 +251,87 @@ 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; 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 +339,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,17 +354,13 @@ 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. @@ -364,11 +369,13 @@ namespace Avalonia.Media /// The bounds. /// A disposable used to undo the opacity. public PushedState PushOpacity(double opacity, Rect bounds) - //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity, bounds); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -380,70 +387,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 7b02649b6c..a41054202e 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,10 +67,7 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { @@ -105,13 +102,11 @@ namespace Avalonia.Media return rect; } - private class DrawingGroupDrawingContext : IDrawingContextImpl + private sealed class DrawingGroupDrawingContext : DrawingContext { private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - private Matrix _transform; - private bool _disposed; // Root drawing created by this DrawingContext. @@ -135,17 +130,7 @@ namespace Avalonia.Media _drawingGroup = drawingGroup; } - public Matrix Transform - { - get => _transform; - set - { - _transform = value; - PushTransform(new MatrixTransform(value)); - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if ((brush == null) && (pen == null)) { @@ -159,7 +144,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { if ((brush == null) && (pen == null)) { @@ -169,7 +154,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground == null) { @@ -179,124 +164,70 @@ namespace Avalonia.Media GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = new GlyphRun(glyphRun) + GlyphRun = glyphRun }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen? pen, Point p1, Point p2) - { - if (pen == null) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); - } - - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) - { - if ((brush == null) && (pen == null)) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); - } - - public void Clear(Color color) - { - throw new NotImplementedException(); - } - - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotImplementedException(); - } - - public void Custom(ICustomDrawOperation custom) - { - throw new NotImplementedException(); - } - - public object? GetFeature(Type t) => null; - - public void DrawBitmap(IRef 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, Rect bounds) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -366,22 +297,34 @@ namespace Avalonia.Media // Restore the previous value of the current drawing group _currentDrawingGroup = _previousDrawingGroupStack.Pop(); } - + /// /// 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/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index ac2dce1e42..abfd2e33ac 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -27,8 +27,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); /// /// Gets or sets the that describes the shape of this . 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/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 2564d89bac..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -354,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index e9086eee37..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/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 55bfb33041..5c48c11e19 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Collections; namespace Avalonia.Media { @@ -20,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public AvaloniaList Points + public Points Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index c1b9b77401..75249ff7e7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -89,13 +89,13 @@ namespace Avalonia.Media.TextFormatting var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); - var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; + var glyphInfo = shapedBuffer[glyphIndex]; - shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, + shapedBuffer[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; + glyphRun.GlyphInfos = shapedBuffer; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index f29bdd4459..3f26d081b0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -9,12 +10,13 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IReadOnlyList, IDisposable { private GlyphInfo[]? _rentedBuffer; + private ArraySlice _glyphInfos; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); Text = text; - GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + _glyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; @@ -23,27 +25,70 @@ namespace Avalonia.Media.TextFormatting internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; - GlyphInfos = glyphInfos; + _glyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; } - internal ArraySlice GlyphInfos { get; private set; } - - public int Length - => GlyphInfos.Length; + /// + /// The buffer's length. + /// + public int Length => _glyphInfos.Length; + /// + /// The buffer's glyph typeface. + /// public IGlyphTypeface GlyphTypeface { get; } + /// + /// The buffers font rendering em size. + /// public double FontRenderingEmSize { get; } + /// + /// The buffer's bidi level. + /// public sbyte BidiLevel { get; } + /// + /// The buffer's reading direction. + /// public bool IsLeftToRight => (BidiLevel & 1) == 0; + /// + /// The text that is represended by this buffer. + /// public ReadOnlyMemory Text { get; } + /// + /// Reverses the buffer. + /// + public void Reverse() + { + _glyphInfos.Span.Reverse(); + } + + public void Dispose() + { + if (_rentedBuffer is not null) + { + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + _glyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array + } + } + + public GlyphInfo this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _glyphInfos[index]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _glyphInfos[index] = value; + } + + public IEnumerator GetEnumerator() => _glyphInfos.GetEnumerator(); + /// /// Finds a glyph index for given character index. /// @@ -53,20 +98,19 @@ namespace Avalonia.Media.TextFormatting /// private int FindGlyphIndex(int characterIndex) { - if (characterIndex < GlyphInfos[0].GlyphCluster) + if (characterIndex < _glyphInfos[0].GlyphCluster) { return 0; } - if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster) + if (characterIndex > _glyphInfos[_glyphInfos.Length - 1].GlyphCluster) { - return GlyphInfos.Length - 1; + return _glyphInfos.Length - 1; } - var comparer = GlyphInfo.ClusterAscendingComparer; - var glyphInfos = GlyphInfos.Span; + var glyphInfos = _glyphInfos.Span; var searchValue = new GlyphInfo(default, characterIndex, default); @@ -109,42 +153,24 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphInfos[0].GlyphCluster; - var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; + var firstCluster = _glyphInfos[0].GlyphCluster; + var lastCluster = _glyphInfos[_glyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; var glyphCount = FindGlyphIndex(start + length); var first = new ShapedBuffer(Text.Slice(0, length), - GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + _glyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); var second = new ShapedBuffer(Text.Slice(length), - GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + _glyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); } - int IReadOnlyCollection.Count => GlyphInfos.Length; - - public GlyphInfo this[int index] - { - get => GlyphInfos[index]; - set => GlyphInfos[index] = value; - } - - public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); + int IReadOnlyCollection.Count => _glyphInfos.Length; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public void Dispose() - { - if (_rentedBuffer is not null) - { - ArrayPool.Shared.Return(_rentedBuffer); - _rentedBuffer = null; - GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array - } - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 568148e15c..c5dd30b620 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -85,7 +85,7 @@ namespace Avalonia.Media.TextFormatting { _glyphRun = null; - ShapedBuffer.GlyphInfos.Span.Reverse(); + ShapedBuffer.Reverse(); IsReversed = !IsReversed; } @@ -106,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; + var advance = ShapedBuffer[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -130,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; + var advance = ShapedBuffer[i].GlyphAdvance; if (width + advance > availableWidth) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7f74f49982..12efb3c383 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -583,7 +583,7 @@ namespace Avalonia.Media.TextFormatting { if (shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; + var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster; var lastCluster = firstCluster; for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) 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/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/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 source) { - var e = new SourceUntypedBindingEntry(this, property, source); + var e = new SourceUntypedBindingEntry(Owner!.Owner, this, property, source); Add(e); return e; } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 5908d9e535..9e9b4a3190 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -1,121 +1,25 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Data; -using Avalonia.Threading; namespace Avalonia.PropertyStore { - internal class LocalValueBindingObserver : IObserver, - IObserver>, - IDisposable + internal class LocalValueBindingObserver : LocalValueBindingObserverBase, + IObserver { - private readonly ValueStore _owner; - private IDisposable? _subscription; - private T? _defaultValue; - private bool _isDefaultValueInitialized; - public LocalValueBindingObserver(ValueStore owner, StyledProperty property) + : base(owner, property) { - _owner = owner; - Property = property; } - public StyledProperty Property { get;} - - public void Start(IObservable source) - { - _subscription = source.Subscribe(this); - } - - public void Start(IObservable> source) - { - _subscription = source.Subscribe(this); - } + public void Start(IObservable source) => _subscription = source.Subscribe(this); - public void Dispose() + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + public void OnNext(object? value) { - _subscription?.Dispose(); - _subscription = null; - _owner.OnLocalValueBindingCompleted(Property, this); - } - - public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); - public void OnError(Exception error) => OnCompleted(); - - public void OnNext(T value) - { - static void Execute(LocalValueBindingObserver instance, T value) - { - var owner = instance._owner; - var property = instance.Property; - - if (property.ValidateValue?.Invoke(value) == false) - value = instance.GetCachedDefaultValue(); - - owner.SetValue(property, value, BindingPriority.LocalValue); - } - - if (Dispatcher.UIThread.CheckAccess()) - { - Execute(this, value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, newValue)); - } - } - - public void OnNext(BindingValue value) - { - static void Execute(LocalValueBindingObserver instance, BindingValue value) - { - var owner = instance._owner; - var property = instance.Property; - - LoggingUtils.LogIfNecessary(owner.Owner, property, value); - - if (value.HasValue) - { - var effectiveValue = value.Value; - if (property.ValidateValue?.Invoke(effectiveValue) == false) - effectiveValue = instance.GetCachedDefaultValue(); - owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); - } - else - { - owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); - } - } - - if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) + if (value == BindingOperations.DoNothing) return; - - if (Dispatcher.UIThread.CheckAccess()) - { - Execute(this, value); - } - else - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, newValue)); - } - } - - private T GetCachedDefaultValue() - { - if (!_isDefaultValueInitialized) - { - _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); - _isDefaultValueInitialized = true; - } - - return _defaultValue!; + base.OnNext(BindingValue.FromUntyped(value, Property.PropertyType)); } } } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs new file mode 100644 index 0000000000..5d920cf88d --- /dev/null +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserverBase.cs @@ -0,0 +1,133 @@ +using System; +using Avalonia.Data; +using Avalonia.Threading; + +namespace Avalonia.PropertyStore +{ + internal class LocalValueBindingObserverBase : IObserver, + IObserver>, + IDisposable + { + private readonly ValueStore _owner; + private readonly bool _hasDataValidation; + protected IDisposable? _subscription; + private T? _defaultValue; + private bool _isDefaultValueInitialized; + + protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty property) + { + _owner = owner; + Property = property; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; + } + + public StyledProperty Property { get;} + + public void Start(IObservable source) + { + _subscription = source.Subscribe(this); + } + + public void Start(IObservable> source) + { + _subscription = source.Subscribe(this); + } + + public void Dispose() + { + _subscription?.Dispose(); + _subscription = null; + OnCompleted(); + } + + public void OnCompleted() + { + if (_hasDataValidation) + _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); + + _owner.OnLocalValueBindingCompleted(Property, this); + } + + public void OnError(Exception error) => OnCompleted(); + + public void OnNext(T value) + { + static void Execute(LocalValueBindingObserverBase instance, T value) + { + var owner = instance._owner; + var property = instance.Property; + + if (property.ValidateValue?.Invoke(value) == false) + value = instance.GetCachedDefaultValue(); + + owner.SetLocalValue(property, value); + + if (instance._hasDataValidation) + owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null); + } + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); + } + } + + public void OnNext(BindingValue value) + { + static void Execute(LocalValueBindingObserverBase instance, BindingValue value) + { + var owner = instance._owner; + var property = instance.Property; + var originalType = value.Type; + + LoggingUtils.LogIfNecessary(owner.Owner, property, value); + + // Revert to the default value if the binding value fails validation, or if + // there was no value (though not if there was a data validation error). + if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) || + (!value.HasValue && value.Type != BindingValueType.DataValidationError)) + value = value.WithValue(instance.GetCachedDefaultValue()); + + if (value.HasValue) + owner.SetLocalValue(property, value.Value); + if (instance._hasDataValidation) + owner.Owner.OnUpdateDataValidation(property, originalType, value.Error); + } + + if (value.Type is BindingValueType.DoNothing) + return; + + if (Dispatcher.UIThread.CheckAccess()) + { + Execute(this, value); + } + else + { + // To avoid allocating closure in the outer scope we need to capture variables + // locally. This allows us to skip most of the allocations when on UI thread. + var instance = this; + var newValue = value; + Dispatcher.UIThread.Post(() => Execute(instance, newValue)); + } + } + + private T GetCachedDefaultValue() + { + if (!_isDefaultValueInitialized) + { + _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); + _isDefaultValueInitialized = true; + } + + return _defaultValue!; + } + } +} diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs deleted file mode 100644 index 46e6ed810a..0000000000 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using Avalonia.Data; -using Avalonia.Threading; - -namespace Avalonia.PropertyStore -{ - internal class LocalValueUntypedBindingObserver : IObserver, - IDisposable - { - private readonly ValueStore _owner; - private IDisposable? _subscription; - private T? _defaultValue; - private bool _isDefaultValueInitialized; - - public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) - { - _owner = owner; - Property = property; - } - - public StyledProperty Property { get; } - - public void Start(IObservable source) - { - _subscription = source.Subscribe(this); - } - - public void Dispose() - { - _subscription?.Dispose(); - _subscription = null; - _owner.OnLocalValueBindingCompleted(Property, this); - } - - public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); - public void OnError(Exception error) => OnCompleted(); - - public void OnNext(object? value) - { - static void Execute(LocalValueUntypedBindingObserver instance, object? value) - { - var owner = instance._owner; - var property = instance.Property; - - if (value is BindingNotification n) - { - value = n.Value; - LoggingUtils.LogIfNecessary(owner.Owner, property, n); - } - - if (value == AvaloniaProperty.UnsetValue) - { - owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); - } - else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) - { - owner.SetValue(property, typedValue, BindingPriority.LocalValue); - } - else - { - owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); - LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); - } - } - - if (value == BindingOperations.DoNothing) - return; - - if (Dispatcher.UIThread.CheckAccess()) - { - Execute(this, value); - } - else if (value != BindingOperations.DoNothing) - { - // To avoid allocating closure in the outer scope we need to capture variables - // locally. This allows us to skip most of the allocations when on UI thread. - var instance = this; - var newValue = value; - Dispatcher.UIThread.Post(() => Execute(instance, newValue)); - } - } - - private T GetCachedDefaultValue() - { - if (!_isDefaultValueInitialized) - { - _defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); - _isDefaultValueInitialized = true; - } - - return _defaultValue!; - } - } -} diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b82714817b..99c6a3ee9d 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore private readonly Func? _validate; public SourceUntypedBindingEntry( + AvaloniaObject target, ValueFrame frame, StyledProperty property, IObservable source) - : base(frame, property, source) + : base(target, frame, property, source) { _validate = property.ValidateValue; } diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 550f5c0001..c209138605 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -10,18 +10,20 @@ namespace Avalonia.PropertyStore internal sealed class TypedBindingEntry : BindingEntryBase { public TypedBindingEntry( + AvaloniaObject target, ValueFrame frame, StyledProperty property, IObservable source) - : base(frame, property, source) + : base(target, frame, property, source) { } public TypedBindingEntry( + AvaloniaObject target, ValueFrame frame, StyledProperty property, IObservable> source) - : base(frame, property, source) + : base(target, frame, property, source) { } diff --git a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs index a77d7fddb6..e3a7607479 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedBindingEntry.cs @@ -12,10 +12,11 @@ namespace Avalonia.PropertyStore private readonly Func? _validate; public UntypedBindingEntry( + AvaloniaObject target, ValueFrame frame, AvaloniaProperty property, IObservable source) - : base(frame, property, source) + : base(target, frame, property, source) { _validate = ((IStyledPropertyAccessor)property).ValidateValue; } diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index ec6ed392c1..0887f11ec9 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -104,7 +104,7 @@ namespace Avalonia.PropertyStore { if (priority == BindingPriority.LocalValue) { - var observer = new LocalValueUntypedBindingObserver(this, property); + var observer = new LocalValueBindingObserver(this, property); DisposeExistingLocalValueBinding(property); _localValueBindings ??= new(); _localValueBindings[property.Id] = observer; @@ -193,18 +193,7 @@ namespace Avalonia.PropertyStore } else { - if (TryGetEffectiveValue(property, out var existing)) - { - var effective = (EffectiveValue)existing; - effective.SetLocalValueAndRaise(this, property, value); - } - else - { - var effectiveValue = CreateEffectiveValue(property); - AddEffectiveValue(property, effectiveValue); - effectiveValue.SetLocalValueAndRaise(this, property, value); - } - + SetLocalValue(property, value); return null; } } @@ -223,6 +212,21 @@ namespace Avalonia.PropertyStore } } + public void SetLocalValue(StyledProperty property, T value) + { + if (TryGetEffectiveValue(property, out var existing)) + { + var effective = (EffectiveValue)existing; + effective.SetLocalValueAndRaise(this, property, value); + } + else + { + var effectiveValue = CreateEffectiveValue(property); + AddEffectiveValue(property, effectiveValue); + effectiveValue.SetLocalValueAndRaise(this, property, value); + } + } + public object? GetValue(AvaloniaProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) @@ -834,8 +838,6 @@ namespace Avalonia.PropertyStore break; } - current?.EndReevaluation(); - if (current?.Priority == BindingPriority.Unset) { if (current.BasePriority == BindingPriority.Unset) @@ -848,6 +850,8 @@ namespace Avalonia.PropertyStore current.RemoveAnimationAndRaise(this, property); } } + + current?.EndReevaluation(); } finally { @@ -919,7 +923,6 @@ namespace Avalonia.PropertyStore for (var i = _effectiveValues.Count - 1; i >= 0; --i) { _effectiveValues.GetKeyValue(i, out var key, out var e); - e.EndReevaluation(); if (e.Priority == BindingPriority.Unset) { @@ -929,6 +932,8 @@ namespace Avalonia.PropertyStore if (i > _effectiveValues.Count) break; } + + e.EndReevaluation(); } } finally diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 7fa2d4955f..df3a70b3e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor { _root = root; _compositor = compositor; - _recordingContext = new DrawingContext(_recorder); + _recordingContext = _recorder; CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; @@ -225,7 +225,7 @@ public class CompositingRenderer : IRendererWithCompositor sortedChildren.Dispose(); } else - foreach (var ch in v.GetVisualChildren()) + foreach (var ch in visualChildren) { var compositionChild = ch.CompositionVisual; if (compositionChild != null) diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs index 10a7c3e360..5d45a725c1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Platform; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -13,8 +14,6 @@ namespace Avalonia.Rendering.Composition.Drawing; /// 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 6b380608fe..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,26 +301,24 @@ 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, Rect bounds) + + protected override void PushOpacityCore(double opacity, Rect bounds) { var next = NextDrawAs(); @@ -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 08e506536f..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; @@ -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/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 09d2d55ce3..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; 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/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 39aaf55014..cbdf3c3c1e 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -46,6 +46,7 @@ namespace Avalonia defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, + enableDataValidation: false, notifying: DataContextNotifying); /// @@ -802,8 +803,11 @@ namespace Avalonia if (theme.HasChildren) { - foreach (var child in theme.Children) - ApplyStyle(child, null, type); + var children = theme.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], null, type); + } } } @@ -815,8 +819,11 @@ namespace Avalonia if (host.IsStylesInitialized) { - foreach (var style in host.Styles) - ApplyStyle(style, host, FrameType.Style); + var styles = host.Styles; + for (var i = 0; i < styles.Count; ++i) + { + ApplyStyle(styles[i], host, FrameType.Style); + } } } @@ -825,8 +832,11 @@ namespace Avalonia if (style is Style s) s.TryAttach(this, host, type); - foreach (var child in style.Children) - ApplyStyle(child, host, type); + var children = style.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], host, type); + } } private void ReevaluateImplicitTheme() 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/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/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index c5ef9a0b2b..3b0aa03492 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -71,9 +71,9 @@ namespace Avalonia.Styling var activators = new OrActivatorBuilder(); var neverThisInstance = false; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { - var match = selector.Match(control, parent, subscribe); + var match = _selectors[i].Match(control, parent, subscribe); switch (match.Result) { @@ -108,16 +108,19 @@ namespace Avalonia.Styling internal override void ValidateNestingSelector(bool inControlTheme) { - foreach (var selector in _selectors) - selector.ValidateNestingSelector(inControlTheme); + for (var i = 0; i < _selectors.Count; i++) + { + _selectors[i].ValidateNestingSelector(inControlTheme); + } } private Type? EvaluateTargetType() { Type? result = null; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { + var selector = _selectors[i]; if (selector.TargetType == null) { return null; 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/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7883fd23ab..f8670cfdb3 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Controls; using Avalonia.Styling.Activators; using Avalonia.Utilities; @@ -15,7 +14,7 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; - private readonly Lazy> _classes = new Lazy>(() => new List()); + private List? _classes; private Type? _targetType; private string? _selectorString; @@ -81,7 +80,7 @@ namespace Avalonia.Styling /// /// The style classes which the selector matches. /// - public IList Classes => _classes.Value; + public IList Classes => _classes ??= new(); /// public override string ToString(Style? owner) @@ -122,16 +121,16 @@ namespace Avalonia.Styling return SelectorMatch.NeverThisInstance; } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { if (subscribe) { - var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes); return new SelectorMatch(observable); } - if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, _classes)) { return SelectorMatch.NeverThisInstance; } @@ -172,9 +171,9 @@ namespace Avalonia.Styling builder.Append(Name); } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { - foreach (var c in Classes) + foreach (var c in _classes) { if (!c.StartsWith(":")) { 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.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index 5cac2ef658..0589abb2dd 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -304,13 +304,9 @@ namespace Avalonia.Utilities { if (_entryCount == _entries!.Length) { - const double growthFactor = 1.2; - var newSize = (int)(_entryCount * growthFactor); - - if (newSize == _entryCount) - { - newSize++; - } + var newSize = _entryCount == DefaultInitialCapacity ? + DefaultInitialCapacity * 2 : + (int)(_entryCount * 1.5); var destEntries = new Entry[newSize]; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8b0cc06136..df0c5b100f 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -732,6 +732,23 @@ namespace Avalonia } } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + + var count = VisualChildren.Count; + var templatedParent = TemplatedParent; + + for (var i = 0; i < count; ++i) + { + if (VisualChildren[i] is StyledElement child && + child.TemplatedParent == templatedParent) + { + child.OnTemplatedParentControlThemeChanged(); + } + } + } + /// /// Computes the value according to the /// and 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/Simple.xaml b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml index 76e44e4daf..464a12770e 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml @@ -202,7 +202,7 @@ @@ -129,7 +129,7 @@ - + internal class CombinedGeometryImpl : GeometryImpl { - public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public CombinedGeometryImpl(SKPath? stroke, SKPath? fill) { - var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; - var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + StrokePath = stroke; + FillPath = fill; + Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; + } + + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + if (g1.PlatformImpl is GeometryImpl i1 + && g2.PlatformImpl is GeometryImpl i2 + && TryCreate(combineMode, i1, i2) is { } result) + return result; + + return new(null, null); + } + public static CombinedGeometryImpl? TryCreate(GeometryCombineMode combineMode, GeometryImpl g1, GeometryImpl g2) + { var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, @@ -21,13 +35,28 @@ namespace Avalonia.Skia _ => SKPathOp.Union }; - var path = path1?.Op(path2, op); + var stroke = + g1.StrokePath != null && g2.StrokePath != null + ? g1.StrokePath.Op(g2.StrokePath, op) + : null; + + SKPath? fill = null; + if (g1.FillPath != null && g2.FillPath != null) + { + // Reuse stroke if fill paths are the same + if (ReferenceEquals(g1.FillPath, g1.StrokePath) && ReferenceEquals(g2.FillPath, g2.StrokePath)) + fill = stroke; + else + fill = g1.FillPath.Op(g2.FillPath, op); + } - EffectivePath = path; - Bounds = path?.Bounds.ToAvaloniaRect() ?? default; + if (stroke == null && fill == null) + return null; + return new CombinedGeometryImpl(stroke, fill); } public override Rect Bounds { get; } - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + public override SKPath? FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 969f0b5e2a..af0231579c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Skia.Helpers; using SkiaSharp; +using ISceneBrush = Avalonia.Media.ISceneBrush; namespace Avalonia.Skia { @@ -25,7 +27,6 @@ namespace Avalonia.Skia private readonly Stack _opacityStack = new(); private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer? _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; @@ -61,12 +62,7 @@ namespace Avalonia.Skia /// Dpi of drawings. /// public Vector Dpi; - - /// - /// Visual brush renderer. - /// - public IVisualBrushRenderer? VisualBrushRenderer; - + /// /// Render text without Lcd rendering. /// @@ -141,7 +137,6 @@ namespace Avalonia.Skia ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); _dpi = createInfo.Dpi; - _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; @@ -235,20 +230,21 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - if (brush is not null) + if (brush is not null && impl.FillPath != null) { using (var fill = CreatePaint(_fillPaint, brush, size)) { - Canvas.DrawPath(impl.EffectivePath, fill.Paint); + Canvas.DrawPath(impl.FillPath, fill.Paint); } } if (pen is not null + && impl.StrokePath != null && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { using (stroke) { - Canvas.DrawPath(impl.EffectivePath, stroke.Paint); + Canvas.DrawPath(impl.StrokePath, stroke.Paint); } } } @@ -644,7 +640,7 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); + Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true); } /// @@ -908,7 +904,7 @@ namespace Avalonia.Skia paintWrapper.AddDisposable(intermediate); - using (var context = intermediate.CreateDrawingContext(null)) + using (var context = intermediate.CreateDrawingContext()) { var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); @@ -970,36 +966,98 @@ namespace Avalonia.Skia } } - /// - /// Configure paint wrapper to use visual brush. - /// - /// Paint wrapper. - /// Visual brush. - /// Visual brush renderer. - /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, - IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage) + private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) { - if (visualBrushRenderer == null) - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } - - var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); + if(content.UseScalableRasterization) + ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize); + else + ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize); + } + + private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - var intermediate = CreateRenderTarget(intermediateSize, false); + using var intermediate = CreateRenderTarget(intermediateSize, false); - using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) + using (var ctx = intermediate.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); - - visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } - tileBrushImage = intermediate; - paintWrapper.AddDisposable(tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate); + } + } + + private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var contentSize = rect.Size; + if (contentSize.Width <= 0 || contentSize.Height <= 0) + { + paintWrapper.Paint.Color = SKColor.Empty; + return; + } + + var tileBrush = content.Brush; + var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y); + + var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize); + transform *= calc.IntermediateTransform; + + using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); + using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) + { + ctx.PushClip(calc.IntermediateClip); + content.Render(ctx, transform); + ctx.PopClip(); + } + + using var picture = pictureTarget.GetPicture(); + + var paintTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); + + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + paintTransform = SKMatrix.Concat(paintTransform, + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + + if (tileBrush.Transform is { }) + { + var origin = tileBrush.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(origin); + var brushTransform = (-offset) * tileBrush.Transform.Value * (offset); + + paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix()); + } + + using (var shader = picture.ToShader(tileX, tileY, paintTransform, + new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height))) + { + paintWrapper.Paint.FilterQuality = SKFilterQuality.None; + paintWrapper.Paint.Shader = shader; } } @@ -1113,12 +1171,25 @@ namespace Avalonia.Skia } var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; var tileBrushImage = default(IDrawableBitmapImpl); - if (visualBrush != null) + if (brush is ISceneBrush sceneBrush) { - ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + using (var content = sceneBrush.CreateContent()) + { + if (content != null) + { + ConfigureSceneBrushContent(ref paintWrapper, content, targetSize); + return paintWrapper; + } + else + paint.Color = default; + } + } + else if (brush is ISceneBrushContent sceneBrushContent) + { + ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize); + return paintWrapper; } else { diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs index ac05691c67..64976d4eea 100644 --- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class EllipseGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath => StrokePath; public EllipseGeometryImpl(Rect rect) { var path = new SKPath(); path.AddOval(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index d53dcd2df3..90a2f9169b 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -99,7 +99,20 @@ namespace Avalonia.Skia { SKTypeface? skTypeface = null; - if (typeface.FontFamily.Key is null) + if(typeface.FontFamily.Key is not null) + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface); + + if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + } + + if (skTypeface is null) { var defaultName = SKTypeface.Default.FamilyName; @@ -128,13 +141,7 @@ namespace Avalonia.Skia skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) ?? SKTypeface.Default; } - else - { - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - - skTypeface = fontCollection.Get(typeface); - } - + if (skTypeface == null) { throw new InvalidOperationException( diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index f1216100bc..a22b67e09e 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -36,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -55,7 +55,6 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true }; diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 2828f9a9c1..01be42bad0 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -11,26 +11,51 @@ namespace Avalonia.Skia { public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { - var path = new SKPath + var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; + var count = children.Count; + + var stroke = new SKPath { - FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + FillType = fillType }; - - var count = children.Count; + bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + if (children[i].PlatformImpl is GeometryImpl geo) { - path.AddPath(effectivePath); + if (geo.StrokePath != null) + stroke.AddPath(geo.StrokePath); + if (!ReferenceEquals(geo.StrokePath, geo.FillPath)) + requiresFillPass = true; } } + + StrokePath = stroke; + + if (requiresFillPass) + { + var fill = new SKPath + { + FillType = fillType + }; + + for (var i = 0; i < count; ++i) + { + if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + fill.AddPath(fillPath); + } + + FillPath = fill; + } + else + FillPath = stroke; - EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = stroke.TightBounds.ToAvaloniaRect(); } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 34270c2078..aee84d1346 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -14,7 +14,7 @@ namespace Avalonia.Skia private PathCache _pathCache; private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(StrokePath!); /// public abstract Rect Bounds { get; } @@ -24,19 +24,20 @@ namespace Avalonia.Skia { get { - if (EffectivePath is null) + if (StrokePath is null) return 0; return CachedPathMeasure.Length; } } - public abstract SKPath? EffectivePath { get; } + public abstract SKPath? StrokePath { get; } + public abstract SKPath? FillPath { get; } /// public bool FillContains(Point point) { - return PathContainsCore(EffectivePath, point); + return PathContainsCore(FillPath, point); } /// @@ -74,7 +75,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; - paint.GetFillPath(EffectivePath, strokePath); + paint.GetFillPath(StrokePath, strokePath); SKPaintCache.Shared.ReturnReset(paint); @@ -96,14 +97,10 @@ namespace Avalonia.Skia /// public IGeometryImpl? Intersect(IGeometryImpl geometry) { - if (EffectivePath is { } path - && (geometry as GeometryImpl)?.EffectivePath is { } otherPath - && path.Op(otherPath, SKPathOp.Intersect) is { } result) - { - return new StreamGeometryImpl(result); - } - - return null; + var other = geometry as GeometryImpl; + if (other == null) + return null; + return CombinedGeometryImpl.TryCreate(GeometryCombineMode.Intersect, this, other); } /// @@ -128,7 +125,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAtDistance(double distance, out Point point) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); return false; @@ -142,7 +139,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); tangent = new Point(); @@ -158,7 +155,7 @@ namespace Avalonia.Skia public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { - if (EffectivePath is null) + if (StrokePath is null) { segmentGeometry = null; return false; @@ -172,7 +169,7 @@ namespace Avalonia.Skia if (res) { - segmentGeometry = new StreamGeometryImpl(_skPathSegment); + segmentGeometry = new StreamGeometryImpl(_skPathSegment, null); } return res; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 7f9108481d..797c565ca1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var session = _renderTarget.BeginRenderingSession(); @@ -31,7 +31,6 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, Gpu = _skiaGpu, CurrentSession = session diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 4d8afe9830..b774ddf411 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -13,15 +13,13 @@ namespace Avalonia.Skia.Helpers /// /// /// - /// /// DrawingContext - public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer? visualBrushRenderer = null) + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi) { var createInfo = new DrawingContextImpl.CreateInfo { Canvas = canvas, Dpi = dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, }; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index 4cb1430a3b..6adfc01951 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -60,5 +60,15 @@ namespace Avalonia.Skia.Helpers } } } + + // This method is here mostly for debugging purposes + internal static void SavePicture(SKPicture picture, float scale, string path) + { + var snapshotSize = new SKSizeI((int)Math.Ceiling(picture.CullRect.Width * scale), + (int)Math.Ceiling(picture.CullRect.Height * scale)); + using var snap = + SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale)); + SaveImage(snap, path); + } } } diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs index b102a4c119..2ac0a2e18c 100644 --- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs @@ -9,7 +9,8 @@ namespace Avalonia.Skia internal class LineGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => null; public LineGeometryImpl(Point p1, Point p2) { @@ -17,7 +18,7 @@ namespace Avalonia.Skia path.MoveTo(p1.ToSKPoint()); path.LineTo(p2.ToSKPoint()); - EffectivePath = path; + StrokePath = path; Bounds = new Rect( new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)), new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y))); diff --git a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs new file mode 100644 index 0000000000..280b7c27cd --- /dev/null +++ b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Platform; +using Avalonia.Reactive; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class PictureRenderTarget : IDisposable +{ + private readonly ISkiaGpu? _gpu; + private readonly GRContext? _grContext; + private readonly Vector _dpi; + private SKPicture? _picture; + + public PictureRenderTarget(ISkiaGpu? gpu, GRContext? grContext, Vector dpi) + { + _gpu = gpu; + _grContext = grContext; + _dpi = dpi; + } + + public SKPicture GetPicture() + { + var rv = _picture ?? throw new InvalidOperationException(); + _picture = null; + return rv; + } + + public IDrawingContextImpl CreateDrawingContext(Size size) + { + var recorder = new SKPictureRecorder(); + var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96), + (float)(size.Height * _dpi.Y / 96))); + + canvas.RestoreToCount(-1); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = _dpi, + DisableTextLcdRendering = true, + GrContext = _grContext, + Gpu = _gpu, + }; + return new DrawingContextImpl(createInfo, Disposable.Create(() => + { + _picture = recorder.EndRecording(); + canvas.Dispose(); + recorder.Dispose(); + })); + } + + public void Dispose() => _picture?.Dispose(); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ab1c6b8816..9c4b326f14 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -65,7 +65,7 @@ namespace Avalonia.Skia public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - return new CombinedGeometryImpl(combineMode, g1, g2); + return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) @@ -104,7 +104,7 @@ namespace Avalonia.Skia SKFontCache.Shared.Return(skFont); - return new StreamGeometryImpl(path); + return new StreamGeometryImpl(path, path); } /// diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs index 93d453e8f0..2d127e07c9 100644 --- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class RectangleGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => StrokePath; public RectangleGeometryImpl(Rect rect) { var path = new SKPath(); path.AddRect(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 0c3289767e..eb081c4f50 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -10,36 +11,39 @@ namespace Avalonia.Skia internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { private Rect _bounds; - private readonly SKPath _effectivePath; + private readonly SKPath _strokePath; + private SKPath? _fillPath; /// /// Initializes a new instance of the class. /// - /// An existing Skia . + /// An existing Skia for the stroke. + /// An existing Skia for the fill, can also be null or the same as the stroke /// Precomputed path bounds. - public StreamGeometryImpl(SKPath path, Rect bounds) + public StreamGeometryImpl(SKPath stroke, SKPath? fill, Rect? bounds = null) { - _effectivePath = path; - _bounds = bounds; + _strokePath = stroke; + _fillPath = fill; + _bounds = bounds ?? stroke.TightBounds.ToAvaloniaRect(); } - /// - /// Initializes a new instance of the class. - /// - /// An existing Skia . - public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) + private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect)) { + } /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), default) + public StreamGeometryImpl() : this(CreateEmptyPath()) { } - + /// - public override SKPath EffectivePath => _effectivePath; + public override SKPath? StrokePath => _strokePath; + + /// + public override SKPath? FillPath => _fillPath; /// public override Rect Bounds => _bounds; @@ -47,7 +51,9 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); + var stroke = _strokePath.Clone(); + var fill = _fillPath == _strokePath ? stroke : _fillPath.Clone(); + return new StreamGeometryImpl(stroke, fill, Bounds); } /// @@ -74,7 +80,10 @@ namespace Avalonia.Skia private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private readonly SKPath _path; + private SKPath Stroke => _geometryImpl._strokePath; + private SKPath Fill => _geometryImpl._fillPath ??= new(); + private bool _isFilled; + private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke); /// /// Initializes a new instance of the class. @@ -83,52 +92,79 @@ namespace Avalonia.Skia public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._effectivePath; } /// /// Will update bounds of passed geometry. public void Dispose() { - _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect(); _geometryImpl.InvalidateCaches(); } /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - _path.ArcTo( + var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small; + var sweep = sweepDirection == SweepDirection.Clockwise + ? SKPathDirection.Clockwise + : SKPathDirection.CounterClockwise; + Stroke.ArcTo( (float)size.Width, (float)size.Height, (float)rotationAngle, - isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small, - sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, + arc, + sweep, (float)point.X, (float)point.Y); + if(Duplicate) + Fill.ArcTo( + (float)size.Width, + (float)size.Height, + (float)rotationAngle, + arc, + sweep, + (float)point.X, + (float)point.Y); } /// public void BeginFigure(Point startPoint, bool isFilled) { - _path.MoveTo((float)startPoint.X, (float)startPoint.Y); + if (!isFilled) + { + if (Stroke == Fill) + _geometryImpl._fillPath = Stroke.Clone(); + } + + _isFilled = isFilled; + Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y); + if(Duplicate) + Fill.MoveTo((float)startPoint.X, (float)startPoint.Y); } /// public void CubicBezierTo(Point point1, Point point2, Point point3) { - _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + if(Duplicate) + Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); } /// public void QuadraticBezierTo(Point point1, Point point2) { - _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + if(Duplicate) + Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); } /// public void LineTo(Point point) { - _path.LineTo((float)point.X, (float)point.Y); + Stroke.LineTo((float)point.X, (float)point.Y); + if(Duplicate) + Fill.LineTo((float)point.X, (float)point.Y); } /// @@ -136,14 +172,16 @@ namespace Avalonia.Skia { if (isClosed) { - _path.Close(); + Stroke.Close(); + if (Duplicate) + Fill.Close(); } } /// public void SetFillRule(FillRule fillRule) { - _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; + Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; } } } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index f88e74d738..92210c30e2 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -97,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -106,7 +106,6 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e7dd4fb6da..153e4f0720 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -56,8 +56,6 @@ namespace Avalonia.Skia var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; - var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -83,7 +81,7 @@ namespace Avalonia.Skia 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index fb3c2e403f..2dee8c318c 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -17,16 +17,27 @@ namespace Avalonia.Skia { SourceGeometry = source; Transform = transform; + var matrix = transform.ToSKMatrix(); - var transformedPath = source.EffectivePath.Clone(); - transformedPath?.Transform(transform.ToSKMatrix()); - - EffectivePath = transformedPath; + var transformedPath = StrokePath = source.StrokePath.Clone(); + transformedPath?.Transform(matrix); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; + + if (ReferenceEquals(source.StrokePath, source.FillPath)) + FillPath = transformedPath; + else if (source.FillPath != null) + { + FillPath = transformedPath = source.FillPath.Clone(); + transformedPath.Transform(matrix); + } } /// - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + + /// + public override SKPath? FillPath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 02932c52da..10f9239b1a 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => + return new DrawingContextImpl( null, target, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 984a24fb30..0af326d6a8 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1 { } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var locked = _surface.Lock(); if (locked.Format == PixelFormat.Rgb565) @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(visualBrushRenderer); + .CreateDrawingContext(); } public bool IsCorrupted => false; @@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1 _target = target; } - public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public override IDrawingContextImpl CreateDrawingContext() { - return base.CreateDrawingContext(visualBrushRenderer, () => + return base.CreateDrawingContext(() => { using (var l = WicImpl.Lock(BitmapLockFlags.Read)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 0dd9c155bb..f9b5953e3f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media /// internal class DrawingContextImpl : IDrawingContextImpl { - private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; @@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( - IVisualBrushRenderer visualBrushRenderer, ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { - _visualBrushRenderer = visualBrushRenderer; _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; @@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media var radialGradientBrush = brush as IRadialGradientBrush; var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; - var visualBrush = brush as IVisualBrush; + var sceneBrush = brush as ISceneBrush; + var sceneBrushContent = brush as ISceneBrushContent; if (solidColorBrush != null) { @@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media (BitmapImpl)imageBrush.Source.PlatformImpl.Item, destinationSize); } - else if (visualBrush != null) + else if (sceneBrush != null || sceneBrushContent != null) { - if (_visualBrushRenderer != null) + sceneBrushContent ??= sceneBrush.CreateContent(); + if (sceneBrushContent != null) { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + var rect = sceneBrushContent.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { @@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); using (var intermediate = new BitmapRenderTarget( - _deviceContext, - CompatibleRenderTargetOptions.None, - pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) + _deviceContext, + CompatibleRenderTargetOptions.None, + pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + sceneBrushContent.Render(ctx, + rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } return new ImageBrushImpl( - visualBrush, + sceneBrushContent.Brush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), destinationSize); } + } } - else - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } } return new SolidColorBrushImpl(null, _deviceContext); diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 829b887d9d..a08c96c40c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, calc.IntermediateSize.ToSharpDX()); - using (var context = new RenderTarget(result).CreateDrawingContext(null)) + using (var context = new RenderTarget(result).CreateDrawingContext()) { var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2dbc1d67d1..6b1ca911fb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); + return new DrawingContextImpl( this, _renderTarget, null, () => Version++); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index d6b1e618e5..fa40e75fa7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - => CreateDrawingContext(visualBrushRenderer, null); + public virtual IDrawingContextImpl CreateDrawingContext() + => CreateDrawingContext(null); public bool IsCorrupted => false; - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) + public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) { - return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => + return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => { Version++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index ff0fff6b14..d42bc3c5cf 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -52,8 +52,6 @@ namespace Avalonia.Direct2D1.Media var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; - var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -79,7 +77,7 @@ namespace Avalonia.Direct2D1.Media 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 8d5062336c..4392e35058 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); + return new DrawingContextImpl(this, _renderTarget); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 531c4119af..385120505c 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var size = GetWindowSize(); var dpi = GetWindowDpi(); @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 Resize(); } - return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); + return new DrawingContextImpl(this, _deviceContext, _swapChain); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 48ebd4068e..3eeedc4b5d 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -41,6 +41,8 @@ namespace Avalonia.Win32.Automation { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple }, { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired }, { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, + { SelectionItemPatternIdentifiers.IsSelectedProperty, UiaPropertyId.SelectionItemIsSelected }, + { SelectionItemPatternIdentifiers.SelectionContainerProperty, UiaPropertyId.SelectionItemSelectionContainer } }; private static ConditionalWeakTable s_nodes = new(); diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index 2d9331b5dc..6044eb6d2d 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + True diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 7e63987d1b..ff5a4aa81d 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -4,6 +4,7 @@ netstandard2.0 enable false + True diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index b6036bba8f..9f74d2fc08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -888,7 +888,8 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); var called = false; - var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})"; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; + var message = "Unable to convert object 'foo' of type 'System.String' to type 'System.Double'."; LogCallback checkLogMessage = (level, area, src, mt, pv) => { @@ -898,9 +899,7 @@ namespace Avalonia.Base.UnitTests src == target && pv[0].GetType() == typeof(Class1) && (AvaloniaProperty)pv[1] == Class1.QuxProperty && - (Type)pv[2] == typeof(double) && - (string)pv[3] == "foo" && - (Type)pv[4] == typeof(string)) + (string)pv[2] == message) { called = true; } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index d48e58136a..12cd39046b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -1,115 +1,212 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.UnitTests; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests { public class AvaloniaObjectTests_DataValidation { - [Fact] - public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation() + public abstract class TestBase + where T : AvaloniaProperty { - var target = new Class1(); - var source = new Subject>(); + [Fact] + public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetNonValidatedProperty(); - target.Bind(Class1.NonValidatedProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); - Assert.Empty(target.Notifications); - } + Assert.Empty(target.Notifications); + } - [Fact] - public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() - { - var target = new Class1(); - var source = new Subject>(); + [Fact] + public void Binding_Validated_Property_Calls_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error1 = new Exception(); + var error2 = new Exception(); - target.Bind(Class1.NonValidatedDirectProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error1)); + source.OnNext(BindingValue.BindingError(error2)); + source.OnNext(7); - Assert.Empty(target.Notifications); - } + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error1), + new(BindingValueType.BindingError, 0, error2), + new(BindingValueType.Value, 7, null), + }, target.Notifications); + } - [Fact] - public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() - { - var target = new Class1(); - var source = new Subject>(); - - target.Bind(Class1.ValidatedDirectIntProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(7); - - var result = target.Notifications; - Assert.Equal(4, result.Count); - Assert.Equal(BindingValueType.Value, result[0].type); - Assert.Equal(6, result[0].value); - Assert.Equal(BindingValueType.BindingError, result[1].type); - Assert.Equal(BindingValueType.DataValidationError, result[2].type); - Assert.Equal(BindingValueType.Value, result[3].type); - Assert.Equal(7, result[3].value); + [Fact] + public void Binding_Validated_Property_Calls_UpdateDataValidation_Untyped() + { + var target = new Class1(); + var source = new Subject(); + var property = GetProperty(); + var error1 = new Exception(); + var error2 = new Exception(); + + target.Bind(property, source); + source.OnNext(6); + source.OnNext(new BindingNotification(error1, BindingErrorType.DataValidationError)); + source.OnNext(new BindingNotification(error2, BindingErrorType.Error)); + source.OnNext(7); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error1), + new(BindingValueType.BindingError, 0, error2), + new(BindingValueType.Value, 7, null), + }, target.Notifications); + } + + [Fact] + public void Binding_Overridden_Validated_Property_Calls_UpdateDataValidation() + { + var target = new Class2(); + var source = new Subject>(); + var property = GetNonValidatedProperty(); + + // Class2 overrides the non-validated property metadata to enable data validation. + target.Bind(property, source); + source.OnNext(1); + + Assert.Equal(1, target.Notifications.Count); + } + + [Fact] + public void Disposing_Binding_Subscription_Clears_DataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error = new Exception(); + var sub = target.Bind(property, source); + + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error)); + sub.Dispose(); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error), + new(BindingValueType.UnsetValue, 6, null), + }, target.Notifications); + } + + [Fact] + public void Completing_Binding_Clears_DataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error = new Exception(); + + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error)); + source.OnCompleted(); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error), + new(BindingValueType.UnsetValue, 6, null), + }, target.Notifications); + } + + protected abstract T GetProperty(); + protected abstract T GetNonValidatedProperty(); } - [Fact] - public void Binding_Overridden_Validated_Direct_Property_Calls_UpdateDataValidation() + public class DirectPropertyTests : TestBase> { - var target = new Class2(); - var source = new Subject>(); + [Fact] + public void Bound_Validated_String_Property_Can_Be_Set_To_Null() + { + var source = new ViewModel + { + StringValue = "foo", + }; - // Class2 overrides `NonValidatedDirectProperty`'s metadata to enable data validation. - target.Bind(Class1.NonValidatedDirectProperty, source); - source.OnNext(1); + var target = new Class1 + { + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; + + Assert.Equal("foo", target.ValidatedDirectString); - Assert.Equal(1, target.Notifications.Count); + source.StringValue = null; + + Assert.Null(target.ValidatedDirectString); + } + + protected override DirectPropertyBase GetProperty() => Class1.ValidatedDirectIntProperty; + protected override DirectPropertyBase GetNonValidatedProperty() => Class1.NonValidatedDirectIntProperty; } - [Fact] - public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + public class StyledPropertyTests : TestBase> { - var source = new ViewModel + [Fact] + public void Bound_Validated_String_Property_Can_Be_Set_To_Null() { - StringValue = "foo", - }; + var source = new ViewModel + { + StringValue = "foo", + }; - var target = new Class1 - { - [!Class1.ValidatedDirectStringProperty] = new Binding + var target = new Class1 { - Path = nameof(ViewModel.StringValue), - Source = source, - }, - }; + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; - Assert.Equal("foo", target.ValidatedDirectString); + Assert.Equal("foo", target.ValidatedDirectString); - source.StringValue = null; + source.StringValue = null; - Assert.Null(target.ValidatedDirectString); + Assert.Null(target.ValidatedDirectString); + } + + protected override StyledProperty GetProperty() => Class1.ValidatedStyledIntProperty; + protected override StyledProperty GetNonValidatedProperty() => Class1.NonValidatedStyledIntProperty; } + private record class Notification(BindingValueType type, object? value, Exception? error); + private class Class1 : AvaloniaObject { - public static readonly StyledProperty NonValidatedProperty = - AvaloniaProperty.Register( - nameof(NonValidated)); - - public static readonly DirectProperty NonValidatedDirectProperty = + public static readonly DirectProperty NonValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(NonValidatedDirect), - o => o.NonValidatedDirect, - (o, v) => o.NonValidatedDirect = v); + nameof(NonValidatedDirectInt), + o => o.NonValidatedDirectInt, + (o, v) => o.NonValidatedDirectInt = v); public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( @@ -118,27 +215,30 @@ namespace Avalonia.Base.UnitTests (o, v) => o.ValidatedDirectInt = v, enableDataValidation: true); - public static readonly DirectProperty ValidatedDirectStringProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( nameof(ValidatedDirectString), o => o.ValidatedDirectString, (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); + public static readonly StyledProperty NonValidatedStyledIntProperty = + AvaloniaProperty.Register( + nameof(NonValidatedStyledInt)); + + public static readonly StyledProperty ValidatedStyledIntProperty = + AvaloniaProperty.Register( + nameof(ValidatedStyledInt), + enableDataValidation: true); + private int _nonValidatedDirect; private int _directInt; - private string _directString; + private string? _directString; - public int NonValidated - { - get { return GetValue(NonValidatedProperty); } - set { SetValue(NonValidatedProperty, value); } - } - - public int NonValidatedDirect + public int NonValidatedDirectInt { get { return _directInt; } - set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } + set { SetAndRaise(NonValidatedDirectIntProperty, ref _nonValidatedDirect, value); } } public int ValidatedDirectInt @@ -147,20 +247,32 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); } } - public string ValidatedDirectString + public string? ValidatedDirectString { get { return _directString; } set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } - public List<(BindingValueType type, object value)> Notifications { get; } = new(); + public int NonValidatedStyledInt + { + get { return GetValue(NonValidatedStyledIntProperty); } + set { SetValue(NonValidatedStyledIntProperty, value); } + } + + public int ValidatedStyledInt + { + get => GetValue(ValidatedStyledIntProperty); + set => SetValue(ValidatedStyledIntProperty, value); + } + + public List Notifications { get; } = new(); protected override void UpdateDataValidation( AvaloniaProperty property, BindingValueType state, - Exception error) + Exception? error) { - Notifications.Add((state, GetValue(property))); + Notifications.Add(new(state, GetValue(property), error)); } } @@ -168,16 +280,18 @@ namespace Avalonia.Base.UnitTests { static Class2() { - NonValidatedDirectProperty.OverrideMetadata( + NonValidatedDirectIntProperty.OverrideMetadata( new DirectPropertyMetadata(enableDataValidation: true)); + NonValidatedStyledIntProperty.OverrideMetadata( + new StyledPropertyMetadata(enableDataValidation: true)); } } public class ViewModel : NotifyingBase { - private string _stringValue; + private string? _stringValue; - public string StringValue + public string? StringValue { get { return _stringValue; } set { _stringValue = value; RaisePropertyChanged(); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 7e932373c2..181596a681 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -198,6 +198,7 @@ namespace Avalonia.Base.UnitTests defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, + enableDataValidation: false, notifying: FooNotifying); public int NotifyCount { get; private set; } diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 3d7dc66cc4..1bb1b4af73 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -1,13 +1,63 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests.Input { public class MouseDeviceTests : PointerTestsBase { + [Fact] + public void Initial_Buttons_Are_Not_Set_Without_Corresponding_Mouse_Down() + { + using var scope = AvaloniaLocator.EnterScope(); + var settingsMock = new Mock(); + var threadingMock = new Mock(); + + threadingMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true); + + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(settingsMock.Object); + + using var app = UnitTestApplication.Start( + new TestServices( + inputManager: new InputManager(), + threadingInterface: threadingMock.Object)); + + var renderer = RendererMocks.CreateRenderer(); + var device = new MouseDevice(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var control = new Control(); + var root = CreateInputRoot(impl.Object, control); + + MouseButton button = default; + + root.PointerReleased += (s, e) => button = e.InitialPressMouseButton; + + var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown); + var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp); + + SetHit(renderer, control); + + impl.Object.Input!(up); + + Assert.Equal(MouseButton.None, button); + + impl.Object.Input!(down); + impl.Object.Input!(up); + + Assert.Equal(MouseButton.Left, button); + + impl.Object.Input!(up); + + Assert.Equal(MouseButton.None, button); + } + [Fact] public void Capture_Is_Transferred_To_Parent_When_Control_Removed() { @@ -37,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Input impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); Assert.NotNull(result); - + result.Capture(control); Assert.Same(control, result.Captured); @@ -67,8 +117,8 @@ namespace Avalonia.Base.UnitTests.Input }) } }); - - + + Point? result = null; root.PointerMoved += (_, a) => { diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs index 2d45c699f1..5915343764 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs @@ -55,20 +55,29 @@ public abstract class PointerTestsBase return root; } + protected static RawPointerEventArgs CreateRawPointerArgs( + IPointerDevice pointerDevice, + IInputRoot root, + RawPointerEventType type, + Point? position = default) + { + return new RawPointerEventArgs(pointerDevice, 0, root, type, position ?? default, default); + } + protected static RawPointerEventArgs CreateRawPointerMovedArgs( IPointerDevice pointerDevice, IInputRoot root, - Point? positition = null) + Point? position = null) { return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move, - positition ?? default, default); + position ?? default, default); } protected static PointerEventArgs CreatePointerMovedArgs( - IInputRoot root, IInputElement? source, Point? positition = null) + IInputRoot root, IInputElement? source, Point? position = null) { return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock().Object, (Visual)root, - positition ?? default, default, PointerPointProperties.None, KeyModifiers.None); + position ?? default, default, PointerPointProperties.None, KeyModifiers.None); } protected static Mock CreatePointerDeviceMock( diff --git a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs index d75bf9fe8c..f91b4b613c 100644 --- a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs +++ b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests private DrawingContext CreateDrawingContext() { - return new DrawingContext(Mock.Of()); + return new PlatformDrawingContext(Mock.Of()); } private class TestControl : Control diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index c1468a28e4..9d810fa110 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph new Matrix(), Brushes.Black, null, - geometry, default); + geometry); geometryNode.HitTest(new Point()); } @@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph private class TestRectangleDrawOperation : RectangleNode { public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) - : base(transform, pen.Brush, pen, bounds, new BoxShadows()) + : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows()) { } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs index 565b217180..a2e438e3e0 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void FillOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100)); var point = new Point(x, y); @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void StrokeOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100)); var point = new Point(x, y); diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 60603937d9..6ba648af78 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -569,6 +569,46 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_Recursively_To_VisualChildren() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + var inner = Assert.IsType(border.Child); + + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Red, inner.Background); + + Assert.Equal(null, inner.BorderBrush); + Assert.Equal(null, inner.BorderBrush); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Green, inner.Background); + + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); @@ -595,7 +635,8 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme(string tag = "theme") { - var template = new FuncControlTemplate((o, n) => new Border()); + var template = new FuncControlTemplate( + (o, n) => new Border() { Child = new Border() }); return new ControlTheme { diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs index b0db806afa..2905b1e464 100644 --- a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering _lineFill = new Line { Fill = new SolidColorBrush() }; _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; - _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); + _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true); AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 15ff6e68e3..e5c96dcab6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,9 +11,6 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { - static PointerEventArgs CreateArgs(RoutedEvent ev, object source) - => new PointerEventArgs(ev, source, new FakePointer(), (Visual)source, default, 0, PointerPointProperties.None, default); - static PointerPressedEventArgs CreatePressed(object source) => new PointerPressedEventArgs(source, new FakePointer(), (Visual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); @@ -171,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, isSubMenuOpen: true, parent: menu.Object); var nextItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -191,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -206,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -365,7 +362,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); @@ -381,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); item.Verify(x => x.Open(), Times.Never); @@ -401,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(hasSubMenu: true, isSubMenuOpen: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); parentItem.SetupGet(x => x.SubItems).Returns(new[] { item.Object, sibling.Object }); @@ -421,7 +418,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -438,7 +435,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(sibling.Object); target.PointerExited(item, e); @@ -454,7 +451,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); item.Setup(x => x.IsPointerOverSubMenu).Returns(true); target.PointerExited(item, e); @@ -488,8 +485,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); var childItem = CreateMockMenuItem(parent: item.Object); - var enter = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); - var leave = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var enter = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var leave = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); // Pointer enters item; item is selected. target.PointerEntered(item, enter); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4804b29fee..bc1225e0e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -18,6 +18,7 @@ using Avalonia.Input; using Avalonia.Rendering; using System.Threading.Tasks; using Avalonia.Threading; +using Avalonia.Interactivity; namespace Avalonia.Controls.UnitTests.Primitives { @@ -1048,6 +1049,30 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Events_Should_Be_Routed_To_Popup_Parent() + { + using (CreateServices()) + { + var popupContent = new Border(); + var popup = new Popup { Child = popupContent }; + var popupParent = new Border { Child = popup }; + var root = PreparedWindow(popupParent); + var raised = 0; + + root.LayoutManager.ExecuteInitialLayoutPass(); + popup.Open(); + root.LayoutManager.ExecuteLayoutPass(); + + var ev = new RoutedEventArgs(Button.ClickEvent); + + popupParent.AddHandler(Button.ClickEvent, (s, e) => ++raised); + popupContent.RaiseEvent(ev); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 55c43f6f96..ba8e7242a1 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -10,6 +10,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -355,6 +357,58 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(0, 0), scroll.Offset); } + [Fact] + public void NthChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + + [Fact] + public void NthLastChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthLastChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -399,7 +453,8 @@ namespace Avalonia.Controls.UnitTests private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget( IEnumerable? items = null, - bool useItemTemplate = true) + bool useItemTemplate = true, + IEnumerable