diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 741570061b..4a7a329fc6 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -42,6 +42,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", @@ -61,4 +62,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index 525e01c891..56847bae31 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" EndProject @@ -548,7 +555,6 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU - {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU @@ -560,6 +566,10 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -626,6 +636,7 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 23abf1d53f..b1fb915e04 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -238,7 +238,7 @@ -(BOOL)canBecomeKeyWindow { - if(_canBecomeKeyWindow) + if(_canBecomeKeyWindow && !_closed) { // If the window has a child window being shown as a dialog then don't allow it to become the key window. auto parent = dynamic_cast(_parent.getRaw()); @@ -292,12 +292,14 @@ { if (_parent == nullptr) return; - + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ @try { - [self invalidateShadow]; + [self invalidateShadow]; + if (self->_parent != nullptr) + self->_parent->BringToFront(); } @finally{ } diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs index 5fd331035a..f2dd217657 100644 --- a/nukebuild/BuildTasksPatcher.cs +++ b/nukebuild/BuildTasksPatcher.cs @@ -4,9 +4,58 @@ using System.IO.Compression; using System.Linq; using ILRepacking; using Mono.Cecil; +using Mono.Cecil.Cil; public class BuildTasksPatcher { + /// + /// This helper class, avoid argument null exception + /// when cecil write AssemblyNameDefinition on MemoryStream. + /// + private class Wrapper : ISymbolWriterProvider + { + readonly ISymbolWriterProvider _provider; + readonly string _filename; + + public Wrapper(ISymbolWriterProvider provider, string filename) + { + _provider = provider; + _filename = filename; + } + + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName) => + _provider.GetSymbolWriter(module, string.IsNullOrWhiteSpace(fileName) ? _filename : fileName); + + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream) => + _provider.GetSymbolWriter(module, symbolStream); + } + + private static string GetSourceLinkInfo(string path) + { + try + { + using (var asm = AssemblyDefinition.ReadAssembly(path, + new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + })) + { + if (asm.MainModule.CustomDebugInformations?.OfType()?.FirstOrDefault() is { } sli) + { + return sli.Content; + } + } + } + catch + { + + } + return null; + } + public static void PatchBuildTasksInPackage(string packagePath) { using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), @@ -19,7 +68,7 @@ public class BuildTasksPatcher { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempDir); - var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll"); + var temp = Path.Combine(tempDir, entry.Name); var output = temp + ".output"; File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName, Path.Combine(tempDir, "Microsoft.Build.Framework.dll")); @@ -27,41 +76,74 @@ public class BuildTasksPatcher try { entry.ExtractToFile(temp, true); + // Get Original SourceLinkInfo Content + var sourceLinkInfoContent = GetSourceLinkInfo(temp); var repack = new ILRepacking.ILRepack(new RepackOptions() { Internalize = true, InputAssemblies = new[] { - temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0] - .FullyQualifiedName, + temp, + typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0].FullyQualifiedName, typeof(Mono.Cecil.Rocks.MethodBodyRocks).Assembly.GetModules()[0].FullyQualifiedName, typeof(Mono.Cecil.Pdb.PdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, - typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName - + typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, }, - SearchDirectories = new string[0], + SearchDirectories = Array.Empty(), + DebugInfo = true, // Allowed read debug info OutputFile = output }); repack.Repack(); - // 'hurr-durr assembly with the same name is already loaded' prevention using (var asm = AssemblyDefinition.ReadAssembly(output, - new ReaderParameters { ReadWrite = true, InMemory = true, })) + new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + })) { asm.Name = new AssemblyNameDefinition( "Avalonia.Build.Tasks." + Guid.NewGuid().ToString().Replace("-", ""), new Version(0, 0, 0)); - asm.Write(patched); + + var mainModule = asm.MainModule; + + // If we have SourceLink info copy to patched assembly. + if (!string.IsNullOrEmpty(sourceLinkInfoContent)) + { + mainModule.CustomDebugInformations.Add(new SourceLinkDebugInformation(sourceLinkInfoContent)); + } + + // Try to get SymbolWriter if it has it + var reader = mainModule.SymbolReader; + var hasDebugInfo = reader is not null; + var proivder = reader?.GetWriterProvider() is ISymbolWriterProvider p + ? new Wrapper(p, "Avalonia.Build.Tasks.dll") + : default(ISymbolWriterProvider); + + var parameters = new WriterParameters + { +#if ISNETFULLFRAMEWORK + StrongNameKeyPair = signingStep.KeyPair, +#endif + WriteSymbols = hasDebugInfo, + SymbolWriterProvider = proivder, + DeterministicMvid = hasDebugInfo, + }; + asm.Write(patched, parameters); patched.Position = 0; } + } finally { try { - if(Directory.Exists(tempDir)) + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch @@ -79,4 +161,4 @@ public class BuildTasksPatcher } } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e24860e3e1..e5f29abb68 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -306,25 +306,8 @@ namespace ControlCatalog.Pages resultText += @$" Content: "; -#if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif - using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + resultText += await ReadTextFromFile(file, 10000); } openedFileContent.Text = resultText; @@ -354,6 +337,28 @@ namespace ControlCatalog.Pages } } + public static async Task ReadTextFromFile(IStorageFile file, int length) + { +#if NET6_0_OR_GREATER + await using var stream = await file.OpenReadAsync(); +#else + using var stream = await file.OpenReadAsync(); +#endif + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 3f8a023060..390fa32b9c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -25,7 +25,6 @@ BorderThickness="2"> Drag Me (custom) - + + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index e384db88b3..26430b4b61 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -1,27 +1,29 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - TextBlock _DropState; + private readonly TextBlock _dropState; private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); - _DropState = this.Get("DropState"); + _dropState = this.Get("DropState"); int textCount = 0; SetupDnd("Text", d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move); - SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); + SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); } void SetupDnd(string suffix, Action factory, DragDropEffects effects) @@ -68,12 +70,12 @@ namespace ControlCatalog.Pages // Only allow if the dragged data contains text or filenames. if (!e.Data.Contains(DataFormats.Text) - && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(DataFormats.Files) && !e.Data.Contains(CustomFormat)) e.DragEffects = DragDropEffects.None; } - void Drop(object? sender, DragEventArgs e) + async void Drop(object? sender, DragEventArgs e) { if (e.Source is Control c && c.Name == "MoveTarget") { @@ -85,11 +87,41 @@ namespace ControlCatalog.Pages } if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); + { + _dropState.Text = e.Data.GetText(); + } + else if (e.Data.Contains(DataFormats.Files)) + { + var files = e.Data.GetFiles() ?? Array.Empty(); + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 1000); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var items = await folder.GetItemsAsync(); + contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + } + } + + _dropState.Text = contentStr; + } +#pragma warning disable CS0618 // Type or member is obsolete else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty()); + { + var files = e.Data.GetFileNames(); + _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty()); + } +#pragma warning restore CS0618 // Type or member is obsolete else if (e.Data.Contains(CustomFormat)) - _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + { + _dropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } } dragMe.PointerPressed += DoDrag; diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index dc0eaf0a51..b1b1b99c9c 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 3b14f0ce12..ac78d9c739 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -6,4 +6,5 @@ 11 + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index a6f463fdcb..58e8aef1e9 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -56,6 +56,16 @@ + + + Sample RadioButton + + Three States: Option 1 + Three States: Option 2 + + + + Unchecked @@ -153,6 +163,9 @@ + + + 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 2c9efc7767..f3a046ef80 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -664,14 +664,12 @@ namespace Avalonia /// The property that has changed. /// The old property value. /// The new property value. - /// The priority of the binding that produced the value. protected void RaisePropertyChanged( DirectPropertyBase property, - Optional oldValue, - BindingValue newValue, - BindingPriority priority = BindingPriority.LocalValue) + T oldValue, + T newValue) { - RaisePropertyChanged(property, oldValue, newValue, priority, true); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true); } /// @@ -720,7 +718,7 @@ namespace Avalonia /// /// True if the value changed, otherwise false. /// - protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) + protected bool SetAndRaise(DirectPropertyBase property, ref T field, T value) { VerifyAccess(); @@ -786,6 +784,11 @@ namespace Avalonia } } + internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) + { + UpdateDataValidation(property, state, error); + } + /// /// Gets a description of an observable that van be used in logs. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 6231483ff8..9fbf680a5c 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -199,13 +199,11 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); binding = binding ?? throw new ArgumentNullException(nameof(binding)); - var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata; - var result = binding.Initiate( target, property, anchor, - metadata?.EnableDataValidation ?? false); + property.GetMetadata(target.GetType()).EnableDataValidation ?? false); if (result != null) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 45ab293a89..24244c5068 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -227,6 +227,7 @@ namespace Avalonia /// The default binding mode for the property. /// A value validation callback. /// A value coercion callback. + /// Whether the property is interested in data validation. /// A public static StyledProperty Register( string name, @@ -234,7 +235,8 @@ namespace Avalonia bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, - Func? coerce = null) + Func? coerce = null, + bool enableDataValidation = false) where TOwner : AvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); @@ -242,7 +244,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, - coerce: coerce); + coerce: coerce, + enableDataValidation: enableDataValidation); var result = new StyledProperty( name, @@ -253,7 +256,7 @@ namespace Avalonia AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } - + /// /// /// A method that gets called before and after the property starts being notified on an @@ -267,6 +270,7 @@ namespace Avalonia BindingMode defaultBindingMode, Func? validate, Func? coerce, + bool enableDataValidation, Action? notifying) where TOwner : AvaloniaObject { @@ -275,7 +279,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, - coerce: coerce); + coerce: coerce, + enableDataValidation: enableDataValidation); var result = new StyledProperty( name, diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 2963567b14..62bb65351f 100644 --- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -13,10 +13,13 @@ namespace Avalonia /// Initializes a new instance of the class. /// /// The default binding mode. + /// Whether the property is interested in data validation. public AvaloniaPropertyMetadata( - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + bool? enableDataValidation = null) { _defaultBindingMode = defaultBindingMode; + EnableDataValidation = enableDataValidation; } /// @@ -31,6 +34,17 @@ namespace Avalonia } } + /// + /// Gets a value indicating whether the property is interested in data validation. + /// + /// + /// Data validation is validation performed at the target of a binding, for example in a + /// view model using the INotifyDataErrorInfo interface. Only certain properties on a + /// control (such as a TextBox's Text property) will be interested in receiving data + /// validation messages so this feature must be explicitly enabled by setting this flag. + /// + public bool? EnableDataValidation { get; private set; } + /// /// Merges the metadata with the base metadata. /// @@ -44,6 +58,8 @@ namespace Avalonia { _defaultBindingMode = baseMetadata.DefaultBindingMode; } + + EnableDataValidation ??= baseMetadata.EnableDataValidation; } } } diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 4b0bab0c92..8aed1545a5 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -138,18 +138,18 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged -= PropertyChanged; + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; } } @@ -163,18 +163,15 @@ namespace Avalonia.Controls PublishNext(GetValue()); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(GetValue()); - } + PublishNext(GetValue()); } private object? GetValue() { - if (_target is not StyledElement themeStyleable - || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (_target is not IThemeVariantHost themeVariantHost + || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } @@ -236,9 +233,9 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is StyledElement styleable) + if (_owner is IThemeVariantHost themeVariantHost) { - styleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } _owner = _target.Owner; @@ -247,20 +244,18 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is StyledElement styleable2) + if (_owner is IThemeVariantHost themeVariantHost2) { - styleable2.PropertyChanged += PropertyChanged; + themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } + PublishNext(); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(); - } + PublishNext(); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) @@ -270,8 +265,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is StyledElement themeStyleable) - || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (!(_target.Owner is IThemeVariantHost themeVariantHost) + || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 9ec256225b..57e4fa4a8e 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -22,7 +22,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } ValueChanged(GetValue(target)); @@ -39,7 +39,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 7c2caf02b4..e8e3e6d509 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } } 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/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index cf5a6592e1..35d50e669a 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input +using System; + +namespace Avalonia.Input { public static class DataFormats { @@ -7,9 +9,15 @@ /// public static readonly string Text = nameof(Text); + /// + /// Dataformat for one or more files. + /// + public static readonly string Files = nameof(Files); + /// /// Dataformat for one or more filenames /// + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs index 688f5f9cc8..93a6baa03c 100644 --- a/src/Avalonia.Base/Input/DataObject.cs +++ b/src/Avalonia.Base/Input/DataObject.cs @@ -2,37 +2,34 @@ namespace Avalonia.Input { + /// + /// Specific and mutable implementation of the IDataObject interface. + /// public class DataObject : IDataObject { - private readonly Dictionary _items = new Dictionary(); + private readonly Dictionary _items = new(); + /// public bool Contains(string dataFormat) { return _items.ContainsKey(dataFormat); } + /// public object? Get(string dataFormat) { - if (_items.ContainsKey(dataFormat)) - return _items[dataFormat]; - return null; + return _items.TryGetValue(dataFormat, out var item) ? item : null; } + /// public IEnumerable GetDataFormats() { return _items.Keys; } - public IEnumerable? GetFileNames() - { - return Get(DataFormats.FileNames) as IEnumerable; - } - - public string? GetText() - { - return Get(DataFormats.Text) as string; - } - + /// + /// Sets a value to the internal store of the data object with as a key. + /// public void Set(string dataFormat, object value) { _items[dataFormat] = value; diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs new file mode 100644 index 0000000000..6af531b0d8 --- /dev/null +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; + +namespace Avalonia.Input +{ + public static class DataObjectExtensions + { + /// + /// Returns a list of files if the DataObject contains files or filenames. + /// . + /// + /// + /// Collection of storage items - files or folders. If format isn't available, returns null. + /// + public static IEnumerable? GetFiles(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Files) as IEnumerable; + } + + /// + /// Returns a list of filenames if the DataObject contains filenames. + /// + /// + /// + /// Collection of file names. If format isn't available, returns null. + /// + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + public static IEnumerable? GetFileNames(this IDataObject dataObject) + { + return (dataObject.Get(DataFormats.FileNames) as IEnumerable) + ?? dataObject.GetFiles()? + .Select(f => f.TryGetLocalPath()) + .Where(p => !string.IsNullOrEmpty(p)) + .OfType(); + } + + /// + /// Returns the dragged text if the DataObject contains any text. + /// + /// + /// + /// A text string. If format isn't available, returns null. + /// + public static string? GetText(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Text) as string; + } + } +} diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index 1db008aa3a..6ccd0a8499 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; namespace Avalonia.Input { @@ -19,21 +21,12 @@ namespace Avalonia.Input /// bool Contains(string dataFormat); - /// - /// Returns the dragged text if the DataObject contains any text. - /// - /// - string? GetText(); - - /// - /// Returns a list of filenames if the DataObject contains filenames. - /// - /// - IEnumerable? GetFileNames(); - /// /// Tries to get the data of the given DataFormat. /// + /// + /// Object data. If format isn't available, returns null. + /// object? Get(string dataFormat); } } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index e1c42c4ead..50980f1c3d 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -184,6 +184,7 @@ namespace Avalonia.Input source?.RaiseEvent(e); _pointer.Capture(null); + _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 98da83c1ce..285249a5f8 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -131,6 +131,7 @@ namespace Avalonia.Input source?.RaiseEvent(e); pointer.Capture(null); + _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 4a273b0291..ea84dc84bd 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -798,6 +798,12 @@ namespace Avalonia.Layout InvalidateMeasure(); } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + InvalidateMeasure(); + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs index 497596fcc1..6b41c1c66c 100644 --- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -1,28 +1,79 @@ -#nullable enable -using System; +using System; + +#nullable enable namespace Avalonia.LogicalTree { + /// + /// Describes the action that caused a event. + /// + public enum ChildIndexChangedAction + { + /// + /// The index of a single child changed. + /// + ChildIndexChanged, + + /// + /// The index of multiple children changed and all children should be re-evaluated. + /// + ChildIndexesReset, + + /// + /// The total number of children changed. + /// + TotalCountChanged, + } + /// /// Event args for event. /// public class ChildIndexChangedEventArgs : EventArgs { - public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs(); - - private ChildIndexChangedEventArgs() + /// + /// Initializes a new instance of the class with + /// an action of . + /// + /// The child whose index was changed. + /// The new index of the child. + public ChildIndexChangedEventArgs(ILogical child, int index) { + Action = ChildIndexChangedAction.ChildIndexChanged; + Child = child; + Index = index; } - public ChildIndexChangedEventArgs(ILogical child) + private ChildIndexChangedEventArgs(ChildIndexChangedAction action) { - Child = child; + Action = action; + Index = -1; } /// - /// Logical child which index was changed. - /// If null, all children should be reset. + /// Gets the type of change action that ocurred on the list control. + /// + public ChildIndexChangedAction Action { get; } + + /// + /// Gets the logical child whose index was changed or null if all children should be re-evaluated. /// public ILogical? Child { get; } + + /// + /// Gets the new index of or -1 if all children should be re-evaluated. + /// + public int Index { get; } + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset); + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged); } } diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs index 7fcd73273c..186c9527f2 100644 --- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs @@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree bool TryGetTotalCount(out int count); /// - /// Notifies subscriber when child's index or total count was changed. + /// Notifies subscriber when a child's index was changed. /// event EventHandler? ChildIndexChanged; } diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs new file mode 100644 index 0000000000..2825628948 --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -0,0 +1,66 @@ +using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + /// + /// Paints an area with an . + /// + public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + static DrawingBrush() + { + AffectsRender(DrawingProperty); + } + + /// + /// Initializes a new instance of the class. + /// + public DrawingBrush() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + public DrawingBrush(Drawing visual) + { + Drawing = visual; + } + + /// + /// Gets or sets the visual to draw. + /// + public Drawing? Drawing + { + get { return GetValue(DrawingProperty); } + set { SetValue(DrawingProperty, value); } + } + + ISceneBrushContent? ISceneBrush.CreateContent() + { + if (Drawing == null) + return null; + + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + Drawing?.Draw(recorder); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + drawList.CalculateBounds(), true); + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 622181dba0..31a16dc69c 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -8,83 +8,45 @@ using Avalonia.Media.Imaging; namespace Avalonia.Media { - public sealed class DrawingContext : IDisposable + public abstract class DrawingContext : IDisposable { - private readonly bool _ownsImpl; - private int _currentLevel; + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + private Stack? _states; - private static ThreadSafeObjectPool> StateStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private static ThreadSafeObjectPool> TransformStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _states = StateStackPool.Get(); - - private Stack? _transformContainers = TransformStackPool.Get(); - - readonly struct TransformContainer - { - public readonly Matrix LocalTransform; - public readonly Matrix ContainerTransform; - - public TransformContainer(Matrix localTransform, Matrix containerTransform) - { - LocalTransform = localTransform; - ContainerTransform = containerTransform; - } - } - - public DrawingContext(IDrawingContextImpl impl) + internal DrawingContext() { - PlatformImpl = impl; - _ownsImpl = true; + } - - public DrawingContext(IDrawingContextImpl impl, bool ownsImpl) - { - _ownsImpl = ownsImpl; - PlatformImpl = impl; - } - - public IDrawingContextImpl PlatformImpl { get; } - - private Matrix _currentTransform = Matrix.Identity; - private Matrix _currentContainerTransform = Matrix.Identity; - - /// - /// Gets the current transform of the drawing context. - /// - public Matrix CurrentTransform + public void Dispose() { - get { return _currentTransform; } - private set + if (_states != null) { - _currentTransform = value; - var transform = _currentTransform * _currentContainerTransform; - PlatformImpl.Transform = transform; - } - } + while (_states.Count > 0) + _states.Pop().Dispose(); - //HACK: This is a temporary hack that is used in the render loop - //to update TransformedBounds property - [Obsolete("HACK for render loop, don't use")] - public Matrix CurrentContainerTransform => _currentContainerTransform; + StateStackPool.ReturnAndSetNull(ref _states); + } + DisposeCore(); + } + + protected abstract void DisposeCore(); + /// /// Draws an image. /// /// The image. /// The rect in the output to draw to. - public void DrawImage(IImage source, Rect rect) + public virtual void DrawImage(IImage source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - DrawImage(source, new Rect(source.Size), rect); } + /// /// Draws an image. /// @@ -92,12 +54,22 @@ namespace Avalonia.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = default) { _ = source ?? throw new ArgumentNullException(nameof(source)); - source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } + + /// + /// Draws a platform-specific bitmap impl. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a line. @@ -108,11 +80,11 @@ namespace Avalonia.Media public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) - { - PlatformImpl.DrawLine(pen, p1, p2); - } + DrawLineCore(pen, p1, p2); } + protected abstract void DrawLineCore(IPen pen, Point p1, Point p2); + /// /// Draws a geometry. /// @@ -121,10 +93,10 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) { - if (geometry.PlatformImpl is not null) - DrawGeometry(brush, pen, geometry.PlatformImpl); + if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null) + DrawGeometryCore(brush, pen, geometry.PlatformImpl); } - + /// /// Draws a geometry. /// @@ -133,14 +105,12 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - _ = geometry ?? throw new ArgumentNullException(nameof(geometry)); - - if (brush != null || PenIsVisible(pen)) - { - PlatformImpl.DrawGeometry(brush, pen, geometry); - } + if ((brush != null || PenIsVisible(pen))) + DrawGeometryCore(brush, pen, geometry); } + protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry); + /// /// Draws a rectangle with the specified Brush and Pen. /// @@ -158,14 +128,12 @@ namespace Avalonia.Media /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// - public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, + double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen)) - { return; - } - if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); @@ -175,20 +143,48 @@ namespace Avalonia.Media { radiusY = Math.Min(radiusY, rect.Height / 2); } - - PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + + DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + return; + DrawRectangleCore(brush, pen, rrect, boxShadows); } + protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default); + /// /// Draws the outline of a rectangle. /// /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) - { + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) => DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); - } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); /// /// Draws an ellipse with the specified Brush and Pen. @@ -204,35 +200,50 @@ namespace Avalonia.Media /// public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) { - if (brush == null && !PenIsVisible(pen)) + if (brush != null || PenIsVisible(pen)) { - return; + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height)); } - - var originX = center.X - radiusX; - var originY = center.Y - radiusY; - var width = radiusX * 2; - var height = radiusY * 2; - - PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The bounding rect. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + if (brush != null || PenIsVisible(pen)) + DrawEllipseCore(brush, pen, rect); } + protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect); + /// /// Draws a custom drawing operation /// /// custom operation - public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); + public abstract void Custom(ICustomDrawOperation custom); /// /// Draws text. /// /// The upper-left corner of the text. /// The text. - public void DrawText(FormattedText text, Point origin) + public virtual void DrawText(FormattedText text, Point origin) { _ = text ?? throw new ArgumentNullException(nameof(text)); - text.Draw(this, origin); + text.Draw(this, origin); } /// @@ -240,30 +251,31 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun); + + public record struct PushedState : IDisposable { - _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + private readonly DrawingContext _context; + private readonly int _level; - if (foreground != null) + public PushedState(DrawingContext context) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + _context = context; + _level = _context._states!.Count; } - } - /// - /// Draws a filled rectangle. - /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - { - DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + public void Dispose() + { + if(_context?._states == null) + return; + if(_context._states.Count != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._states.Pop().Dispose(); + } } - - public readonly record struct PushedState : IDisposable + + private readonly record struct RestoreState : IDisposable { - private readonly int _level; private readonly DrawingContext _context; private readonly Matrix _matrix; private readonly PushedStateType _type; @@ -271,62 +283,56 @@ namespace Avalonia.Media public enum PushedStateType { None, - Matrix, + Transform, Opacity, Clip, - MatrixContainer, GeometryClip, OpacityMask, + BitmapBlendMode } - public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default) + public RestoreState(DrawingContext context, PushedStateType type) { - if (context._states is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _context = context; _type = type; - _matrix = matrix; - _level = context._currentLevel += 1; - context._states.Push(this); } public void Dispose() { if (_type == PushedStateType.None) return; - if (_context._states is null || _context._transformContainers is null) + if (_context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); - if (_context._currentLevel != _level) - throw new InvalidOperationException("Wrong Push/Pop state order"); - _context._currentLevel--; - _context._states.Pop(); - if (_type == PushedStateType.Matrix) - _context.CurrentTransform = _matrix; + if (_type == PushedStateType.Transform) + _context.PopTransformCore(); else if (_type == PushedStateType.Clip) - _context.PlatformImpl.PopClip(); + _context.PopClipCore(); else if (_type == PushedStateType.Opacity) - _context.PlatformImpl.PopOpacity(); + _context.PopOpacityCore(); else if (_type == PushedStateType.GeometryClip) - _context.PlatformImpl.PopGeometryClip(); + _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) - _context.PlatformImpl.PopOpacityMask(); - else if (_type == PushedStateType.MatrixContainer) - { - var cont = _context._transformContainers.Pop(); - _context._currentContainerTransform = cont.ContainerTransform; - _context.CurrentTransform = cont.LocalTransform; - } + _context.PopOpacityMaskCore(); + else if (_type == PushedStateType.BitmapBlendMode) + _context.PopBitmapBlendModeCore(); } } - + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. public PushedState PushClip(RoundedRect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + protected abstract void PushClipCore(RoundedRect rect); + /// /// Pushes a clip rectangle. /// @@ -334,9 +340,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + + protected abstract void PushClipCore(Rect rect); /// /// Pushes a clip geometry. @@ -345,29 +355,28 @@ namespace Avalonia.Media /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { - _ = clip ?? throw new ArgumentNullException(nameof(clip)); - - // HACK: This check was added when nullable annotations pointed out that we're potentially - // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but - // I don't want to make that change as part of adding nullable annotations. - if (clip.PlatformImpl is null) - throw new InvalidOperationException("Cannot push empty geometry clip."); - - PlatformImpl.PushGeometryClip(clip.PlatformImpl); - return new PushedState(this, PushedState.PushedStateType.GeometryClip); + PushGeometryClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); + return new PushedState(this); } + + protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) - //TODO: Eliminate platform-specific push opacity call + public PushedState PushOpacity(double opacity, Rect bounds) { - PlatformImpl.PushOpacity(opacity); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -379,70 +388,53 @@ namespace Avalonia.Media /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { - PlatformImpl.PushOpacityMask(mask, bounds); - return new PushedState(this, PushedState.PushedStateType.OpacityMask); + PushOpacityMaskCore(mask, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask)); + return new PushedState(this); } + protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); - /// - /// Pushes a matrix post-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); - - /// - /// Pushes a matrix pre-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); - - /// - /// Sets the current matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushSetTransform(Matrix matrix) + public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) { - var oldMatrix = CurrentTransform; - CurrentTransform = matrix; - - return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + PushBitmapBlendMode(blendingMode); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); + return new PushedState(this); } - /// - /// Pushes a new transform context. - /// - /// A disposable used to undo the transformation. - public PushedState PushTransformContainer() - { - if (_transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); - _currentContainerTransform = CurrentTransform * _currentContainerTransform; - _currentTransform = Matrix.Identity; - return new PushedState(this, PushedState.PushedStateType.MatrixContainer); - } + protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); /// - /// Disposes of any resources held by the . + /// Pushes a matrix transformation. /// - public void Dispose() + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushTransform(Matrix matrix) { - if (_states is null || _transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - while (_states.Count != 0) - _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; - if (_transformContainers.Count != 0) - throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; - if (_ownsImpl) - PlatformImpl.Dispose(); + PushTransformCore(matrix); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform)); + return new PushedState(this); } + [Obsolete("Use PushTransform")] + public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); + + + protected abstract void PushTransformCore(Matrix matrix); + + protected abstract void PopClipCore(); + protected abstract void PopGeometryClipCore(); + protected abstract void PopOpacityCore(); + protected abstract void PopOpacityMaskCore(); + protected abstract void PopBitmapBlendModeCore(); + protected abstract void PopTransformCore(); + private static bool PenIsVisible(IPen? pen) { return pen?.Brush != null && pen.Thickness > 0; diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index b7abda2c61..812d315912 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,17 +67,16 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -103,7 +102,7 @@ namespace Avalonia.Media return rect; } - private class DrawingGroupDrawingContext : IDrawingContextImpl + private sealed class DrawingGroupDrawingContext : DrawingContext { private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); @@ -133,17 +132,7 @@ namespace Avalonia.Media _drawingGroup = drawingGroup; } - public Matrix Transform - { - get => _transform; - set - { - _transform = value; - PushTransform(new MatrixTransform(value)); - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if ((brush == null) && (pen == null)) { @@ -157,7 +146,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { if ((brush == null) && (pen == null)) { @@ -167,7 +156,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground == null) { @@ -177,124 +166,70 @@ namespace Avalonia.Media GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = new GlyphRun(glyphRun) + GlyphRun = glyphRun }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen? pen, Point p1, Point p2) - { - if (pen == null) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); - } - - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) - { - if ((brush == null) && (pen == null)) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); - } - - public void Clear(Color color) - { - throw new NotImplementedException(); - } - - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotImplementedException(); - } - - public void Custom(ICustomDrawOperation custom) - { - throw new NotImplementedException(); - } - - public object? GetFeature(Type t) => null; - - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + protected override void PushClipCore(RoundedRect rect) { throw new NotImplementedException(); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + protected override void PushClipCore(Rect rect) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + protected override void PushGeometryClipCore(Geometry clip) { throw new NotImplementedException(); } - public void PopClip() + protected override void PushOpacityCore(double opacity, Rect bounds) { throw new NotImplementedException(); } - public void PopGeometryClip() + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { throw new NotImplementedException(); } - public void PopOpacity() + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { throw new NotImplementedException(); } - public void PopOpacityMask() + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); } - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void DrawLineCore(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); - } - - public void PushClip(Rect clip) - { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - public void PushClip(RoundedRect clip) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); } - public void PushGeometryClip(IGeometryImpl clip) + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect); - public void PushOpacity(double opacity) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -364,22 +299,34 @@ namespace Avalonia.Media // Restore the previous value of the current drawing group _currentDrawingGroup = _previousDrawingGroupStack.Pop(); } - + /// /// PushTransform - /// Push a Transform which will apply to all drawing operations until the corresponding /// Pop. /// - /// The Transform to push. - private void PushTransform(Transform transform) + /// The transform to push. + protected override void PushTransformCore(Matrix matrix) { // Instantiate a new drawing group and set it as the _currentDrawingGroup var drawingGroup = PushNewDrawingGroup(); // Set the transform on the new DrawingGroup - drawingGroup.Transform = transform; + drawingGroup.Transform = new MatrixTransform(matrix); } + protected override void PopClipCore() => Pop(); + + protected override void PopGeometryClipCore() => Pop(); + + protected override void PopOpacityCore() => Pop(); + + protected override void PopOpacityMaskCore() => Pop(); + + protected override void PopBitmapBlendModeCore() => Pop(); + + protected override void PopTransformCore() => Pop(); + /// /// Creates a new DrawingGroup for a Push* call by setting the /// _currentDrawingGroup to a newly instantiated DrawingGroup, diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 1b22a1ee69..52fbd87db7 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -62,7 +62,7 @@ namespace Avalonia.Media -sourceRect.Y + destRect.Y - bounds.Y); using (context.PushClip(destRect)) - using (context.PushPreTransform(translate * scale)) + using (context.PushTransform(translate * scale)) { Drawing?.Draw(context); } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 3b63a98720..d4640390d7 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -877,7 +877,7 @@ namespace Avalonia.Media var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps)); + TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection)); var collapsedLine = line.Collapse(collapsingProperties); diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 732f1957d0..07fd2d56fa 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -12,6 +12,6 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap Source { get; } + IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs new file mode 100644 index 0000000000..df72dd1ace --- /dev/null +++ b/src/Avalonia.Base/Media/ISceneBrush.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface ISceneBrush : ITileBrush + { + ISceneBrushContent? CreateContent(); + } + + [NotClientImplementable] + public interface ISceneBrushContent : IImmutableBrush, IDisposable + { + ITileBrush Brush { get; } + Rect Rect { get; } + void Render(IDrawingContextImpl context, Matrix? transform); + internal bool UseScalableRasterization { get; } + } + + internal class ImmutableSceneBrush : ImmutableTileBrush + { + public ImmutableSceneBrush(ITileBrush source) : base(source) + { + } + } +} diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs deleted file mode 100644 index a7d3e4da10..0000000000 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - /// - /// Paints an area with an . - /// - [NotClientImplementable] - public interface IVisualBrush : ITileBrush - { - /// - /// Gets the visual to draw. - /// - Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 2f2a0fb627..718ebf1686 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -11,8 +11,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); static ImageBrush() { @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap source) + public ImageBrush(IBitmap? source) { Source = source; } @@ -38,7 +38,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap Source + public IBitmap? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 6577532891..c4720d772e 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { - context.PlatformImpl.DrawBitmap( + context.DrawBitmap( PlatformImpl, 1, sourceRect, diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 88e5e627ee..e77dd9d1ab 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget + public class RenderTargetBitmap : Bitmap, IDisposable { /// /// Initializes a new instance of the class. @@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging /// Renders a visual to the . /// /// The visual to render. - public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); + public void Render(Visual visual) + { + using (var ctx = CreateDrawingContext()) + ImmediateRenderer.Render(visual, ctx); + } /// /// Creates a platform-specific implementation for a . @@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging return factory.CreateRenderTargetBitmap(size, dpi); } - /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); - - bool IRenderTarget.IsCorrupted => false; + public DrawingContext CreateDrawingContext() + { + var platform = PlatformImpl.Item.CreateDrawingContext(); + platform.Clear(Colors.Transparent); + return new PlatformDrawingContext(platform); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7d9534c414..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } @@ -353,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index f9892bf60c..668a907fdf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// The bitmap interpolation mode. public ImmutableImageBrush( - IBitmap source, + IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap Source { get; } + public IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index 0b625080e3..0000000000 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media.Imaging; - -namespace Avalonia.Media.Immutable -{ - /// - /// Paints an area with an . - /// - internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush - { - /// - /// Initializes a new instance of the class. - /// - /// The visual to draw. - /// The horizontal alignment of a tile in the destination. - /// The vertical alignment of a tile in the destination. - /// The rectangle on the destination in which to paint a tile. - /// The opacity of the brush. - /// The transform of the brush. - /// The transform origin of the brush - /// The rectangle of the source image that will be displayed. - /// - /// How the source rectangle will be stretched to fill the destination rect. - /// - /// The tile mode. - /// Controls the quality of interpolation. - public ImmutableVisualBrush( - Visual visual, - AlignmentX alignmentX = AlignmentX.Center, - AlignmentY alignmentY = AlignmentY.Center, - RelativeRect? destinationRect = null, - double opacity = 1, - ImmutableTransform? transform = null, - RelativePoint transformOrigin = default, - RelativeRect? sourceRect = null, - Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) - : base( - alignmentX, - alignmentY, - destinationRect ?? RelativeRect.Fill, - opacity, - transform, - transformOrigin, - sourceRect ?? RelativeRect.Fill, - stretch, - tileMode, - bitmapInterpolationMode) - { - Visual = visual; - } - - /// - /// Initializes a new instance of the class. - /// - /// The brush from which this brush's properties should be copied. - public ImmutableVisualBrush(IVisualBrush source) - : base(source) - { - Visual = source.Visual; - } - - /// - public Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs new file mode 100644 index 0000000000..eb8a93722c --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport +{ + private readonly IDrawingContextImpl _impl; + private readonly bool _ownsImpl; + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _transforms; + + + public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true) + { + _impl = impl; + _ownsImpl = ownsImpl; + } + + protected override void DrawLineCore(IPen pen, Point p1, Point p2) => + _impl.DrawLine(pen, p1, p2); + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) => + _impl.DrawGeometry(brush, pen, geometry); + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default) => + _impl.DrawRectangle(brush, pen, rrect, boxShadows); + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); + + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + + public override void Custom(ICustomDrawOperation custom) => + custom.Render(_impl); + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + if (foreground != null) + _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + } + + protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); + + protected override void PushClipCore(Rect rect) => _impl.PushClip(rect); + + protected override void PushGeometryClipCore(Geometry clip) => + _impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException()); + + protected override void PushOpacityCore(double opacity, Rect bounds) => + _impl.PushOpacity(opacity, bounds); + + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) => + _impl.PushOpacityMask(mask, bounds); + + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) => + _impl.PushBitmapBlendMode(blendingMode); + + protected override void PushTransformCore(Matrix matrix) + { + _transforms ??= TransformStackPool.Get(); + var current = _impl.Transform; + _transforms.Push(current); + _impl.Transform = matrix * current; + } + + protected override void PopClipCore() => _impl.PopClip(); + + protected override void PopGeometryClipCore() => _impl.PopGeometryClip(); + + protected override void PopOpacityCore() => _impl.PopOpacity(); + + protected override void PopOpacityMaskCore() => _impl.PopOpacityMask(); + + protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode(); + + protected override void PopTransformCore() => + _impl.Transform = + (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); + + protected override void DisposeCore() + { + if (_ownsImpl) + _impl.Dispose(); + if (_transforms != null) + { + if (_transforms.Count != 0) + throw new InvalidOperationException("Not all states are disposed"); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } + } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport idc) + idc.DrawRectangle(material, rect); + else + DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect); + } +} diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 40ba613717..9b7bf3f74c 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -6,11 +6,13 @@ namespace Avalonia.Media { public readonly double Width; public readonly TextRunProperties TextRunProperties; + public readonly FlowDirection FlowDirection; - public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties) + public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; TextRunProperties = textRunProperties; + FlowDirection = flowDirection; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 0d85f3e7c5..c1b9b77401 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting return; } - if (lineImpl.NewLineLength > 0) - { - return; - } - - if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) - { - return; - } - var breakOportunities = new Queue(); var currentPosition = textLine.FirstTextSourceIndex; @@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting continue; } - var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 7f23ac98b4..568148e15c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting internal SplitResult Split(int length) { - if (IsReversed) + var isReversed = IsReversed; + + if (isReversed) { Reverse(); - } + length = Length - length; + } #if DEBUG - if(length == 0) + if (length == 0) { throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero."); } -#endif - +#endif var splitBuffer = ShapedBuffer.Split(length); var first = new ShapedTextRun(splitBuffer.First, Properties); - #if DEBUG +#if DEBUG if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - #endif - var second = new ShapedTextRun(splitBuffer.Second!, Properties); + if (isReversed) + { + return new SplitResult(second, first); + } + return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 72882df0b5..7cdf81ecc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -15,6 +17,11 @@ /// public abstract TextRun Symbol { get; } + /// + /// Gets the flow direction that is used for collapsing. + /// + public abstract FlowDirection FlowDirection { get; } + /// /// Collapses given text line. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 4c93a1d851..6422f23dcd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting @@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting var availableWidth = properties.Width - shapedSymbol.Size.Width; - while (runIndex < textRuns.Count) + if(properties.FlowDirection== FlowDirection.LeftToRight) { - var currentRun = textRuns[runIndex]; - - switch (currentRun) + while (runIndex < textRuns.Count) { - case ShapedTextRun shapedRun: - { - currentWidth += shapedRun.Size.Width; + var currentRun = textRuns[runIndex]; - if (currentWidth > availableWidth) + switch (currentRun) + { + case ShapedTextRun shapedRun: { - if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + currentWidth += shapedRun.Size.Width; + + if (currentWidth > availableWidth) { - if (isWordEllipsis && measuredLength < textLine.Length) + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + if (isWordEllipsis && measuredLength < textLine.Length) { - var nextBreakPosition = lineBreak.PositionMeasure; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - if (nextBreakPosition >= measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - break; + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } - collapsedLength += measuredLength; + availableWidth -= shapedRun.Size.Width; - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } - availableWidth -= shapedRun.Size.Width; + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + } - break; - } + availableWidth -= drawableRun.Size.Width; - case DrawableTextRun drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) - { - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } + } - availableWidth -= drawableRun.Size.Width; + collapsedLength += currentRun.Length; - break; - } + runIndex++; } + } + else + { + runIndex = textRuns.Count - 1; + + while (runIndex >= 0) + { + var currentRun = textRuns[runIndex]; - collapsedLength += currentRun.Length; + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + currentWidth += shapedRun.Size.Width; - runIndex++; - } + if (currentWidth > availableWidth) + { + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + { + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + { + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= shapedRun.Size.Width; + + break; + } + + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= drawableRun.Size.Width; + + break; + } + } + + collapsedLength += currentRun.Length; + + runIndex--; + } + } + return null; } - private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, - TextRun shapedSymbol) + private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, + FlowDirection flowDirection, TextRun shapedSymbol) { + var textRuns = textLine.TextRuns; + if (collapsedLength <= 0) { return new[] { shapedSymbol }; } + if(flowDirection == FlowDirection.RightToLeft) + { + collapsedLength = textLine.Length - collapsedLength; + } + var objectPool = FormattingObjectPool.Instance; var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); try { - var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - return collapsedRuns; + if (flowDirection == FlowDirection.RightToLeft) + { + var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; + postSplitRuns.CopyTo(collapsedRuns, 1); + collapsedRuns[0] = shapedSymbol; + return collapsedRuns; + } + else + { + var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } finally { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4dbc472133..a382416b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting var lastTrailingIndex = 0; - if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; @@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting { lastTrailingIndex += textEndOfLine.Length; } - } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var whitespaceWidth = 0d; + var justificationWidth = MaxWidth; - for (var i = 0; i < textLines.Count; i++) + if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - var line = textLines[i]; - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - - if (lineWhitespaceWidth > whitespaceWidth) - { - whitespaceWidth = lineWhitespaceWidth; - } + justificationWidth = width; } - var justificationWidth = width - whitespaceWidth; - if (justificationWidth > 0) { var justificationProperties = new InterWordJustification(justificationWidth); - for (var i = 0; i < textLines.Count - 1; i++) + for (var i = 0; i < textLines.Count; i++) { var line = textLines[i]; @@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting /// The . private TextCollapsingProperties? GetCollapsingProperties(double width) { - if(_textTrimming == TextTrimming.None) + if (_textTrimming == TextTrimming.None) { return null; } - return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); + return _textTrimming.CreateCollapsingProperties( + new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection)); } public void Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 2e85b1e187..a21a5d45e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting /// Length of leading prefix. /// width in which collapsing is constrained to /// text run properties of ellipsis symbol + /// the flow direction of the collapes line. public TextLeadingPrefixCharacterEllipsis( string ellipsis, int prefixLength, double width, - TextRunProperties textRunProperties) + TextRunProperties textRunProperties, + FlowDirection flowDirection) { if (_prefixLength < 0) { @@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting _prefixLength = prefixLength; Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 187b3154ad..b3321d4d9f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting currentX += drawableTextRun.Size.Width; } - if(lastRunIndex - 1 < 0) + if (lastRunIndex - 1 < 0) { break; } @@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting directionalWidth -= drawableTextRun.Size.Width; } - if(firstRunIndex + 1 == _textRuns.Length) + if (firstRunIndex + 1 == _textRuns.Length) { break; } @@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override void Dispose() @@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting } } - if (index == lastRunIndex) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength += textRun.GlyphRun.Metrics.NewLineLength; - } - widthIncludingWhitespace += textRun.Size.Width; break; @@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - } - if (drawableTextRun.Size.Height > height) { height = drawableTextRun.Size.Height; @@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting } } + width = widthIncludingWhitespace; + + for (var i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextRun shapedText) + { + var glyphRun = shapedText.GlyphRun; + var glyphRunMetrics = glyphRun.Metrics; + + newLineLength += glyphRunMetrics.NewLineLength; + + if (glyphRunMetrics.TrailingWhitespaceLength == 0) + { + break; + } + + trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; + + var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + + width -= whitespaceWidth; + } + } + var start = GetParagraphOffsetX(width, widthIncludingWhitespace); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) @@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, start); case TextAlignment.Right: - return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + return Math.Max(0, _paragraphWidth - width); default: return 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index ccae99cc75..8a6607bce2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -12,10 +12,13 @@ /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) + /// The flow direction of the collapsed line. + public TextTrailingCharacterEllipsis(string ellipsis, double width, + TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -24,6 +27,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c622c76a60..5252766382 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -12,14 +12,17 @@ /// Text used as collapsing symbol. /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. + /// flow direction of the collapsed line. public TextTrailingWordEllipsis( string ellipsis, double width, - TextRunProperties textRunProperties + TextRunProperties textRunProperties, + FlowDirection flowDirection ) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -28,6 +31,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 7ba25eb005..19e6a70357 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) { - return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 2edbaabbc6..8a3c5aa397 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -17,10 +17,10 @@ namespace Avalonia.Media { if (_isWordBased) { - return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } - return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 2be3e9a94e..6bfe20271f 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,11 +1,14 @@ using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush + public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender { /// /// Defines the property. @@ -43,10 +46,23 @@ namespace Avalonia.Media set { SetValue(VisualProperty, value); } } - /// - IImmutableBrush IMutableBrush.ToImmutable() + ISceneBrushContent? ISceneBrush.CreateContent() { - return new ImmutableVisualBrush(this); + if (Visual == null) + return null; + + if (Visual is IVisualBrushInitialize initialize) + initialize.EnsureInitialized(); + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + new(Visual.Bounds.Size), false); } } } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8509067cd0..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 5826cfb2ff..d1964bf07e 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Metadata; @@ -47,7 +48,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(IPen pen, Point point); + bool StrokeContains(IPen? pen, Point point); /// /// Makes a clone of the geometry with the specified transform. @@ -87,6 +88,7 @@ namespace Avalonia.Platform /// If ture, the resulting snipped path will start with a BeginFigure call. /// The resulting snipped path. /// If the snipping operation is successful. - bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry); } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 73e9e58da4..31ad84341d 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -14,11 +14,7 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// - /// A render to be used to render visual brushes. May be null if no visual brushes are - /// to be drawn. - /// - IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); + IDrawingContextImpl CreateDrawingContext(); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5bf9ff9d9a..543fb0ab74 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFile : IStorageBookmarkFile { - public BclStorageFile(string fileName) - { - FileInfo = new FileInfo(fileName); - } - public BclStorageFile(FileInfo fileInfo) { FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 1e21c197bb..d8e3d91f75 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFolder : IStorageBookmarkFolder { - public BclStorageFolder(string path) - { - DirectoryInfo = new DirectoryInfo(path); - if (!DirectoryInfo.Exists) - { - throw new ArgumentException("Directory must exist"); - } - } - public BclStorageFolder(DirectoryInfo directoryInfo) { DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 55e84ee937..a8cbffb417 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO; internal static class StorageProviderHelpers { + public static IStorageItem? TryCreateBclStorageItem(string path) + { + var directory = new DirectoryInfo(path); + if (directory.Exists) + { + return new BclStorageFolder(directory); + } + + var file = new FileInfo(path); + if (file.Exists) + { + return new BclStorageFile(file); + } + + return null; + } + public static Uri FilePathToUri(string path) { var uriPath = new StringBuilder(path) diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs index 6f97916a26..ed061aa2d5 100644 --- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -12,6 +12,8 @@ public class PickerOptions /// /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// Can be obtained from previously picked folder or using + /// or . /// public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index 6f8b945cd6..1febb4506a 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -11,12 +11,24 @@ public static class StorageProviderExtensions /// public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); + } + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } /// public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); + } + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index e1ff0970c2..a841803ee1 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,27 +16,37 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; - private TValue? _defaultValue; - private bool _isDefaultValueInitialized; + private UncommonFields? _uncommon; protected BindingEntryBase( + AvaloniaObject target, ValueFrame frame, AvaloniaProperty property, IObservable> source) + : this(target, frame, property, (object)source) { - Frame = frame; - Source = source; - Property = property; } protected BindingEntryBase( + AvaloniaObject target, ValueFrame frame, AvaloniaProperty property, IObservable source) + : this(target, frame, property, (object)source) + { + } + + private BindingEntryBase( + AvaloniaObject target, + ValueFrame frame, + AvaloniaProperty property, + object source) { Frame = frame; - Source = source; Property = property; + Source = source; + if (property.GetMetadata(target.GetType()).EnableDataValidation == true) + _uncommon = new() { _hasDataValidation = true }; } public bool HasValue @@ -68,6 +78,20 @@ namespace Avalonia.PropertyStore return _value!; } + public bool GetDataValidationState(out BindingValueType state, out Exception? error) + { + if (_uncommon?._hasDataValidation == true) + { + state = _uncommon._dataValidationState; + error = _uncommon._dataValidationError; + return true; + } + + state = BindingValueType.Value; + error = null; + return false; + } + public void Start() => Start(true); public void OnCompleted() => BindingCompleted(); @@ -111,16 +135,28 @@ namespace Avalonia.PropertyStore { static void Execute(BindingEntryBase instance, BindingValue value) { - if (instance.Frame.Owner is null) + if (instance.Frame.Owner is not { } valueStore) return; - LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); + var owner = valueStore.Owner; + var property = instance.Property; + var originalType = value.Type; + + LoggingUtils.LogIfNecessary(owner, property, value); - var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + if (!value.HasValue && value.Type != BindingValueType.DataValidationError) + value = value.WithValue(instance.GetCachedDefaultValue()); - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) + if (instance._uncommon?._hasDataValidation == true) { - instance._value = effectiveValue; + instance._uncommon._dataValidationState = value.Type; + instance._uncommon._dataValidationError = value.Error; + } + + if (value.HasValue && + (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value))) + { + instance._value = value.Value; instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); @@ -152,13 +188,23 @@ namespace Avalonia.PropertyStore private TValue GetCachedDefaultValue() { - if (!_isDefaultValueInitialized) + if (_uncommon?._isDefaultValueInitialized != true) { - _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); - _isDefaultValueInitialized = true; + _uncommon ??= new(); + _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _uncommon._isDefaultValueInitialized = true; } - return _defaultValue!; + return _uncommon._defaultValue!; + } + + private class UncommonFields + { + public TValue? _defaultValue; + public bool _isDefaultValueInitialized; + public bool _hasDataValidation; + public BindingValueType _dataValidationState; + public Exception? _dataValidationError; } } } diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs index cbe2435953..4bf98e3f7b 100644 --- a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs @@ -9,11 +9,13 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; public DirectBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; Property = property; } @@ -33,10 +35,17 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; + OnCompleted(); + } + + public void OnCompleted() + { _owner.OnLocalValueBindingCompleted(Property, this); + + if (_hasDataValidation) + _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); } - public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); public void OnError(Exception error) => OnCompleted(); public void OnNext(T value) diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs index 5d60b44bef..1cf108df9b 100644 --- a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs @@ -10,11 +10,13 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; Property = property; } @@ -30,6 +32,9 @@ namespace Avalonia.PropertyStore _subscription?.Dispose(); _subscription = null; _owner.OnLocalValueBindingCompleted(Property, this); + + if (_hasDataValidation) + _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); } public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 78f0ad46b7..11a4dd7893 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore /// internal abstract class EffectiveValue { - private IValueEntry? _valueEntry; - private IValueEntry? _baseValueEntry; - /// /// Gets the current effective value as a boxed value. /// @@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets the active value entry for the current effective value. + /// + public IValueEntry? ValueEntry { get; private set; } + + /// + /// Gets the active value entry for the current base value. + /// + public IValueEntry? BaseValueEntry { get; private set; } + /// /// Gets a value indicating whether the was overridden by a call to /// . @@ -63,14 +70,14 @@ namespace Avalonia.PropertyStore { if (Priority == BindingPriority.Unset) { - _valueEntry?.Unsubscribe(); - _valueEntry = null; + ValueEntry?.Unsubscribe(); + ValueEntry = null; } if (BasePriority == BindingPriority.Unset) { - _baseValueEntry?.Unsubscribe(); - _baseValueEntry = null; + BaseValueEntry?.Unsubscribe(); + BaseValueEntry = null; } } @@ -135,40 +142,34 @@ namespace Avalonia.PropertyStore // value, then the current entry becomes our base entry. if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited) { - Debug.Assert(_valueEntry is not null); - _baseValueEntry = _valueEntry; - _valueEntry = null; + Debug.Assert(ValueEntry is not null); + BaseValueEntry = ValueEntry; + ValueEntry = null; } - if (_valueEntry != entry) + if (ValueEntry != entry) { - _valueEntry?.Unsubscribe(); - _valueEntry = entry; + ValueEntry?.Unsubscribe(); + ValueEntry = entry; } } else if (Priority <= BindingPriority.Animation) { // We've received a non-animation value and have an active animation value, so the // new entry becomes our base entry. - if (_baseValueEntry != entry) + if (BaseValueEntry != entry) { - _baseValueEntry?.Unsubscribe(); - _baseValueEntry = entry; + BaseValueEntry?.Unsubscribe(); + BaseValueEntry = entry; } } - else if (_valueEntry != entry) + else if (ValueEntry != entry) { // Both the current value and the new value are non-animation values, so the new // entry replaces the existing entry. - _valueEntry?.Unsubscribe(); - _valueEntry = entry; + ValueEntry?.Unsubscribe(); + ValueEntry = entry; } } - - protected void UnsubscribeValueEntries() - { - _valueEntry?.Unsubscribe(); - _baseValueEntry?.Unsubscribe(); - } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index c469034f9b..0788b39459 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; +using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -61,6 +62,12 @@ namespace Avalonia.PropertyStore UpdateValueEntry(value, priority); SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); + + if (priority > BindingPriority.LocalValue && + value.GetDataValidationState(out var state, out var error)) + { + owner.Owner.OnUpdateDataValidation(value.Property, state, error); + } } public void SetLocalValueAndRaise( @@ -128,12 +135,10 @@ namespace Avalonia.PropertyStore public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) { - UnsubscribeValueEntries(); - DisposeAndRaiseUnset(owner, (StyledProperty)property); - } + ValueEntry?.Unsubscribe(); + BaseValueEntry?.Unsubscribe(); - public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property) - { + var p = (StyledProperty)property; BindingPriority priority; T oldValue; @@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore if (!EqualityComparer.Default.Equals(oldValue, Value)) { - owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true); + owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true); if (property.Inherits) - owner.OnInheritedEffectiveValueDisposed(property, Value); + owner.OnInheritedEffectiveValueDisposed(p, Value); + } + + if (ValueEntry?.GetDataValidationState(out _, out _) ?? + BaseValueEntry?.GetDataValidationState(out _, out _) ?? + false) + { + owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null); } } diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs index 271d85f8bc..5898bef491 100644 --- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; namespace Avalonia.PropertyStore { @@ -22,6 +23,16 @@ namespace Avalonia.PropertyStore /// object? GetValue(); + /// + /// Gets the data validation state if supported. + /// + /// The binding validation state. + /// The current binding error, if any. + /// + /// True if the entry supports data validation, otherwise false. + /// + bool GetDataValidationState(out BindingValueType state, out Exception? error); + /// /// Called when the value entry is removed from the value store. /// diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs index d8a353dc70..16b96eff5d 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; namespace Avalonia.PropertyStore { @@ -27,5 +28,12 @@ namespace Avalonia.PropertyStore object? IValueEntry.GetValue() => _value; T IValueEntry.GetValue() => _value; + + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } } } diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 7e9f3ab312..222d857aa3 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable> source) { - var e = new TypedBindingEntry(this, property, source); + var e = new TypedBindingEntry(Owner!.Owner, this, property, source); Add(e); return e; } @@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable source) { - var e = new TypedBindingEntry(this, property, source); + var e = new TypedBindingEntry(Owner!.Owner, this, property, source); Add(e); return e; } @@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable 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..01299e4ffa 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; 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 b75d080cfd..f81cc5a1a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -7,7 +8,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.VisualTree; +using Avalonia.Threading; // Special license applies License.md @@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition; /// /// An IDrawingContextImpl implementation that builds /// -internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport { private CompositionDrawListBuilder _builder = new(); private int _drawOperationIndex; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public Matrix Transform { get; set; } = Matrix.Identity; + private Stack? _transforms; - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } + private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } + private Stack? _needsToPopOpacityMask; + public Matrix Transform { get; set; } = Matrix.Identity; + public void BeginUpdate(CompositionDrawList? list) { _builder.Reset(list); _drawOperationIndex = 0; } - public CompositionDrawList EndUpdate() + public CompositionDrawList? EndUpdate() { + // Make sure that any pending pop operations are completed + Dispose(); + _builder.TrimTo(_drawOperationIndex); - return _builder.DrawOperations!; + return _builder.DrawOperations; } + + protected override void DisposeCore() + { + if (_transforms != null) + { + _transforms.Clear(); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + if (_needsToPopOpacityMask != null) + { + _needsToPopOpacityMask.Clear(); + _needsToPopOpacityMask = null; + } + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry)); } else { @@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { var next = NextDrawAs(); @@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen? pen, Point p1, Point p2) + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) { if (pen is null) { @@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + Add(new LineNode(Transform, pen, p1, p2)); } else { @@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows)); } else { @@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect)); } else { ++_drawOperationIndex; } } - - public void Custom(ICustomDrawOperation custom) + + public override void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, custom)) @@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } - public object? GetFeature(Type t) => null; - - /// - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground is null) { @@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl)); } else @@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushTransformCore(Matrix matrix) { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + _transforms ??= TransformStackPool.Get(); + _transforms.Push(Transform); + Transform = matrix * Transform; } + + protected override void PopTransformCore() => + Transform = (_transforms ?? throw new InvalidOperationException()).Pop(); - /// - public void PopClip() + protected override void PopClipCore() { var next = NextDrawAs(); @@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PopGeometryClip() + protected override void PopGeometryClipCore() { var next = NextDrawAs(); @@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopBitmapBlendMode() + protected override void PopBitmapBlendModeCore() { var next = NextDrawAs(); @@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacity() + protected override void PopOpacityCore() { var next = NextDrawAs(); @@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacityMask() + protected override void PopOpacityMaskCore() { + if (!_needsToPopOpacityMask!.Pop()) + return; + var next = NextDrawAs(); if (next == null || !next.Item.Equals(null, null)) { - Add(new OpacityMaskNode()); + Add(new OpacityMaskPopNode()); } else { @@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(Rect clip) + + protected override void PushClipCore(Rect clip) { var next = NextDrawAs(); @@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(RoundedRect clip) + protected override void PushClipCore(RoundedRect clip) { var next = NextDrawAs(); @@ -294,32 +301,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushGeometryClip(IGeometryImpl? clip) + protected override void PushGeometryClipCore(Geometry clip) { - if (clip is null) + if (clip.PlatformImpl is null) return; var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, clip)) + if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl)) { - Add(new GeometryClipNode(Transform, clip)); + Add(new GeometryClipNode(Transform, clip.PlatformImpl)); } else { ++_drawOperationIndex; } } - - /// - public void PushOpacity(double opacity) + + protected override void PushOpacityCore(double opacity, Rect bounds) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity, bounds)) { - Add(new OpacityNode(opacity)); + Add(new OpacityNode(opacity, bounds)); } else { @@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushOpacityMask(IBrush mask, Rect bounds) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { var next = NextDrawAs(); + bool needsToPop = true; if (next == null || !next.Item.Equals(mask, bounds)) { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + var immutableMask = ConvertBrush(mask); + if (immutableMask != null) + Add(new OpacityMaskNode(immutableMask, bounds)); + else + needsToPop = false; } else { ++_drawOperationIndex; } + + _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get(); + _needsToPopOpacityMask.Push(needsToPop); } /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { var next = NextDrawAs(); @@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW : null; } - private static IDisposable? CreateChildScene(IBrush? brush) + private IImmutableBrush? ConvertBrush(IBrush? brush) { - if (brush is VisualBrush visualBrush) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer - // We should directly reference the corresponding CompositionVisual (which should - // be attached to the same composition target) like UWP does. - // Render-able visuals shouldn't be dangling unattached - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - - var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(null); - ImmediateRenderer.Render(visual, new DrawingContext(recorder)); - var drawList = recorder.EndUpdate(); - drawList.Size = visual.Bounds.Size; - - return drawList; - } - } - return null; + if (brush is IMutableBrush mutable) + return mutable.ToImmutable(); + if (brush is ISceneBrush sceneBrush) + return sceneBrush.CreateContent(); + return (IImmutableBrush?)brush; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 50df8bd32b..eaa9a70ca0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDrawingContextImpl _impl; - private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) + public CompositorDrawingContextProxy(IDrawingContextImpl impl) { _impl = impl; - _visualBrushRenderer = visualBrushRenderer; - } - - // This is a hack to make it work with the current way of handling visual brushes - public CompositionDrawList? VisualBrushDrawList - { - get => _visualBrushRenderer.VisualBrushDrawList; - set => _visualBrushRenderer.VisualBrushDrawList = value; } public Matrix PostTransform { get; set; } = Matrix.Identity; @@ -111,9 +102,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopClip(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { - _impl.PushOpacity(opacity); + _impl.PushOpacity(opacity, bounds); } public void PopOpacity() @@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont } public object? GetFeature(Type t) => _impl.GetFeature(t); - - public class VisualBrushRenderer : IVisualBrushRenderer - { - public CompositionDrawList? VisualBrushDrawList { get; set; } - public Size GetRenderTargetSize(IVisualBrush brush) - { - return VisualBrushDrawList?.Size ?? default; - } - - public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (VisualBrushDrawList != null) - { - foreach (var cmd in VisualBrushDrawList) - cmd.Item.Render(context); - } - } - } + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 63ec8d756b..977acd8470 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server Readback.CompleteWrite(Revision); _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext(null)) + using (var targetContext = _renderTarget.CreateDrawingContext()) { var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) @@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsDefault) { - var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); - using (var context = _layer.CreateDrawingContext(visualBrushHelper)) + using (var context = _layer.CreateDrawingContext()) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); + Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,27 +41,29 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix @@ -74,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index f5312ad39b..0000000000 --- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - [Unstable] - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 8e5dc38317..4a12e78817 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -14,19 +14,8 @@ namespace Avalonia.Rendering /// a simple tree traversal. /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer + internal class ImmediateRenderer { - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(Visual visual, IRenderTarget target) - { - using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); - Render(context, visual, visual.Bounds); - } - /// /// Renders a visual to a drawing context. /// @@ -36,28 +25,6 @@ namespace Avalonia.Rendering { Render(context, visual, visual.Bounds); } - - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? default; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (brush.Visual is { } visual) - { - Render(new DrawingContext(context), visual, visual.Bounds); - } - } - - internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) - { - Render(context, visual, visual.Bounds); - } private static Rect GetTransformedBounds(Visual visual) { @@ -75,7 +42,7 @@ namespace Avalonia.Rendering } - private static void Render(DrawingContext context, Visual visual, Rect clipRect) + public static void Render(DrawingContext context, Visual visual, Rect clipRect) { var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; @@ -117,7 +84,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index e81966ce81..62fc73db44 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : DrawOperation + internal abstract class BrushDrawOperation : DrawOperationWithTransform { - public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) + public IImmutableBrush? Brush { get; } + + public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush) : base(bounds, transform) { - Aux = aux; + Brush = brush; } - /// - /// Auxiliary data required to draw the brush - /// - public IDisposable? Aux { get; } - public override void Dispose() { - Aux?.Dispose(); + (Brush as ISceneBrushContent)?.Dispose(); base.Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index e1bfaa4aa3..782e287989 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a clip push or pop. /// - internal class ClipNode : IDrawOperation + internal class ClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index b7311936d3..ff2616bfe4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -4,30 +4,19 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperation + internal sealed class CustomDrawOperation : DrawOperationWithTransform { - public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) : base(custom.Bounds, transform) { - Transform = transform; Custom = custom; } - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - return Custom.HitTest(p * Transform.Invert()); - } - - return false; - } + public override bool HitTest(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; Custom.Render(context); } @@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph Transform == transform && Custom?.Equals(custom) == true; } - public interface ICustomDrawOperation : IDrawOperation, IEquatable + public interface ICustomDrawOperation : IEquatable, IDisposable { - + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index c49e7705e0..5b93cd8cfc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph { } } + + internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform + { + protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform) + { + Transform = transform; + } + + public Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index 4600653b9d..d5f0270cb2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph { public EllipseNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - Rect rect, - IDisposable? aux = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) + Rect rect) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; } - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// public ImmutablePen? Pen { get; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the rect of the ellipse to draw. /// @@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph rect.Equals(Rect); } - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawEllipse(Brush, Pen, Rect); - } + public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); public override bool HitTest(Point p) { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - var center = Rect.Center; var strokeThickness = Pen?.Thickness ?? 0; @@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph return false; } + + public override void Dispose() + { + (Brush as ISceneBrushContent)?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 82f8fc2d56..e1f79e0e10 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a rectangle draw. /// - internal class ExperimentalAcrylicNode : DrawOperation + internal class ExperimentalAcrylicNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect) : base(rect.Rect, transform) { - Transform = transform; Material = material.ToImmutable(); Rect = rect; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - public IExperimentalAcrylicMaterial Material { get; } /// @@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if(context is IDrawingContextWithAcrylicLikeSupport idc) { idc.DrawRectangle(Material, Rect); @@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) - { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) - { - p *= Transform.Invert(); - - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } - - return false; - } + public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 842edf2bcb..8575e61de4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a geometry clip push or pop. /// - internal class GeometryClipNode : IDrawOperation + internal class GeometryClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index cf53b86fa7..3ab535897a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph /// The geometry. /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - IGeometryImpl geometry, - IDisposable? aux) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) + IGeometryImpl geometry) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } /// public override bool HitTest(Point p) { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index a2d914bdd7..4d8759f545 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, - IBrush foreground, - IRef glyphRun, - IDisposable? aux = null) - : base(new Rect(glyphRun.Item.Size), transform, aux) + IImmutableBrush foreground, + IRef glyphRun) + : base(new Rect(glyphRun.Item.Size), transform, foreground) { - Transform = transform; - Foreground = foreground.ToImmutable(); GlyphRun = glyphRun.Clone(); } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the foreground brush. - /// - public IBrush Foreground { get; } - + + /// /// Gets the glyph run to draw. /// public IRef GlyphRun { get; } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun); - } + public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); /// /// Determines if this draw operation equals another. @@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && - Equals(foreground, Foreground) && + Equals(foreground, Brush) && Equals(glyphRun.Item, GlyphRun.Item); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); public override void Dispose() { GlyphRun?.Dispose(); + base.Dispose(); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 2bfd2080c3..6a1aefe6b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation : IDisposable + internal interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. @@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph /// The drawing context. void Render(IDrawingContextImpl context); } + + internal interface IDrawOperationWithTransform : IDrawOperation + { + /// + /// Gets the transform with which the node will be drawn. + /// + Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 339881e675..dd9787e8d1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : DrawOperation + internal class ImageNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform) { - Transform = transform; Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } + } /// /// Gets the image to draw. @@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BitmapInterpolationMode BitmapInterpolationMode { get; } - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - /// /// Determines if this draw operation equals another. /// @@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 0af8ba2752..f21791d038 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a line draw. /// - internal class LineNode : BrushDrawOperation + internal class LineNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IPen pen, Point p1, - Point p2, - IDisposable? aux = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) + Point p2) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { - Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the stroke pen. /// @@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawLine(Pen, P1, P2); } public override bool HitTest(Point p) { - if (!Transform.HasInverse) - return false; - - p *= Transform.Invert(); - var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 3ecc07fa54..e10d712c2d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Auxiliary data required to draw the brush. - public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(default, Matrix.Identity, aux) + public OpacityMaskNode(IImmutableBrush mask, Rect bounds) + : base(default, Matrix.Identity, mask) { - Mask = mask.ToImmutable(); MaskBounds = bounds; } - /// - /// Initializes a new instance of the class that represents an - /// opacity mask pop. - /// - public OpacityMaskNode() - : base(default, Matrix.Identity, null) - { - } - - /// - /// Gets the mask to be pushed or null if the operation represents a pop. - /// - public IBrush? Mask { get; } - /// /// Gets the bounds of the opacity mask or null if the operation represents a pop. /// @@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; + public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; /// public override void Render(IDrawingContextImpl context) { - if (Mask != null) - { - context.PushOpacityMask(Mask, MaskBounds!.Value); - } - else - { - context.PopOpacityMask(); - } + context.PushOpacityMask(Brush!, MaskBounds!.Value); } } + + internal class OpacityMaskPopNode : DrawOperation + { + public OpacityMaskPopNode() : base(default, Matrix.Identity) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) => context.PopOpacityMask(); + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index f2ffd7411c..cee9ce9df7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows, - IDisposable? aux = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) + BoxShadows boxShadows) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; BoxShadows = boxShadows; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - - context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - } + public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// public override bool HitTest(Point p) { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) + if (Brush != null) { - p *= Transform.Invert(); - - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); - return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); - } + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); + return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); } - - return false; } } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 2cdb973174..82e948eea8 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -28,7 +28,7 @@ namespace Avalonia public class StyledElement : Animatable, IDataContextProvider, ILogical, - IResourceHost, + IThemeVariantHost, IStyleHost, IStyleable, ISetLogicalParent, @@ -46,6 +46,7 @@ namespace Avalonia defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, + enableDataValidation: false, notifying: DataContextNotifying); /// @@ -75,23 +76,6 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); - - /// - /// Defines the RequestedThemeVariant property. - /// - public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - defaultValue: ThemeVariant.Default); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -160,6 +144,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -278,15 +265,6 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } - /// - /// Gets the UI theme that is currently used by the element, which might be different than the . - /// - /// - /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. - /// Otherwise, current OS theme variant is returned. - /// - public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); - /// /// Gets the styled element's logical children. /// @@ -325,6 +303,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -394,7 +375,7 @@ namespace Avalonia /// public bool ApplyStyling() { - if (_initCount == 0 && (!_stylesApplied || !_themeApplied)) + if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); @@ -524,13 +505,7 @@ namespace Avalonia NotifyResourcesChanged(); } -#nullable disable - RaisePropertyChanged( - ParentProperty, - new Optional(old), - new BindingValue(Parent), - BindingPriority.LocalValue); -#nullable enable + RaisePropertyChanged(ParentProperty, old, Parent); } } @@ -650,13 +625,19 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); - else if (change.Property == RequestedThemeVariantProperty) + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) { if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ActualThemeVariantProperty, themeVariant); + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); else - ClearValue(ActualThemeVariantProperty); + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index c71973fde8..6f10de3651 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -16,11 +16,13 @@ namespace Avalonia /// The default value of the property. /// The default binding mode. /// A value coercion callback. + /// Whether the property is interested in data validation. public StyledPropertyMetadata( Optional defaultValue = default, BindingMode defaultBindingMode = BindingMode.Default, - Func? coerce = null) - : base(defaultBindingMode) + Func? coerce = null, + bool enableDataValidation = false) + : base(defaultBindingMode, enableDataValidation) { _defaultValue = defaultValue; CoerceValue = coerce; diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 33d4cd0824..8bdcec2e53 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; + private int _index = -1; public NthChildActivator( ILogical control, @@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators protected override bool EvaluateIsActive() { - return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + var index = _index >= 0 ? _index : _provider.GetChildIndex(_control); + return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch; } - protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged; - protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged; + protected override void Initialize() + { + _provider.ChildIndexChanged += ChildIndexChanged; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChanged; + } private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { // Run matching again if: - // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. - // 2. e.Child is null, when all children indices were changed. - // 3. Subscribed child index was changed. - if (_reversed - || e.Child is null - || e.Child == _control) + // 1. Subscribed child index was changed + // 2. Child indexes were reset + // 3. We're a reversed (nth-last-child) selector and total count has changed + if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) || + (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged)) { + // We're using the _index field to pass the index of the child to EvaluateIsActive + // *only* when the active state is re-evaluated via this event handler. The docs + // for EvaluateIsActive say: + // + // > This method should read directly from its inputs and not rely on any + // > subscriptions to fire in order to be up-to-date. + // + // Which is good advice in general, however in this case we need to break the rule + // and use the value from the event subscription instead of calling + // IChildIndexProvider.GetChildIndex. This is because this event can be fired during + // the process of realizing an element of a virtualized list; in this case calling + // GetChildIndex may not return the correct index as the element isn't yet realized. + _index = e.Index; ReevaluateIsActive(); + _index = -1; } } + + private void TotalCountChanged(object? sender, EventArgs e) + { + if (_reversed) + ReevaluateIsActive(); + } } } diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs deleted file mode 100644 index 2467d99b3b..0000000000 --- a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Metadata; - -namespace Avalonia.Styling; - -/// -/// Interface for an application host element with a root theme variant. -/// -[Unstable] -public interface IGlobalThemeVariantProvider : IResourceHost -{ - /// - /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. - /// - ThemeVariant ActualThemeVariant { get; } - - /// - /// Raised when the theme variant is changed on the element or an ancestor of the element. - /// - event EventHandler? ActualThemeVariantChanged; -} diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs new file mode 100644 index 0000000000..01583148a8 --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for the host element with a theme variant. +/// +[Unstable] +public interface IThemeVariantHost : IResourceHost +{ + /// + /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty. + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index ccfc2c781d..532179bb2c 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -61,7 +61,7 @@ namespace Avalonia.Styling { return subscribe ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) - : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + : Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed); } else { @@ -70,10 +70,9 @@ namespace Avalonia.Styling } internal static SelectorMatch Evaluate( - ILogical logical, IChildIndexProvider childIndexProvider, + int index, IChildIndexProvider childIndexProvider, int step, int offset, bool reversed) { - var index = childIndexProvider.GetChildIndex(logical); if (index < 0) { return SelectorMatch.NeverThisInstance; diff --git a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs index 826b45582d..be5a999771 100644 --- a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs @@ -15,7 +15,7 @@ namespace Avalonia.Styling AvaloniaProperty property, BindingMode mode, IObservable source) - : base(instance, property, source) + : base(target, instance, property, source) { _target = target; _mode = mode; diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs index 7a39407ba2..7604c26244 100644 --- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -19,6 +20,13 @@ namespace Avalonia.Styling public object? GetValue() => _value ??= _template.Build(); + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } + void IValueEntry.Unsubscribe() { } } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index 093597c6a0..9b009be6d2 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -90,6 +90,13 @@ namespace Avalonia.Styling object? IValueEntry.GetValue() => Value; + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } + private AvaloniaProperty EnsureProperty() { return Property ?? throw new InvalidOperationException("Setter.Property must be set."); @@ -99,7 +106,8 @@ namespace Avalonia.Styling { if (!Property!.IsDirect) { - var i = binding.Initiate(target, Property)!; + var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false; + var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!; var mode = i.Mode; if (mode == BindingMode.Default) diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 8218533f4f..389136b0f5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -6,11 +6,26 @@ using Avalonia.Platform; namespace Avalonia.Styling; /// -/// Specifies a UI theme variant that should be used for the +/// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] public sealed record ThemeVariant { + /// + /// Defines the ActualThemeVariant property. + /// + internal static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + "ActualThemeVariant", + inherits: true); + + /// + /// Defines the RequestedThemeVariant property. + /// + internal static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + "RequestedThemeVariant", defaultValue: Default); + /// /// Creates a new instance of the /// diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 827a02334a..30b7738409 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Avalonia.Threading { - public class ThreadSafeObjectPool where T : class, new() + internal class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); @@ -17,11 +17,14 @@ namespace Avalonia.Threading } } - public void Return(T obj) + public void ReturnAndSetNull(ref T? obj) { + if (obj == null) + return; lock (_stack) { _stack.Push(obj); + obj = null; } } } diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs index 6da899bab2..2f62564e0e 100644 --- a/src/Avalonia.Base/Utilities/WeakEvents.cs +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Input; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -20,15 +21,30 @@ public class WeakEvents }); /// - /// Represents PropertyChanged event from + /// Represents PropertyChanged event from with auto-dispatching to the UI thread /// public static readonly WeakEvent - PropertyChanged = WeakEvent.Register( + ThreadSafePropertyChanged = WeakEvent.Register( (s, h) => { - PropertyChangedEventHandler handler = (_, e) => h(s, e); + bool unsubscribed = false; + PropertyChangedEventHandler handler = (_, e) => + { + if (Dispatcher.UIThread.CheckAccess()) + h(s, e); + else + Dispatcher.UIThread.Post(() => + { + if (!unsubscribed) + h(s, e); + }); + }; s.PropertyChanged += handler; - return () => s.PropertyChanged -= handler; + return () => + { + unsubscribed = true; + s.PropertyChanged -= handler; + }; }); diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 87bb1d3790..8b0cc06136 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -573,7 +573,7 @@ namespace Avalonia /// The new visual parent. protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent) { - RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); + RaisePropertyChanged(VisualParentProperty, oldParent, newParent); } internal override ParametrizedLogger? GetBindingWarningLogger( diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 36fd9fe709..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 06a77f0894..f5db7c0855 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index f9b84793c6..fcf72385b2 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index d906cd359c..5a4ddd36f4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex(DataGridRow row) { - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index)); } /// diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index dd8575c989..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -1,6 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.6 0.8 @@ -9,19 +51,6 @@ M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z - - - - - - - - - - - @@ -29,23 +58,10 @@ - + - - - - - - - - - @@ -565,5 +581,6 @@ + diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 951e60c25b..3d3d01e06e 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -536,11 +536,12 @@ namespace Avalonia.Controls internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo) { + var index = virtInfo.Index; + _viewportManager.OnElementPrepared(element, virtInfo); if (ElementPrepared != null) { - var index = virtInfo.Index; if (_elementPreparedArgs == null) { @@ -554,7 +555,7 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index)); } internal void OnElementClearing(Control element) @@ -573,7 +574,7 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1)); } internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex) @@ -592,7 +593,7 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex)); } private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6d3ba3cf8a..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -50,13 +50,13 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - StyledElement.ActualThemeVariantProperty.AddOwner(); + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); /// public event EventHandler? ResourcesChanged; @@ -95,11 +95,8 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// - public ThemeVariant ActualThemeVariant - { - get => GetValue(ActualThemeVariantProperty); - } + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// /// Gets the current instance of the class. @@ -256,7 +253,7 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) - .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs new file mode 100644 index 0000000000..9cc0f17818 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs @@ -0,0 +1,21 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ImageAutomationPeer : ControlAutomationPeer + { + public ImageAutomationPeer(Control owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Image"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Image; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs new file mode 100644 index 0000000000..b0f83c1f2a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; + +namespace Avalonia.Controls.Automation.Peers +{ + public class RadioButtonAutomationPeer : ToggleButtonAutomationPeer, ISelectionItemProvider + { + public RadioButtonAutomationPeer(RadioButton owner) : base(owner) + { + owner.PropertyChanged += (a, e) => + { + if (e.Property == RadioButton.IsCheckedProperty) + { + RaiseToggleStatePropertyChangedEvent((bool?)e.OldValue, (bool?)e.NewValue); + } + }; + } + + override protected string GetClassNameCore() + { + return "RadioButton"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.RadioButton; + } + + public bool IsSelected => ((RadioButton)Owner).IsChecked == true; + + public ISelectionProvider? SelectionContainer => null; + + public void AddToSelection() + { + if (((RadioButton)Owner).IsChecked != true) + throw new InvalidOperationException("Operation cannot be performed"); + } + + public void RemoveFromSelection() + { + if (((RadioButton)Owner).IsChecked == true) + throw new InvalidOperationException("Operation cannot be performed"); + } + + public void Select() + { + if (!IsEnabled()) + throw new InvalidOperationException("Element is disabled thus it cannot be selected"); + + ((RadioButton)Owner).IsChecked = true; + } + + internal virtual void RaiseToggleStatePropertyChangedEvent(bool? oldValue, bool? newValue) + { + RaisePropertyChangedEvent( + SelectionItemPatternIdentifiers.IsSelectedProperty, + oldValue == true, + newValue == true); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs new file mode 100644 index 0000000000..418ae1f1fe --- /dev/null +++ b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs @@ -0,0 +1,16 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class SelectionItemPatternIdentifiers + { + /// Indicates the element is currently selected. + public static AutomationProperty IsSelectedProperty { get; } = new AutomationProperty(); + + /// Indicates the element is currently selected. + public static AutomationProperty SelectionContainerProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ab7c9948c4..325593508c 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -403,7 +403,7 @@ namespace Avalonia.Controls { if (_focusAdorner == null) { - var template = GetValue(FocusAdornerProperty); + var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner; if (template != null) { diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 2ad6a58d38..668de5bca9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -191,7 +191,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnCollapsing(RoutedEventArgs eventArgs) + protected virtual void OnCollapsing(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } @@ -207,7 +207,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnExpanding(RoutedEventArgs eventArgs) + protected virtual void OnExpanding(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e1f840672d..dbffb803a3 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls public sealed override void Render(DrawingContext context) { - if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + if (context is IDrawingContextWithAcrylicLikeSupport idc) { var cornerRadius = CornerRadius; diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 3e76835e92..b14cc78e60 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,5 +1,6 @@ using Avalonia.Automation; using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -130,5 +131,10 @@ namespace Avalonia.Controls return new Size(); } } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ImageAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ce12d5f2bf..9483f98881 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -102,7 +102,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; @@ -218,7 +217,6 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } - /// public event EventHandler HorizontalSnapPointsChanged { @@ -495,6 +493,7 @@ namespace Avalonia.Controls else if (change.Property == ItemCountProperty) { UpdatePseudoClasses(change.GetNewValue()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); } else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { @@ -579,7 +578,7 @@ namespace Avalonia.Controls internal void RegisterItemsPresenter(ItemsPresenter presenter) { Presenter = presenter; - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } internal void PrepareItemContainer(Control container, object? item, int index) @@ -601,17 +600,14 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { - _containerBeingPrepared = new(index, container); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); - _containerBeingPrepared = null; - + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); _scrollViewer?.RegisterAnchorCandidate(container); } internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) { ContainerIndexChangedOverride(container, oldIndex, newIndex); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex)); } internal void ClearItemContainer(Control container) @@ -742,9 +738,6 @@ namespace Avalonia.Controls int IChildIndexProvider.GetChildIndex(ILogical child) { - if (_containerBeingPrepared?.Item2 == child) - return _containerBeingPrepared.Item1; - return child is Control container ? IndexFromContainer(container) : -1; } diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index a7dc035459..fa18ee468c 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.LogicalTree; using Avalonia.Media; @@ -60,8 +61,19 @@ namespace Avalonia.Controls event EventHandler? IChildIndexProvider.ChildIndexChanged { - add => _childIndexChanged += value; - remove => _childIndexChanged -= value; + add + { + if (_childIndexChanged is null) + Children.PropertyChanged += ChildrenPropertyChanged; + _childIndexChanged += value; + } + + remove + { + _childIndexChanged -= value; + if (_childIndexChanged is null) + Children.PropertyChanged -= ChildrenPropertyChanged; + } } /// @@ -152,7 +164,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); InvalidateMeasureOnChildrenChanged(); } @@ -161,6 +173,12 @@ namespace Avalonia.Controls InvalidateMeasure(); } + private void ChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Children.Count) || e.PropertyName is null) + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + } + private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) where TPanel : Panel { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 454f7eac9d..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 79719912ea..1e07036919 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -34,6 +34,12 @@ namespace Avalonia.Controls.Primitives public static readonly AttachedProperty AdornerProperty = AvaloniaProperty.RegisterAttached("Adorner"); + /// + /// Defines the property. + /// + public static readonly StyledProperty?> DefaultFocusAdornerProperty = + AvaloniaProperty.Register?>(nameof(DefaultFocusAdorner)); + private static readonly AttachedProperty s_adornedElementInfoProperty = AvaloniaProperty.RegisterAttached("AdornedElementInfo"); @@ -86,6 +92,15 @@ namespace Avalonia.Controls.Primitives visual.SetValue(AdornerProperty, adorner); } + /// + /// Gets or sets the default control's focus adorner. + /// + public ITemplate? DefaultFocusAdorner + { + get => GetValue(DefaultFocusAdornerProperty); + set => SetValue(DefaultFocusAdornerProperty, value); + } + private static void AdornerChanged(AvaloniaPropertyChangedEventArgs e) { if (e.Sender is Visual visual) @@ -279,8 +294,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d6cd71aedc..9d443d9289 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -387,6 +387,15 @@ namespace Avalonia.Controls.Primitives popupHost.Transform = null; } + if (popupHost is PopupRoot topLevelPopup) + { + topLevelPopup + .Bind( + ThemeVariantScope.ActualThemeVariantProperty, + this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + .DisposeWith(handlerCleanup); + } + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 065c4ff2e5..2ee32b0dda 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -345,10 +345,7 @@ namespace Avalonia.Controls.Primitives if (_oldSelectedItems != SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } } @@ -909,10 +906,7 @@ namespace Avalonia.Controls.Primitives else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) { - RaisePropertyChanged( - SelectedItemsProperty, - new Optional(_oldSelectedItems), - new BindingValue(SelectedItems)); + RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems); _oldSelectedItems = SelectedItems; } else if (e.PropertyName == nameof(ISelectionModel.Source)) diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index b87be34a9d..87772aced7 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Reactive; using Avalonia.Rendering; @@ -147,6 +149,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new RadioButtonAutomationPeer(this); + } + private void SetGroupName(string? newGroupName) { var oldGroupName = GroupName; diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index 18cf96ddca..d2b91def7d 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -1,37 +1,30 @@ using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls { public partial class RelativePanel { - private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) - { - layoutableParent.InvalidateArrange(); - } - } static RelativePanel() { ClipToBoundsProperty.OverrideDefaultValue(true); - AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AffectsParentArrange( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); + + AffectsParentMeasure( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); } /// diff --git a/src/Avalonia.Controls/SelectableTextBlock.cs b/src/Avalonia.Controls/SelectableTextBlock.cs index f8ce5d23f6..6603e20a2a 100644 --- a/src/Avalonia.Controls/SelectableTextBlock.cs +++ b/src/Avalonia.Controls/SelectableTextBlock.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls point = new Point( MathUtilities.Clamp(point.X, 0, Math.Max(TextLayout.Bounds.Width, 0)), - MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Width, 0))); + MathUtilities.Clamp(point.Y, 0, Math.Max(TextLayout.Bounds.Height, 0))); var hit = TextLayout.HitTestPoint(point); var textPosition = hit.TextPosition; diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 828bf2a1fb..7de726a932 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Automation; +using Avalonia.Controls.Automation.Peers; namespace Avalonia.Controls { @@ -380,6 +381,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SliderAutomationPeer(this); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs similarity index 56% rename from src/Avalonia.Controls/SplitView.cs rename to src/Avalonia.Controls/SplitView/SplitView.cs index 35b135e152..1099a40f08 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -1,77 +1,16 @@ -using Avalonia.Controls.Metadata; +using System; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; -using Avalonia.VisualTree; -using System; -using Avalonia.Reactive; -using Avalonia.Controls.Presenters; -using Avalonia.Controls.Templates; -using Avalonia.LogicalTree; namespace Avalonia.Controls { - /// - /// Defines constants for how the SplitView Pane should display - /// - public enum SplitViewDisplayMode - { - /// - /// Pane is displayed next to content, and does not auto collapse - /// when tapped outside - /// - Inline, - /// - /// Pane is displayed next to content. When collapsed, pane is still - /// visible according to CompactPaneLength. Pane does not auto collapse - /// when tapped outside - /// - CompactInline, - /// - /// Pane is displayed above content. Pane collapses when tapped outside - /// - Overlay, - /// - /// Pane is displayed above content. When collapsed, pane is still - /// visible according to CompactPaneLength. Pane collapses when tapped outside - /// - CompactOverlay - } - - /// - /// Defines constants for where the Pane should appear - /// - public enum SplitViewPanePlacement - { - Left, - Right - } - - public class SplitViewTemplateSettings : AvaloniaObject - { - internal SplitViewTemplateSettings() { } - - public static readonly StyledProperty ClosedPaneWidthProperty = - AvaloniaProperty.Register(nameof(ClosedPaneWidth), 0d); - - public static readonly StyledProperty PaneColumnGridLengthProperty = - AvaloniaProperty.Register(nameof(PaneColumnGridLength)); - - public double ClosedPaneWidth - { - get => GetValue(ClosedPaneWidthProperty); - internal set => SetValue(ClosedPaneWidthProperty, value); - } - - public GridLength PaneColumnGridLength - { - get => GetValue(PaneColumnGridLengthProperty); - internal set => SetValue(PaneColumnGridLengthProperty, value); - } - } - /// /// A control with two views: A collapsible pane and an area for content /// @@ -93,26 +32,34 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly StyledProperty CompactPaneLengthProperty = - AvaloniaProperty.Register(nameof(CompactPaneLength), defaultValue: 48); + AvaloniaProperty.Register( + nameof(CompactPaneLength), + defaultValue: 48); /// /// Defines the property /// public static readonly StyledProperty DisplayModeProperty = - AvaloniaProperty.Register(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay); + AvaloniaProperty.Register( + nameof(DisplayMode), + defaultValue: SplitViewDisplayMode.Overlay); /// /// Defines the property /// - public static readonly DirectProperty IsPaneOpenProperty = - AvaloniaProperty.RegisterDirect(nameof(IsPaneOpen), - x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v); + public static readonly StyledProperty IsPaneOpenProperty = + AvaloniaProperty.Register( + nameof(IsPaneOpen), + defaultValue: false, + coerce: CoerceIsPaneOpen); /// /// Defines the property /// public static readonly StyledProperty OpenPaneLengthProperty = - AvaloniaProperty.Register(nameof(OpenPaneLength), defaultValue: 320); + AvaloniaProperty.Register( + nameof(OpenPaneLength), + defaultValue: 320); /// /// Defines the property @@ -150,7 +97,38 @@ namespace Avalonia.Controls public static readonly StyledProperty TemplateSettingsProperty = AvaloniaProperty.Register(nameof(TemplateSettings)); - private bool _isPaneOpen; + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneClosedEvent = + RoutedEvent.Register( + nameof(PaneClosed), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneClosingEvent = + RoutedEvent.Register( + nameof(PaneClosing), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneOpenedEvent = + RoutedEvent.Register( + nameof(PaneOpened), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent PaneOpeningEvent = + RoutedEvent.Register( + nameof(PaneOpening), + RoutingStrategies.Bubble); + private Panel? _pane; private IDisposable? _pointerDisposable; @@ -164,12 +142,6 @@ namespace Avalonia.Controls static SplitView() { - UseLightDismissOverlayModeProperty.Changed.AddClassHandler((x, v) => x.OnUseLightDismissChanged(v)); - CompactPaneLengthProperty.Changed.AddClassHandler((x, v) => x.OnCompactPaneLengthChanged(v)); - PanePlacementProperty.Changed.AddClassHandler((x, v) => x.OnPanePlacementChanged(v)); - DisplayModeProperty.Changed.AddClassHandler((x, v) => x.OnDisplayModeChanged(v)); - - PaneProperty.Changed.AddClassHandler((x, e) => x.PaneChanged(e)); } /// @@ -196,37 +168,8 @@ namespace Avalonia.Controls /// public bool IsPaneOpen { - get => _isPaneOpen; - set - { - if (value == _isPaneOpen) - { - return; - } - - if (value) - { - OnPaneOpening(this, EventArgs.Empty); - SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); - - PseudoClasses.Add(":open"); - PseudoClasses.Remove(":closed"); - OnPaneOpened(this, EventArgs.Empty); - } - else - { - SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false); - OnPaneClosing(this, args); - if (!args.Cancel) - { - SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value); - - PseudoClasses.Add(":closed"); - PseudoClasses.Remove(":open"); - OnPaneClosed(this, EventArgs.Empty); - } - } - } + get => GetValue(IsPaneOpenProperty); + set => SetValue(IsPaneOpenProperty, value); } /// @@ -297,24 +240,48 @@ namespace Avalonia.Controls } /// - /// Fired when the pane is closed + /// Fired when the pane is closed. /// - public event EventHandler? PaneClosed; + public event EventHandler? PaneClosed + { + add => AddHandler(PaneClosedEvent, value); + remove => RemoveHandler(PaneClosedEvent, value); + } /// - /// Fired when the pane is closing + /// Fired when the pane is closing. /// - public event EventHandler? PaneClosing; + /// + /// The event args property may be set to true to cancel the event + /// and keep the pane open. + /// + public event EventHandler? PaneClosing + { + add => AddHandler(PaneClosingEvent, value); + remove => RemoveHandler(PaneClosingEvent, value); + } /// - /// Fired when the pane is opened + /// Fired when the pane is opened. /// - public event EventHandler? PaneOpened; + public event EventHandler? PaneOpened + { + add => AddHandler(PaneOpenedEvent, value); + remove => RemoveHandler(PaneOpenedEvent, value); + } /// - /// Fired when the pane is opening + /// Fired when the pane is opening. /// - public event EventHandler? PaneOpening; + /// + /// The event args property may be set to true to cancel the event + /// and keep the pane closed. + /// + public event EventHandler? PaneOpening + { + add => AddHandler(PaneOpeningEvent, value); + remove => RemoveHandler(PaneOpeningEvent, value); + } protected override bool RegisterContentPresenter(IContentPresenter presenter) { @@ -351,6 +318,89 @@ namespace Avalonia.Controls _pointerDisposable?.Dispose(); } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == CompactPaneLengthProperty) + { + var newLen = change.GetNewValue(); + var displayMode = DisplayMode; + if (displayMode == SplitViewDisplayMode.CompactInline) + { + TemplateSettings.ClosedPaneWidth = newLen; + } + else if (displayMode == SplitViewDisplayMode.CompactOverlay) + { + TemplateSettings.ClosedPaneWidth = newLen; + TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel); + } + } + else if (change.Property == DisplayModeProperty) + { + var oldState = GetPseudoClass(change.GetOldValue()); + var newState = GetPseudoClass(change.GetNewValue()); + + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + + var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue() switch + { + SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), + SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), + SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)), + SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)), + _ => throw new NotImplementedException(), + }; + TemplateSettings.ClosedPaneWidth = closedPaneWidth; + TemplateSettings.PaneColumnGridLength = paneColumnGridLength; + } + else if (change.Property == IsPaneOpenProperty) + { + bool isPaneOpen = change.GetNewValue(); + + if (isPaneOpen) + { + PseudoClasses.Add(":open"); + PseudoClasses.Remove(":closed"); + + OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this)); + } + else + { + PseudoClasses.Add(":closed"); + PseudoClasses.Remove(":open"); + + OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this)); + } + } + else if (change.Property == PaneProperty) + { + if (change.OldValue is ILogical oldChild) + { + LogicalChildren.Remove(oldChild); + } + + if (change.NewValue is ILogical newChild) + { + LogicalChildren.Add(newChild); + } + } + else if (change.Property == PanePlacementProperty) + { + var oldState = GetPseudoClass(change.GetOldValue()); + var newState = GetPseudoClass(change.GetNewValue()); + PseudoClasses.Remove($":{oldState}"); + PseudoClasses.Add($":{newState}"); + } + else if (change.Property == UseLightDismissOverlayModeProperty) + { + var mode = change.GetNewValue(); + PseudoClasses.Set(":lightdismiss", mode); + } + } + private void PointerPressedOutside(object? sender, PointerPressedEventArgs e) { if (!IsPaneOpen) @@ -384,7 +434,7 @@ namespace Avalonia.Controls } if (closePane) { - IsPaneOpen = false; + SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } } @@ -394,41 +444,29 @@ namespace Avalonia.Controls return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); } - protected virtual void OnPaneOpening(SplitView sender, EventArgs args) + protected virtual void OnPaneOpening(CancelRoutedEventArgs args) { - PaneOpening?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneOpened(SplitView sender, EventArgs args) + protected virtual void OnPaneOpened(RoutedEventArgs args) { - PaneOpened?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args) + protected virtual void OnPaneClosing(CancelRoutedEventArgs args) { - PaneClosing?.Invoke(sender, args); + RaiseEvent(args); } - protected virtual void OnPaneClosed(SplitView sender, EventArgs args) + protected virtual void OnPaneClosed(RoutedEventArgs args) { - PaneClosed?.Invoke(sender, args); - } - - private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e) - { - var newLen = (double)e.NewValue!; - var displayMode = DisplayMode; - if (displayMode == SplitViewDisplayMode.CompactInline) - { - TemplateSettings.ClosedPaneWidth = newLen; - } - else if (displayMode == SplitViewDisplayMode.CompactOverlay) - { - TemplateSettings.ClosedPaneWidth = newLen; - TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel); - } + RaiseEvent(args); } + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch @@ -441,6 +479,9 @@ namespace Avalonia.Controls }; } + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch @@ -451,51 +492,47 @@ namespace Avalonia.Controls }; } - private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) - { - var oldState = GetPseudoClass(e.GetOldValue()); - var newState = GetPseudoClass(e.GetNewValue()); - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); - } - - private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) + /// + /// Called when the property has to be coerced. + /// + /// The value to coerce. + protected virtual bool OnCoerceIsPaneOpen(bool value) { - var oldState = GetPseudoClass(e.GetOldValue()); - var newState = GetPseudoClass(e.GetNewValue()); + CancelRoutedEventArgs eventArgs; - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); + if (value) + { + eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this); + OnPaneOpening(eventArgs); + } + else + { + eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this); + OnPaneClosing(eventArgs); + } - var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue() switch + if (eventArgs.Cancel) { - SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), - SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), - SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)), - SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)), - _ => throw new NotImplementedException(), - }; - TemplateSettings.ClosedPaneWidth = closedPaneWidth; - TemplateSettings.PaneColumnGridLength = paneColumnGridLength; - } + return !value; + } - private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e) - { - var mode = (bool)e.NewValue!; - PseudoClasses.Set(":lightdismiss", mode); + return value; } - private void PaneChanged(AvaloniaPropertyChangedEventArgs e) + /// + /// Coerces/validates the property value. + /// + /// The instance. + /// The value to coerce. + /// The coerced/validated value. + private static bool CoerceIsPaneOpen(AvaloniaObject instance, bool value) { - if (e.OldValue is ILogical oldChild) + if (instance is SplitView splitView) { - LogicalChildren.Remove(oldChild); + return splitView.OnCoerceIsPaneOpen(value); } - if (e.NewValue is ILogical newChild) - { - LogicalChildren.Add(newChild); - } + return value; } } } diff --git a/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs b/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs new file mode 100644 index 0000000000..6333f96f86 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs @@ -0,0 +1,29 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants for how the SplitView Pane should display + /// + public enum SplitViewDisplayMode + { + /// + /// Pane is displayed next to content, and does not auto collapse + /// when tapped outside + /// + Inline, + /// + /// Pane is displayed next to content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane does not auto collapse + /// when tapped outside + /// + CompactInline, + /// + /// Pane is displayed above content. Pane collapses when tapped outside + /// + Overlay, + /// + /// Pane is displayed above content. When collapsed, pane is still + /// visible according to CompactPaneLength. Pane collapses when tapped outside + /// + CompactOverlay + } +} diff --git a/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs b/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs new file mode 100644 index 0000000000..62c5387192 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants for where the Pane should appear + /// + public enum SplitViewPanePlacement + { + /// + /// The pane is shown to the left of content. + /// + Left, + + /// + /// The pane is shown to the right of content. + /// + Right + } +} diff --git a/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs new file mode 100644 index 0000000000..f2cbf55986 --- /dev/null +++ b/src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs @@ -0,0 +1,32 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// Provides calculated values for use with the 's control theme or template. + /// This class is NOT intended for general use. + /// + public class SplitViewTemplateSettings : AvaloniaObject + { + internal SplitViewTemplateSettings() { } + + public static readonly StyledProperty ClosedPaneWidthProperty = + AvaloniaProperty.Register(nameof(ClosedPaneWidth), + 0d); + + public static readonly StyledProperty PaneColumnGridLengthProperty = + AvaloniaProperty.Register( + nameof(PaneColumnGridLength)); + + public double ClosedPaneWidth + { + get => GetValue(ClosedPaneWidthProperty); + internal set => SetValue(ClosedPaneWidthProperty, value); + } + + public GridLength PaneColumnGridLength + { + get => GetValue(PaneColumnGridLengthProperty); + internal set => SetValue(PaneColumnGridLengthProperty, value); + } + } +} diff --git a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs deleted file mode 100644 index 46fb2d161b..0000000000 --- a/src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - public class SplitViewPaneClosingEventArgs : EventArgs - { - public bool Cancel { get; set; } - - public SplitViewPaneClosingEventArgs(bool cancel) - { - Cancel = cancel; - } - } -} diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index b9724251c7..f5ad4b2f94 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -7,6 +7,14 @@ namespace Avalonia.Controls /// public class ThemeVariantScope : Decorator { + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariant.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariant.RequestedThemeVariantProperty.AddOwner(); + /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index ed11dec1d0..fdcb8cc537 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -80,6 +80,14 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); + /// /// Defines the event. /// @@ -96,7 +104,7 @@ namespace Avalonia.Controls private readonly IAccessKeyHandler? _accessKeyHandler; private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IGlobalStyles? _globalStyles; - private readonly IGlobalThemeVariantProvider? _applicationThemeHost; + private readonly IThemeVariantHost? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -147,7 +155,7 @@ namespace Avalonia.Controls _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); - _applicationThemeHost = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); Renderer.SceneInvalidated += SceneInvalidated; @@ -405,7 +413,31 @@ namespace Avalonia.Controls { return visual?.VisualRoot as TopLevel; } - + + /// + /// Requests a to be inhibited. + /// The behavior remains inhibited until the return value is disposed. + /// The available set of s depends on the platform. + /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. + /// + public async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) + { + var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); + if (platformBehaviorInhibition == null) + { + return Disposable.Create(() => { }); + } + + switch (type) + { + case PlatformInhibitionType.AppSleep: + await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); + return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); + default: + return Disposable.Create(() => { }); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -571,30 +603,6 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); - /// - /// Requests a to be inhibited. - /// The behavior remains inhibited until the return value is disposed. - /// The available set of s depends on the platform. - /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. - /// - protected async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) - { - var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); - if (platformBehaviorInhibition == null) - { - return Disposable.Create(() => { }); - } - - switch (type) - { - case PlatformInhibitionType.AppSleep: - await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); - return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); - default: - return Disposable.Create(() => { }); - } - } - /// /// Tries to get a service from an , logging a /// warning if not found. @@ -642,7 +650,7 @@ namespace Avalonia.Controls private void GlobalActualThemeVariantChanged(object? sender, EventArgs e) { - SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template); + SetValue(ActualThemeVariantProperty, ((IThemeVariantHost)sender!).ActualThemeVariant, BindingPriority.Template); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 9f8e3e38c0..e9abfef673 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -190,7 +190,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = true; + treeViewItem.SetCurrentValue(IsExpandedProperty, true); return true; } @@ -201,7 +201,7 @@ namespace Avalonia.Controls { if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); return true; } @@ -214,7 +214,7 @@ namespace Avalonia.Controls { if (treeViewItem.IsFocused) { - treeViewItem.IsExpanded = false; + treeViewItem.SetCurrentValue(IsExpandedProperty, false); } else { @@ -265,7 +265,7 @@ namespace Avalonia.Controls { if (ItemCount > 0) { - IsExpanded = !IsExpanded; + SetCurrentValue(IsExpandedProperty, !IsExpanded); e.Handled = true; } } diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index 6239a5120d..799cc47d0c 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Utils var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight, _cornerRadius.BottomRight, _cornerRadius.BottomLeft); - context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows); + context.DrawRectangle(background, pen, rrect, boxShadows); } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 634efbd699..4970a333a5 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls if (firstIndex == -1) { estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); firstIndexU = firstIndex * estimatedElementSize; } @@ -411,13 +411,13 @@ namespace Avalonia.Controls { if (estimatedElementSize == -1) estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); + lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); } return new MeasureViewport { - firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), - lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), + firstIndex = firstIndex, + lastIndex = lastIndex, viewportUStart = viewportStart, viewportUEnd = viewportEnd, startU = firstIndexU, @@ -1131,6 +1131,7 @@ namespace Avalonia.Controls // The removed range was before the realized elements. Update the first index and // the indexes of the realized elements. _firstIndex -= count; + _startUUnstable = true; var newIndex = _firstIndex; for (var i = 0; i < _elements.Count; ++i) @@ -1166,7 +1167,7 @@ namespace Avalonia.Controls // Update the indexes of the elements after the removed range. end = _elements.Count; - var newIndex = first; + var newIndex = first + start; for (var i = start; i < end; ++i) { if (_elements[i] is Control element) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index a0ff3a714f..a8f609c507 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.Controls public event EventHandler? Closed; public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); public Application(Avalonia.Application application) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 3bfe511fbc..1b5f431f36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -16,6 +16,8 @@