diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 741570061b..4a7a329fc6 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -42,6 +42,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", @@ -61,4 +62,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} diff --git a/Avalonia.sln b/Avalonia.sln index 7d9c5243e1..e66b73de0e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,6 +233,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" @@ -554,6 +561,10 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -631,6 +642,7 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 23abf1d53f..b1fb915e04 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -238,7 +238,7 @@ -(BOOL)canBecomeKeyWindow { - if(_canBecomeKeyWindow) + if(_canBecomeKeyWindow && !_closed) { // If the window has a child window being shown as a dialog then don't allow it to become the key window. auto parent = dynamic_cast(_parent.getRaw()); @@ -292,12 +292,14 @@ { if (_parent == nullptr) return; - + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ @try { - [self invalidateShadow]; + [self invalidateShadow]; + if (self->_parent != nullptr) + self->_parent->BringToFront(); } @finally{ } diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs index 5fd331035a..f2dd217657 100644 --- a/nukebuild/BuildTasksPatcher.cs +++ b/nukebuild/BuildTasksPatcher.cs @@ -4,9 +4,58 @@ using System.IO.Compression; using System.Linq; using ILRepacking; using Mono.Cecil; +using Mono.Cecil.Cil; public class BuildTasksPatcher { + /// + /// This helper class, avoid argument null exception + /// when cecil write AssemblyNameDefinition on MemoryStream. + /// + private class Wrapper : ISymbolWriterProvider + { + readonly ISymbolWriterProvider _provider; + readonly string _filename; + + public Wrapper(ISymbolWriterProvider provider, string filename) + { + _provider = provider; + _filename = filename; + } + + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, string fileName) => + _provider.GetSymbolWriter(module, string.IsNullOrWhiteSpace(fileName) ? _filename : fileName); + + public ISymbolWriter GetSymbolWriter(ModuleDefinition module, Stream symbolStream) => + _provider.GetSymbolWriter(module, symbolStream); + } + + private static string GetSourceLinkInfo(string path) + { + try + { + using (var asm = AssemblyDefinition.ReadAssembly(path, + new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + })) + { + if (asm.MainModule.CustomDebugInformations?.OfType()?.FirstOrDefault() is { } sli) + { + return sli.Content; + } + } + } + catch + { + + } + return null; + } + public static void PatchBuildTasksInPackage(string packagePath) { using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), @@ -19,7 +68,7 @@ public class BuildTasksPatcher { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(tempDir); - var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll"); + var temp = Path.Combine(tempDir, entry.Name); var output = temp + ".output"; File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName, Path.Combine(tempDir, "Microsoft.Build.Framework.dll")); @@ -27,41 +76,74 @@ public class BuildTasksPatcher try { entry.ExtractToFile(temp, true); + // Get Original SourceLinkInfo Content + var sourceLinkInfoContent = GetSourceLinkInfo(temp); var repack = new ILRepacking.ILRepack(new RepackOptions() { Internalize = true, InputAssemblies = new[] { - temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0] - .FullyQualifiedName, + temp, + typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0].FullyQualifiedName, typeof(Mono.Cecil.Rocks.MethodBodyRocks).Assembly.GetModules()[0].FullyQualifiedName, typeof(Mono.Cecil.Pdb.PdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, - typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName - + typeof(Mono.Cecil.Mdb.MdbReaderProvider).Assembly.GetModules()[0].FullyQualifiedName, }, - SearchDirectories = new string[0], + SearchDirectories = Array.Empty(), + DebugInfo = true, // Allowed read debug info OutputFile = output }); repack.Repack(); - // 'hurr-durr assembly with the same name is already loaded' prevention using (var asm = AssemblyDefinition.ReadAssembly(output, - new ReaderParameters { ReadWrite = true, InMemory = true, })) + new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + })) { asm.Name = new AssemblyNameDefinition( "Avalonia.Build.Tasks." + Guid.NewGuid().ToString().Replace("-", ""), new Version(0, 0, 0)); - asm.Write(patched); + + var mainModule = asm.MainModule; + + // If we have SourceLink info copy to patched assembly. + if (!string.IsNullOrEmpty(sourceLinkInfoContent)) + { + mainModule.CustomDebugInformations.Add(new SourceLinkDebugInformation(sourceLinkInfoContent)); + } + + // Try to get SymbolWriter if it has it + var reader = mainModule.SymbolReader; + var hasDebugInfo = reader is not null; + var proivder = reader?.GetWriterProvider() is ISymbolWriterProvider p + ? new Wrapper(p, "Avalonia.Build.Tasks.dll") + : default(ISymbolWriterProvider); + + var parameters = new WriterParameters + { +#if ISNETFULLFRAMEWORK + StrongNameKeyPair = signingStep.KeyPair, +#endif + WriteSymbols = hasDebugInfo, + SymbolWriterProvider = proivder, + DeterministicMvid = hasDebugInfo, + }; + asm.Write(patched, parameters); patched.Position = 0; } + } finally { try { - if(Directory.Exists(tempDir)) + if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch @@ -79,4 +161,4 @@ public class BuildTasksPatcher } } } -} \ No newline at end of file +} diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e24860e3e1..e5f29abb68 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -306,25 +306,8 @@ namespace ControlCatalog.Pages resultText += @$" Content: "; -#if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif - using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + resultText += await ReadTextFromFile(file, 10000); } openedFileContent.Text = resultText; @@ -354,6 +337,28 @@ namespace ControlCatalog.Pages } } + public static async Task ReadTextFromFile(IStorageFile file, int length) + { +#if NET6_0_OR_GREATER + await using var stream = await file.OpenReadAsync(); +#else + using var stream = await file.OpenReadAsync(); +#endif + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 3f8a023060..390fa32b9c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -25,7 +25,6 @@ BorderThickness="2"> Drag Me (custom) - + + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index e384db88b3..26430b4b61 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -1,27 +1,29 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - TextBlock _DropState; + private readonly TextBlock _dropState; private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); - _DropState = this.Get("DropState"); + _dropState = this.Get("DropState"); int textCount = 0; SetupDnd("Text", d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move); - SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); + SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); } void SetupDnd(string suffix, Action factory, DragDropEffects effects) @@ -68,12 +70,12 @@ namespace ControlCatalog.Pages // Only allow if the dragged data contains text or filenames. if (!e.Data.Contains(DataFormats.Text) - && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(DataFormats.Files) && !e.Data.Contains(CustomFormat)) e.DragEffects = DragDropEffects.None; } - void Drop(object? sender, DragEventArgs e) + async void Drop(object? sender, DragEventArgs e) { if (e.Source is Control c && c.Name == "MoveTarget") { @@ -85,11 +87,41 @@ namespace ControlCatalog.Pages } if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); + { + _dropState.Text = e.Data.GetText(); + } + else if (e.Data.Contains(DataFormats.Files)) + { + var files = e.Data.GetFiles() ?? Array.Empty(); + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 1000); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var items = await folder.GetItemsAsync(); + contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + } + } + + _dropState.Text = contentStr; + } +#pragma warning disable CS0618 // Type or member is obsolete else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty()); + { + var files = e.Data.GetFileNames(); + _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty()); + } +#pragma warning restore CS0618 // Type or member is obsolete else if (e.Data.Contains(CustomFormat)) - _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + { + _dropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } } dragMe.PointerPressed += DoDrag; diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index dc0eaf0a51..b1b1b99c9c 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,7 @@ diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 3b14f0ce12..ac78d9c739 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -6,4 +6,5 @@ 11 + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 353e01dca7..090cf23b33 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -25,6 +25,7 @@ WindowState: + @@ -56,6 +57,16 @@ + + + Sample RadioButton + + Three States: Option 1 + Three States: Option 2 + + + + Unchecked diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 087f25666b..19eb1d64b0 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,19 +1,17 @@ -using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Media; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.VisualTree; using Microsoft.CodeAnalysis; -using Avalonia.Controls.Primitives; -using Avalonia.Threading; -using Avalonia.Controls.Primitives.PopupPositioning; namespace IntegrationTestApp { @@ -25,6 +23,10 @@ namespace IntegrationTestApp InitializeViewMenu(); InitializeGesturesTab(); this.AttachDevTools(); + + var overlayPopups = this.Get("AppOverlayPopups"); + overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups"; + AddHandler(Button.ClickEvent, OnButtonClick); ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); DataContext = this; diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs index c09b249cfa..6603450b85 100644 --- a/samples/IntegrationTestApp/Program.cs +++ b/samples/IntegrationTestApp/Program.cs @@ -1,17 +1,31 @@ using System; +using System.Linq; using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; namespace IntegrationTestApp { class Program { + public static bool OverlayPopups { get; private set; } + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + OverlayPopups = args.Contains("--overlayPopups"); + + BuildAvaloniaApp() + .With(new Win32PlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .With(new AvaloniaNativePlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index bf27747154..4a3e20ff5b 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -8,22 +9,27 @@ using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Avalonia.Threading; +using Avalonia.Utilities; using SkiaSharp; namespace RenderDemo.Pages { public class CustomSkiaPage : Control { + private readonly GlyphRun _noSkia; public CustomSkiaPage() { ClipToBounds = true; + var text = "Current rendering API is not Skia"; + var glyphs = text.Select(ch => Typeface.Default.GlyphTypeface.GetGlyph(ch)).ToArray(); + _noSkia = new GlyphRun(Typeface.Default.GlyphTypeface, 12, text.AsMemory(), glyphs); } class CustomDrawOp : ICustomDrawOperation { - private readonly FormattedText _noSkia; + private readonly GlyphRun _noSkia; - public CustomDrawOp(Rect bounds, FormattedText noSkia) + public CustomDrawOp(Rect bounds, GlyphRun noSkia) { _noSkia = noSkia; Bounds = bounds; @@ -42,10 +48,7 @@ namespace RenderDemo.Pages { var leaseFeature = context.GetFeature(); if (leaseFeature == null) - using (var c = new DrawingContext(context, false)) - { - c.DrawText(_noSkia, new Point()); - } + context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); else { using var lease = leaseFeature.Lease(); @@ -114,10 +117,7 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black); - - context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); + context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), _noSkia)); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); } } diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs index cc5125609c..2fe57165b3 100644 --- a/samples/RenderDemo/Pages/PathMeasurementPage.cs +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -37,11 +37,8 @@ namespace RenderDemo.Pages public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using (var bitmapCtx = new DrawingContext(ctxi, false)) + using (var bitmapCtx = _bitmap.CreateDrawingContext()) { - ctxi.Clear(default); - var basePath = new PathGeometry(); using (var basePathCtx = basePath.Open()) diff --git a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs index f365b59c20..b88dded39b 100644 --- a/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs +++ b/samples/RenderDemo/Pages/RenderTargetBitmapPage.cs @@ -28,13 +28,11 @@ namespace RenderDemo.Pages readonly Stopwatch _st = Stopwatch.StartNew(); public override void Render(DrawingContext context) { - using (var ctxi = _bitmap.CreateDrawingContext(null)) - using(var ctx = new DrawingContext(ctxi, false)) + using (var ctx = _bitmap.CreateDrawingContext()) using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100) * Matrix.CreateRotation(_st.Elapsed.TotalSeconds) * Matrix.CreateTranslation(100, 100))) { - ctxi.Clear(default); ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100)); } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index d89d6f3690..f3a046ef80 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -784,6 +784,11 @@ namespace Avalonia } } + internal void OnUpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) + { + UpdateDataValidation(property, state, error); + } + /// /// Gets a description of an observable that van be used in logs. /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 6231483ff8..9fbf680a5c 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -199,13 +199,11 @@ namespace Avalonia property = property ?? throw new ArgumentNullException(nameof(property)); binding = binding ?? throw new ArgumentNullException(nameof(binding)); - var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata; - var result = binding.Initiate( target, property, anchor, - metadata?.EnableDataValidation ?? false); + property.GetMetadata(target.GetType()).EnableDataValidation ?? false); if (result != null) { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 45ab293a89..24244c5068 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -227,6 +227,7 @@ namespace Avalonia /// The default binding mode for the property. /// A value validation callback. /// A value coercion callback. + /// Whether the property is interested in data validation. /// A public static StyledProperty Register( string name, @@ -234,7 +235,8 @@ namespace Avalonia bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, - Func? coerce = null) + Func? coerce = null, + bool enableDataValidation = false) where TOwner : AvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); @@ -242,7 +244,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, - coerce: coerce); + coerce: coerce, + enableDataValidation: enableDataValidation); var result = new StyledProperty( name, @@ -253,7 +256,7 @@ namespace Avalonia AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } - + /// /// /// A method that gets called before and after the property starts being notified on an @@ -267,6 +270,7 @@ namespace Avalonia BindingMode defaultBindingMode, Func? validate, Func? coerce, + bool enableDataValidation, Action? notifying) where TOwner : AvaloniaObject { @@ -275,7 +279,8 @@ namespace Avalonia var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, - coerce: coerce); + coerce: coerce, + enableDataValidation: enableDataValidation); var result = new StyledProperty( name, diff --git a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs index 2963567b14..62bb65351f 100644 --- a/src/Avalonia.Base/AvaloniaPropertyMetadata.cs +++ b/src/Avalonia.Base/AvaloniaPropertyMetadata.cs @@ -13,10 +13,13 @@ namespace Avalonia /// Initializes a new instance of the class. /// /// The default binding mode. + /// Whether the property is interested in data validation. public AvaloniaPropertyMetadata( - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + bool? enableDataValidation = null) { _defaultBindingMode = defaultBindingMode; + EnableDataValidation = enableDataValidation; } /// @@ -31,6 +34,17 @@ namespace Avalonia } } + /// + /// Gets a value indicating whether the property is interested in data validation. + /// + /// + /// Data validation is validation performed at the target of a binding, for example in a + /// view model using the INotifyDataErrorInfo interface. Only certain properties on a + /// control (such as a TextBox's Text property) will be interested in receiving data + /// validation messages so this feature must be explicitly enabled by setting this flag. + /// + public bool? EnableDataValidation { get; private set; } + /// /// Merges the metadata with the base metadata. /// @@ -44,6 +58,8 @@ namespace Avalonia { _defaultBindingMode = baseMetadata.DefaultBindingMode; } + + EnableDataValidation ??= baseMetadata.EnableDataValidation; } } } diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 4b0bab0c92..8aed1545a5 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -138,18 +138,18 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged -= PropertyChanged; + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; } } @@ -163,18 +163,15 @@ namespace Avalonia.Controls PublishNext(GetValue()); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(GetValue()); - } + PublishNext(GetValue()); } private object? GetValue() { - if (_target is not StyledElement themeStyleable - || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (_target is not IThemeVariantHost themeVariantHost + || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } @@ -236,9 +233,9 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is StyledElement styleable) + if (_owner is IThemeVariantHost themeVariantHost) { - styleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } _owner = _target.Owner; @@ -247,20 +244,18 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is StyledElement styleable2) + if (_owner is IThemeVariantHost themeVariantHost2) { - styleable2.PropertyChanged += PropertyChanged; + themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } + PublishNext(); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(); - } + PublishNext(); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) @@ -270,8 +265,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is StyledElement themeStyleable) - || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (!(_target.Owner is IThemeVariantHost themeVariantHost) + || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index fe1cdd0e65..451ff6ce00 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -21,10 +21,9 @@ namespace Avalonia TValue unsetValue = default!, BindingMode defaultBindingMode = BindingMode.Default, bool? enableDataValidation = null) - : base(defaultBindingMode) + : base(defaultBindingMode, enableDataValidation) { UnsetValue = unsetValue; - EnableDataValidation = enableDataValidation; } /// @@ -32,16 +31,6 @@ namespace Avalonia /// public TValue UnsetValue { get; private set; } - /// - /// Gets a value indicating whether the property is interested in data validation. - /// - /// - /// Data validation is validation performed at the target of a binding, for example in a - /// view model using the INotifyDataErrorInfo interface. Only certain properties on a - /// control (such as a TextBox's Text property) will be interested in receiving data - /// validation messages so this feature must be explicitly enabled by setting this flag. - /// - public bool? EnableDataValidation { get; private set; } /// object? IDirectPropertyMetadata.UnsetValue => UnsetValue; @@ -51,19 +40,9 @@ namespace Avalonia { base.Merge(baseMetadata, property); - var src = baseMetadata as DirectPropertyMetadata; - - if (src != null) + if (baseMetadata is DirectPropertyMetadata src) { - if (UnsetValue == null) - { - UnsetValue = src.UnsetValue; - } - - if (EnableDataValidation == null) - { - EnableDataValidation = src.EnableDataValidation; - } + UnsetValue ??= src.UnsetValue; } } } diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 59c66ed505..13ca140565 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Avalonia.Interactivity; -using Avalonia.VisualTree; +using Avalonia.LogicalTree; namespace Avalonia.Input { @@ -190,7 +189,7 @@ namespace Avalonia.Input // If the menu is open, only match controls in the menu's visual tree. if (menuIsOpen) { - matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsVisualAncestorOf((Visual)x)); + matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x)); } var match = matches.FirstOrDefault(); diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index cf5a6592e1..35d50e669a 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input +using System; + +namespace Avalonia.Input { public static class DataFormats { @@ -7,9 +9,15 @@ /// public static readonly string Text = nameof(Text); + /// + /// Dataformat for one or more files. + /// + public static readonly string Files = nameof(Files); + /// /// Dataformat for one or more filenames /// + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs index 688f5f9cc8..93a6baa03c 100644 --- a/src/Avalonia.Base/Input/DataObject.cs +++ b/src/Avalonia.Base/Input/DataObject.cs @@ -2,37 +2,34 @@ namespace Avalonia.Input { + /// + /// Specific and mutable implementation of the IDataObject interface. + /// public class DataObject : IDataObject { - private readonly Dictionary _items = new Dictionary(); + private readonly Dictionary _items = new(); + /// public bool Contains(string dataFormat) { return _items.ContainsKey(dataFormat); } + /// public object? Get(string dataFormat) { - if (_items.ContainsKey(dataFormat)) - return _items[dataFormat]; - return null; + return _items.TryGetValue(dataFormat, out var item) ? item : null; } + /// public IEnumerable GetDataFormats() { return _items.Keys; } - public IEnumerable? GetFileNames() - { - return Get(DataFormats.FileNames) as IEnumerable; - } - - public string? GetText() - { - return Get(DataFormats.Text) as string; - } - + /// + /// Sets a value to the internal store of the data object with as a key. + /// public void Set(string dataFormat, object value) { _items[dataFormat] = value; diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs new file mode 100644 index 0000000000..6af531b0d8 --- /dev/null +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; + +namespace Avalonia.Input +{ + public static class DataObjectExtensions + { + /// + /// Returns a list of files if the DataObject contains files or filenames. + /// . + /// + /// + /// Collection of storage items - files or folders. If format isn't available, returns null. + /// + public static IEnumerable? GetFiles(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Files) as IEnumerable; + } + + /// + /// Returns a list of filenames if the DataObject contains filenames. + /// + /// + /// + /// Collection of file names. If format isn't available, returns null. + /// + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + public static IEnumerable? GetFileNames(this IDataObject dataObject) + { + return (dataObject.Get(DataFormats.FileNames) as IEnumerable) + ?? dataObject.GetFiles()? + .Select(f => f.TryGetLocalPath()) + .Where(p => !string.IsNullOrEmpty(p)) + .OfType(); + } + + /// + /// Returns the dragged text if the DataObject contains any text. + /// + /// + /// + /// A text string. If format isn't available, returns null. + /// + public static string? GetText(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Text) as string; + } + } +} diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index 1db008aa3a..6ccd0a8499 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; namespace Avalonia.Input { @@ -19,21 +21,12 @@ namespace Avalonia.Input /// bool Contains(string dataFormat); - /// - /// Returns the dragged text if the DataObject contains any text. - /// - /// - string? GetText(); - - /// - /// Returns a list of filenames if the DataObject contains filenames. - /// - /// - IEnumerable? GetFileNames(); - /// /// Tries to get the data of the given DataFormat. /// + /// + /// Object data. If format isn't available, returns null. + /// object? Get(string dataFormat); } } diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index e1c42c4ead..50980f1c3d 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/src/Avalonia.Base/Input/MouseDevice.cs @@ -184,6 +184,7 @@ namespace Avalonia.Input source?.RaiseEvent(e); _pointer.Capture(null); + _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 98da83c1ce..285249a5f8 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/src/Avalonia.Base/Input/PenDevice.cs @@ -131,6 +131,7 @@ namespace Avalonia.Input source?.RaiseEvent(e); pointer.Capture(null); + _lastMouseDownButton = default; return e.Handled; } diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 4a273b0291..ea84dc84bd 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -798,6 +798,12 @@ namespace Avalonia.Layout InvalidateMeasure(); } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + InvalidateMeasure(); + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs index 497596fcc1..6b41c1c66c 100644 --- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -1,28 +1,79 @@ -#nullable enable -using System; +using System; + +#nullable enable namespace Avalonia.LogicalTree { + /// + /// Describes the action that caused a event. + /// + public enum ChildIndexChangedAction + { + /// + /// The index of a single child changed. + /// + ChildIndexChanged, + + /// + /// The index of multiple children changed and all children should be re-evaluated. + /// + ChildIndexesReset, + + /// + /// The total number of children changed. + /// + TotalCountChanged, + } + /// /// Event args for event. /// public class ChildIndexChangedEventArgs : EventArgs { - public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs(); - - private ChildIndexChangedEventArgs() + /// + /// Initializes a new instance of the class with + /// an action of . + /// + /// The child whose index was changed. + /// The new index of the child. + public ChildIndexChangedEventArgs(ILogical child, int index) { + Action = ChildIndexChangedAction.ChildIndexChanged; + Child = child; + Index = index; } - public ChildIndexChangedEventArgs(ILogical child) + private ChildIndexChangedEventArgs(ChildIndexChangedAction action) { - Child = child; + Action = action; + Index = -1; } /// - /// Logical child which index was changed. - /// If null, all children should be reset. + /// Gets the type of change action that ocurred on the list control. + /// + public ChildIndexChangedAction Action { get; } + + /// + /// Gets the logical child whose index was changed or null if all children should be re-evaluated. /// public ILogical? Child { get; } + + /// + /// Gets the new index of or -1 if all children should be re-evaluated. + /// + public int Index { get; } + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs ChildIndexesReset { get; } = new(ChildIndexChangedAction.ChildIndexesReset); + + /// + /// Gets an instance of the with an action of + /// . + /// + public static ChildIndexChangedEventArgs TotalCountChanged { get; } = new(ChildIndexChangedAction.TotalCountChanged); } } diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs index 7fcd73273c..186c9527f2 100644 --- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs @@ -25,7 +25,7 @@ namespace Avalonia.LogicalTree bool TryGetTotalCount(out int count); /// - /// Notifies subscriber when child's index or total count was changed. + /// Notifies subscriber when a child's index was changed. /// event EventHandler? ChildIndexChanged; } diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs new file mode 100644 index 0000000000..2825628948 --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -0,0 +1,66 @@ +using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + /// + /// Paints an area with an . + /// + public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + static DrawingBrush() + { + AffectsRender(DrawingProperty); + } + + /// + /// Initializes a new instance of the class. + /// + public DrawingBrush() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + public DrawingBrush(Drawing visual) + { + Drawing = visual; + } + + /// + /// Gets or sets the visual to draw. + /// + public Drawing? Drawing + { + get { return GetValue(DrawingProperty); } + set { SetValue(DrawingProperty, value); } + } + + ISceneBrushContent? ISceneBrush.CreateContent() + { + if (Drawing == null) + return null; + + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + Drawing?.Draw(recorder); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + drawList.CalculateBounds(), true); + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 622181dba0..31a16dc69c 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -8,83 +8,45 @@ using Avalonia.Media.Imaging; namespace Avalonia.Media { - public sealed class DrawingContext : IDisposable + public abstract class DrawingContext : IDisposable { - private readonly bool _ownsImpl; - private int _currentLevel; + private static ThreadSafeObjectPool> StateStackPool { get; } = + ThreadSafeObjectPool>.Default; + private Stack? _states; - private static ThreadSafeObjectPool> StateStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private static ThreadSafeObjectPool> TransformStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _states = StateStackPool.Get(); - - private Stack? _transformContainers = TransformStackPool.Get(); - - readonly struct TransformContainer - { - public readonly Matrix LocalTransform; - public readonly Matrix ContainerTransform; - - public TransformContainer(Matrix localTransform, Matrix containerTransform) - { - LocalTransform = localTransform; - ContainerTransform = containerTransform; - } - } - - public DrawingContext(IDrawingContextImpl impl) + internal DrawingContext() { - PlatformImpl = impl; - _ownsImpl = true; + } - - public DrawingContext(IDrawingContextImpl impl, bool ownsImpl) - { - _ownsImpl = ownsImpl; - PlatformImpl = impl; - } - - public IDrawingContextImpl PlatformImpl { get; } - - private Matrix _currentTransform = Matrix.Identity; - private Matrix _currentContainerTransform = Matrix.Identity; - - /// - /// Gets the current transform of the drawing context. - /// - public Matrix CurrentTransform + public void Dispose() { - get { return _currentTransform; } - private set + if (_states != null) { - _currentTransform = value; - var transform = _currentTransform * _currentContainerTransform; - PlatformImpl.Transform = transform; - } - } + while (_states.Count > 0) + _states.Pop().Dispose(); - //HACK: This is a temporary hack that is used in the render loop - //to update TransformedBounds property - [Obsolete("HACK for render loop, don't use")] - public Matrix CurrentContainerTransform => _currentContainerTransform; + StateStackPool.ReturnAndSetNull(ref _states); + } + DisposeCore(); + } + + protected abstract void DisposeCore(); + /// /// Draws an image. /// /// The image. /// The rect in the output to draw to. - public void DrawImage(IImage source, Rect rect) + public virtual void DrawImage(IImage source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - DrawImage(source, new Rect(source.Size), rect); } + /// /// Draws an image. /// @@ -92,12 +54,22 @@ namespace Avalonia.Media /// The rect in the image to draw. /// The rect in the output to draw to. /// The bitmap interpolation mode. - public void DrawImage(IImage source, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) + public virtual void DrawImage(IImage source, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = default) { _ = source ?? throw new ArgumentNullException(nameof(source)); - source.Draw(this, sourceRect, destRect, bitmapInterpolationMode); } + + /// + /// Draws a platform-specific bitmap impl. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + /// The bitmap interpolation mode. + internal abstract void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a line. @@ -108,11 +80,11 @@ namespace Avalonia.Media public void DrawLine(IPen pen, Point p1, Point p2) { if (PenIsVisible(pen)) - { - PlatformImpl.DrawLine(pen, p1, p2); - } + DrawLineCore(pen, p1, p2); } + protected abstract void DrawLineCore(IPen pen, Point p1, Point p2); + /// /// Draws a geometry. /// @@ -121,10 +93,10 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, Geometry geometry) { - if (geometry.PlatformImpl is not null) - DrawGeometry(brush, pen, geometry.PlatformImpl); + if ((brush != null || PenIsVisible(pen)) && geometry.PlatformImpl != null) + DrawGeometryCore(brush, pen, geometry.PlatformImpl); } - + /// /// Draws a geometry. /// @@ -133,14 +105,12 @@ namespace Avalonia.Media /// The geometry. public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { - _ = geometry ?? throw new ArgumentNullException(nameof(geometry)); - - if (brush != null || PenIsVisible(pen)) - { - PlatformImpl.DrawGeometry(brush, pen, geometry); - } + if ((brush != null || PenIsVisible(pen))) + DrawGeometryCore(brush, pen, geometry); } + protected abstract void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry); + /// /// Draws a rectangle with the specified Brush and Pen. /// @@ -158,14 +128,12 @@ namespace Avalonia.Media /// The brush and the pen can both be null. If the brush is null, then no fill is performed. /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. /// - public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, double radiusX = 0, double radiusY = 0, + public void DrawRectangle(IBrush? brush, IPen? pen, Rect rect, + double radiusX = 0, double radiusY = 0, BoxShadows boxShadows = default) { if (brush == null && !PenIsVisible(pen)) - { return; - } - if (!MathUtilities.IsZero(radiusX)) { radiusX = Math.Min(radiusX, rect.Width / 2); @@ -175,20 +143,48 @@ namespace Avalonia.Media { radiusY = Math.Min(radiusY, rect.Height / 2); } - - PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + + DrawRectangleCore(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadows); + } + + /// + /// Draws a rectangle with the specified Brush and Pen. + /// + /// The brush used to fill the rectangle, or null for no fill. + /// The pen used to stroke the rectangle, or null for no stroke. + /// The rectangle bounds. + /// Box shadow effect parameters + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) + { + if (brush == null && !PenIsVisible(pen)) + return; + DrawRectangleCore(brush, pen, rrect, boxShadows); } + protected abstract void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default); + /// /// Draws the outline of a rectangle. /// /// The pen. /// The rectangle bounds. /// The corner radius. - public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) - { + public void DrawRectangle(IPen pen, Rect rect, float cornerRadius = 0.0f) => DrawRectangle(null, pen, rect, cornerRadius, cornerRadius); - } + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) => + DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); /// /// Draws an ellipse with the specified Brush and Pen. @@ -204,35 +200,50 @@ namespace Avalonia.Media /// public void DrawEllipse(IBrush? brush, IPen? pen, Point center, double radiusX, double radiusY) { - if (brush == null && !PenIsVisible(pen)) + if (brush != null || PenIsVisible(pen)) { - return; + var originX = center.X - radiusX; + var originY = center.Y - radiusY; + var width = radiusX * 2; + var height = radiusY * 2; + DrawEllipseCore(brush, pen, new Rect(originX, originY, width, height)); } - - var originX = center.X - radiusX; - var originY = center.Y - radiusY; - var width = radiusX * 2; - var height = radiusY * 2; - - PlatformImpl.DrawEllipse(brush, pen, new Rect(originX, originY, width, height)); + } + + /// + /// Draws an ellipse with the specified Brush and Pen. + /// + /// The brush used to fill the ellipse, or null for no fill. + /// The pen used to stroke the ellipse, or null for no stroke. + /// The bounding rect. + /// + /// The brush and the pen can both be null. If the brush is null, then no fill is performed. + /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible. + /// + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + if (brush != null || PenIsVisible(pen)) + DrawEllipseCore(brush, pen, rect); } + protected abstract void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect); + /// /// Draws a custom drawing operation /// /// custom operation - public void Custom(ICustomDrawOperation custom) => PlatformImpl.Custom(custom); + public abstract void Custom(ICustomDrawOperation custom); /// /// Draws text. /// /// The upper-left corner of the text. /// The text. - public void DrawText(FormattedText text, Point origin) + public virtual void DrawText(FormattedText text, Point origin) { _ = text ?? throw new ArgumentNullException(nameof(text)); - text.Draw(this, origin); + text.Draw(this, origin); } /// @@ -240,30 +251,31 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + public abstract void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun); + + public record struct PushedState : IDisposable { - _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + private readonly DrawingContext _context; + private readonly int _level; - if (foreground != null) + public PushedState(DrawingContext context) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + _context = context; + _level = _context._states!.Count; } - } - /// - /// Draws a filled rectangle. - /// - /// The brush. - /// The rectangle bounds. - /// The corner radius. - public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - { - DrawRectangle(brush, null, rect, cornerRadius, cornerRadius); + public void Dispose() + { + if(_context?._states == null) + return; + if(_context._states.Count != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._states.Pop().Dispose(); + } } - - public readonly record struct PushedState : IDisposable + + private readonly record struct RestoreState : IDisposable { - private readonly int _level; private readonly DrawingContext _context; private readonly Matrix _matrix; private readonly PushedStateType _type; @@ -271,62 +283,56 @@ namespace Avalonia.Media public enum PushedStateType { None, - Matrix, + Transform, Opacity, Clip, - MatrixContainer, GeometryClip, OpacityMask, + BitmapBlendMode } - public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default) + public RestoreState(DrawingContext context, PushedStateType type) { - if (context._states is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _context = context; _type = type; - _matrix = matrix; - _level = context._currentLevel += 1; - context._states.Push(this); } public void Dispose() { if (_type == PushedStateType.None) return; - if (_context._states is null || _context._transformContainers is null) + if (_context._states is null) throw new ObjectDisposedException(nameof(DrawingContext)); - if (_context._currentLevel != _level) - throw new InvalidOperationException("Wrong Push/Pop state order"); - _context._currentLevel--; - _context._states.Pop(); - if (_type == PushedStateType.Matrix) - _context.CurrentTransform = _matrix; + if (_type == PushedStateType.Transform) + _context.PopTransformCore(); else if (_type == PushedStateType.Clip) - _context.PlatformImpl.PopClip(); + _context.PopClipCore(); else if (_type == PushedStateType.Opacity) - _context.PlatformImpl.PopOpacity(); + _context.PopOpacityCore(); else if (_type == PushedStateType.GeometryClip) - _context.PlatformImpl.PopGeometryClip(); + _context.PopGeometryClipCore(); else if (_type == PushedStateType.OpacityMask) - _context.PlatformImpl.PopOpacityMask(); - else if (_type == PushedStateType.MatrixContainer) - { - var cont = _context._transformContainers.Pop(); - _context._currentContainerTransform = cont.ContainerTransform; - _context.CurrentTransform = cont.LocalTransform; - } + _context.PopOpacityMaskCore(); + else if (_type == PushedStateType.BitmapBlendMode) + _context.PopBitmapBlendModeCore(); } } - + /// + /// Pushes a clip rectangle. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. public PushedState PushClip(RoundedRect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + protected abstract void PushClipCore(RoundedRect rect); + /// /// Pushes a clip rectangle. /// @@ -334,9 +340,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { - PlatformImpl.PushClip(clip); - return new PushedState(this, PushedState.PushedStateType.Clip); + PushClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Clip)); + return new PushedState(this); } + + protected abstract void PushClipCore(Rect rect); /// /// Pushes a clip geometry. @@ -345,29 +355,28 @@ namespace Avalonia.Media /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { - _ = clip ?? throw new ArgumentNullException(nameof(clip)); - - // HACK: This check was added when nullable annotations pointed out that we're potentially - // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but - // I don't want to make that change as part of adding nullable annotations. - if (clip.PlatformImpl is null) - throw new InvalidOperationException("Cannot push empty geometry clip."); - - PlatformImpl.PushGeometryClip(clip.PlatformImpl); - return new PushedState(this, PushedState.PushedStateType.GeometryClip); + PushGeometryClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); + return new PushedState(this); } + + protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) - //TODO: Eliminate platform-specific push opacity call + public PushedState PushOpacity(double opacity, Rect bounds) { - PlatformImpl.PushOpacity(opacity); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -379,70 +388,53 @@ namespace Avalonia.Media /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { - PlatformImpl.PushOpacityMask(mask, bounds); - return new PushedState(this, PushedState.PushedStateType.OpacityMask); + PushOpacityMaskCore(mask, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.OpacityMask)); + return new PushedState(this); } + protected abstract void PushOpacityMaskCore(IBrush mask, Rect bounds); - /// - /// Pushes a matrix post-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform * matrix); - - /// - /// Pushes a matrix pre-transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix * CurrentTransform); - - /// - /// Sets the current matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public PushedState PushSetTransform(Matrix matrix) + public PushedState PushBitmapBlendMode(BitmapBlendingMode blendingMode) { - var oldMatrix = CurrentTransform; - CurrentTransform = matrix; - - return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + PushBitmapBlendMode(blendingMode); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.BitmapBlendMode)); + return new PushedState(this); } - /// - /// Pushes a new transform context. - /// - /// A disposable used to undo the transformation. - public PushedState PushTransformContainer() - { - if (_transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); - _currentContainerTransform = CurrentTransform * _currentContainerTransform; - _currentTransform = Matrix.Identity; - return new PushedState(this, PushedState.PushedStateType.MatrixContainer); - } + protected abstract void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode); /// - /// Disposes of any resources held by the . + /// Pushes a matrix transformation. /// - public void Dispose() + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushTransform(Matrix matrix) { - if (_states is null || _transformContainers is null) - throw new ObjectDisposedException(nameof(DrawingContext)); - while (_states.Count != 0) - _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; - if (_transformContainers.Count != 0) - throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; - if (_ownsImpl) - PlatformImpl.Dispose(); + PushTransformCore(matrix); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Transform)); + return new PushedState(this); } + [Obsolete("Use PushTransform")] + public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix); + [Obsolete("Use PushTransform")] + public PushedState PushTransformContainer() => PushTransform(Matrix.Identity); + + + protected abstract void PushTransformCore(Matrix matrix); + + protected abstract void PopClipCore(); + protected abstract void PopGeometryClipCore(); + protected abstract void PopOpacityCore(); + protected abstract void PopOpacityMaskCore(); + protected abstract void PopBitmapBlendModeCore(); + protected abstract void PopTransformCore(); + private static bool PenIsVisible(IPen? pen) { return pen?.Brush != null && pen.Thickness > 0; diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index b7abda2c61..812d315912 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,17 +67,16 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -103,7 +102,7 @@ namespace Avalonia.Media return rect; } - private class DrawingGroupDrawingContext : IDrawingContextImpl + private sealed class DrawingGroupDrawingContext : DrawingContext { private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); @@ -133,17 +132,7 @@ namespace Avalonia.Media _drawingGroup = drawingGroup; } - public Matrix Transform - { - get => _transform; - set - { - _transform = value; - PushTransform(new MatrixTransform(value)); - } - } - - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if ((brush == null) && (pen == null)) { @@ -157,7 +146,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { if ((brush == null) && (pen == null)) { @@ -167,7 +156,7 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground == null) { @@ -177,124 +166,70 @@ namespace Avalonia.Media GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = new GlyphRun(glyphRun) + GlyphRun = glyphRun }; // Add Drawing to the Drawing graph AddDrawing(glyphRunDrawing); } - public void DrawLine(IPen? pen, Point p1, Point p2) - { - if (pen == null) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); - } - - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) - { - if ((brush == null) && (pen == null)) - { - return; - } - - // Instantiate the geometry - var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); - - // Add Drawing to the Drawing graph - AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); - } - - public void Clear(Color color) - { - throw new NotImplementedException(); - } - - public IDrawingContextLayerImpl CreateLayer(Size size) - { - throw new NotImplementedException(); - } - - public void Custom(ICustomDrawOperation custom) - { - throw new NotImplementedException(); - } - - public object? GetFeature(Type t) => null; - - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + protected override void PushClipCore(RoundedRect rect) { throw new NotImplementedException(); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + protected override void PushClipCore(Rect rect) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + protected override void PushGeometryClipCore(Geometry clip) { throw new NotImplementedException(); } - public void PopClip() + protected override void PushOpacityCore(double opacity, Rect bounds) { throw new NotImplementedException(); } - public void PopGeometryClip() + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { throw new NotImplementedException(); } - public void PopOpacity() + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { throw new NotImplementedException(); } - public void PopOpacityMask() + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); } - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void DrawLineCore(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); - } - - public void PushClip(Rect clip) - { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - public void PushClip(RoundedRect clip) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); } - public void PushGeometryClip(IGeometryImpl clip) + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect); - public void PushOpacity(double opacity) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -364,22 +299,34 @@ namespace Avalonia.Media // Restore the previous value of the current drawing group _currentDrawingGroup = _previousDrawingGroupStack.Pop(); } - + /// /// PushTransform - /// Push a Transform which will apply to all drawing operations until the corresponding /// Pop. /// - /// The Transform to push. - private void PushTransform(Transform transform) + /// The transform to push. + protected override void PushTransformCore(Matrix matrix) { // Instantiate a new drawing group and set it as the _currentDrawingGroup var drawingGroup = PushNewDrawingGroup(); // Set the transform on the new DrawingGroup - drawingGroup.Transform = transform; + drawingGroup.Transform = new MatrixTransform(matrix); } + protected override void PopClipCore() => Pop(); + + protected override void PopGeometryClipCore() => Pop(); + + protected override void PopOpacityCore() => Pop(); + + protected override void PopOpacityMaskCore() => Pop(); + + protected override void PopBitmapBlendModeCore() => Pop(); + + protected override void PopTransformCore() => Pop(); + /// /// Creates a new DrawingGroup for a Push* call by setting the /// _currentDrawingGroup to a newly instantiated DrawingGroup, diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 1b22a1ee69..52fbd87db7 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -62,7 +62,7 @@ namespace Avalonia.Media -sourceRect.Y + destRect.Y - bounds.Y); using (context.PushClip(destRect)) - using (context.PushPreTransform(translate * scale)) + using (context.PushTransform(translate * scale)) { Drawing?.Draw(context); } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 3b63a98720..d4640390d7 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -877,7 +877,7 @@ namespace Avalonia.Media var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps)); + TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection)); var collapsedLine = line.Collapse(collapsingProperties); diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 732f1957d0..07fd2d56fa 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -12,6 +12,6 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap Source { get; } + IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs new file mode 100644 index 0000000000..df72dd1ace --- /dev/null +++ b/src/Avalonia.Base/Media/ISceneBrush.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface ISceneBrush : ITileBrush + { + ISceneBrushContent? CreateContent(); + } + + [NotClientImplementable] + public interface ISceneBrushContent : IImmutableBrush, IDisposable + { + ITileBrush Brush { get; } + Rect Rect { get; } + void Render(IDrawingContextImpl context, Matrix? transform); + internal bool UseScalableRasterization { get; } + } + + internal class ImmutableSceneBrush : ImmutableTileBrush + { + public ImmutableSceneBrush(ITileBrush source) : base(source) + { + } + } +} diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs deleted file mode 100644 index a7d3e4da10..0000000000 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - /// - /// Paints an area with an . - /// - [NotClientImplementable] - public interface IVisualBrush : ITileBrush - { - /// - /// Gets the visual to draw. - /// - Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 2f2a0fb627..718ebf1686 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -11,8 +11,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); static ImageBrush() { @@ -30,7 +30,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap source) + public ImageBrush(IBitmap? source) { Source = source; } @@ -38,7 +38,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap Source + public IBitmap? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 6577532891..c4720d772e 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { - context.PlatformImpl.DrawBitmap( + context.DrawBitmap( PlatformImpl, 1, sourceRect, diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 88e5e627ee..e77dd9d1ab 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget + public class RenderTargetBitmap : Bitmap, IDisposable { /// /// Initializes a new instance of the class. @@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging /// Renders a visual to the . /// /// The visual to render. - public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); + public void Render(Visual visual) + { + using (var ctx = CreateDrawingContext()) + ImmediateRenderer.Render(visual, ctx); + } /// /// Creates a platform-specific implementation for a . @@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging return factory.CreateRenderTargetBitmap(size, dpi); } - /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); - - bool IRenderTarget.IsCorrupted => false; + public DrawingContext CreateDrawingContext() + { + var platform = PlatformImpl.Item.CreateDrawingContext(); + platform.Clear(Colors.Transparent); + return new PlatformDrawingContext(platform); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7d9534c414..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } @@ -353,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index f9892bf60c..668a907fdf 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -24,13 +24,13 @@ namespace Avalonia.Media.Immutable /// The tile mode. /// The bitmap interpolation mode. public ImmutableImageBrush( - IBitmap source, + IBitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, double opacity = 1, ImmutableTransform? transform = null, - RelativePoint transformOrigin = new RelativePoint(), + RelativePoint transformOrigin = default, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, TileMode tileMode = TileMode.None, @@ -61,6 +61,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap Source { get; } + public IBitmap? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index 0b625080e3..0000000000 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media.Imaging; - -namespace Avalonia.Media.Immutable -{ - /// - /// Paints an area with an . - /// - internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush - { - /// - /// Initializes a new instance of the class. - /// - /// The visual to draw. - /// The horizontal alignment of a tile in the destination. - /// The vertical alignment of a tile in the destination. - /// The rectangle on the destination in which to paint a tile. - /// The opacity of the brush. - /// The transform of the brush. - /// The transform origin of the brush - /// The rectangle of the source image that will be displayed. - /// - /// How the source rectangle will be stretched to fill the destination rect. - /// - /// The tile mode. - /// Controls the quality of interpolation. - public ImmutableVisualBrush( - Visual visual, - AlignmentX alignmentX = AlignmentX.Center, - AlignmentY alignmentY = AlignmentY.Center, - RelativeRect? destinationRect = null, - double opacity = 1, - ImmutableTransform? transform = null, - RelativePoint transformOrigin = default, - RelativeRect? sourceRect = null, - Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) - : base( - alignmentX, - alignmentY, - destinationRect ?? RelativeRect.Fill, - opacity, - transform, - transformOrigin, - sourceRect ?? RelativeRect.Fill, - stretch, - tileMode, - bitmapInterpolationMode) - { - Visual = visual; - } - - /// - /// Initializes a new instance of the class. - /// - /// The brush from which this brush's properties should be copied. - public ImmutableVisualBrush(IVisualBrush source) - : base(source) - { - Visual = source.Visual; - } - - /// - public Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs new file mode 100644 index 0000000000..eb8a93722c --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport +{ + private readonly IDrawingContextImpl _impl; + private readonly bool _ownsImpl; + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _transforms; + + + public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true) + { + _impl = impl; + _ownsImpl = ownsImpl; + } + + protected override void DrawLineCore(IPen pen, Point p1, Point p2) => + _impl.DrawLine(pen, p1, p2); + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) => + _impl.DrawGeometry(brush, pen, geometry); + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default) => + _impl.DrawRectangle(brush, pen, rrect, boxShadows); + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); + + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + + public override void Custom(ICustomDrawOperation custom) => + custom.Render(_impl); + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + if (foreground != null) + _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + } + + protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); + + protected override void PushClipCore(Rect rect) => _impl.PushClip(rect); + + protected override void PushGeometryClipCore(Geometry clip) => + _impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException()); + + protected override void PushOpacityCore(double opacity, Rect bounds) => + _impl.PushOpacity(opacity, bounds); + + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) => + _impl.PushOpacityMask(mask, bounds); + + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) => + _impl.PushBitmapBlendMode(blendingMode); + + protected override void PushTransformCore(Matrix matrix) + { + _transforms ??= TransformStackPool.Get(); + var current = _impl.Transform; + _transforms.Push(current); + _impl.Transform = matrix * current; + } + + protected override void PopClipCore() => _impl.PopClip(); + + protected override void PopGeometryClipCore() => _impl.PopGeometryClip(); + + protected override void PopOpacityCore() => _impl.PopOpacity(); + + protected override void PopOpacityMaskCore() => _impl.PopOpacityMask(); + + protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode(); + + protected override void PopTransformCore() => + _impl.Transform = + (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); + + protected override void DisposeCore() + { + if (_ownsImpl) + _impl.Dispose(); + if (_transforms != null) + { + if (_transforms.Count != 0) + throw new InvalidOperationException("Not all states are disposed"); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } + } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport idc) + idc.DrawRectangle(material, rect); + else + DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect); + } +} diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 40ba613717..9b7bf3f74c 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -6,11 +6,13 @@ namespace Avalonia.Media { public readonly double Width; public readonly TextRunProperties TextRunProperties; + public readonly FlowDirection FlowDirection; - public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties) + public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; TextRunProperties = textRunProperties; + FlowDirection = flowDirection; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 0d85f3e7c5..c1b9b77401 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting return; } - if (lineImpl.NewLineLength > 0) - { - return; - } - - if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) - { - return; - } - var breakOportunities = new Queue(); var currentPosition = textLine.FirstTextSourceIndex; @@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting continue; } - var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 7f23ac98b4..568148e15c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting internal SplitResult Split(int length) { - if (IsReversed) + var isReversed = IsReversed; + + if (isReversed) { Reverse(); - } + length = Length - length; + } #if DEBUG - if(length == 0) + if (length == 0) { throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero."); } -#endif - +#endif var splitBuffer = ShapedBuffer.Split(length); var first = new ShapedTextRun(splitBuffer.First, Properties); - #if DEBUG +#if DEBUG if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - #endif - var second = new ShapedTextRun(splitBuffer.Second!, Properties); + if (isReversed) + { + return new SplitResult(second, first); + } + return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 72882df0b5..7cdf81ecc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -15,6 +17,11 @@ /// public abstract TextRun Symbol { get; } + /// + /// Gets the flow direction that is used for collapsing. + /// + public abstract FlowDirection FlowDirection { get; } + /// /// Collapses given text line. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 4c93a1d851..6422f23dcd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting @@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting var availableWidth = properties.Width - shapedSymbol.Size.Width; - while (runIndex < textRuns.Count) + if(properties.FlowDirection== FlowDirection.LeftToRight) { - var currentRun = textRuns[runIndex]; - - switch (currentRun) + while (runIndex < textRuns.Count) { - case ShapedTextRun shapedRun: - { - currentWidth += shapedRun.Size.Width; + var currentRun = textRuns[runIndex]; - if (currentWidth > availableWidth) + switch (currentRun) + { + case ShapedTextRun shapedRun: { - if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + currentWidth += shapedRun.Size.Width; + + if (currentWidth > availableWidth) { - if (isWordEllipsis && measuredLength < textLine.Length) + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + if (isWordEllipsis && measuredLength < textLine.Length) { - var nextBreakPosition = lineBreak.PositionMeasure; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - if (nextBreakPosition >= measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - break; + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } - collapsedLength += measuredLength; + availableWidth -= shapedRun.Size.Width; - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } - availableWidth -= shapedRun.Size.Width; + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + } - break; - } + availableWidth -= drawableRun.Size.Width; - case DrawableTextRun drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) - { - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } + } - availableWidth -= drawableRun.Size.Width; + collapsedLength += currentRun.Length; - break; - } + runIndex++; } + } + else + { + runIndex = textRuns.Count - 1; + + while (runIndex >= 0) + { + var currentRun = textRuns[runIndex]; - collapsedLength += currentRun.Length; + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + currentWidth += shapedRun.Size.Width; - runIndex++; - } + if (currentWidth > availableWidth) + { + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + { + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + { + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= shapedRun.Size.Width; + + break; + } + + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= drawableRun.Size.Width; + + break; + } + } + + collapsedLength += currentRun.Length; + + runIndex--; + } + } + return null; } - private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, - TextRun shapedSymbol) + private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, + FlowDirection flowDirection, TextRun shapedSymbol) { + var textRuns = textLine.TextRuns; + if (collapsedLength <= 0) { return new[] { shapedSymbol }; } + if(flowDirection == FlowDirection.RightToLeft) + { + collapsedLength = textLine.Length - collapsedLength; + } + var objectPool = FormattingObjectPool.Instance; var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); try { - var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - return collapsedRuns; + if (flowDirection == FlowDirection.RightToLeft) + { + var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; + postSplitRuns.CopyTo(collapsedRuns, 1); + collapsedRuns[0] = shapedSymbol; + return collapsedRuns; + } + else + { + var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } finally { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4dbc472133..a382416b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting var lastTrailingIndex = 0; - if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; @@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting { lastTrailingIndex += textEndOfLine.Length; } - } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var whitespaceWidth = 0d; + var justificationWidth = MaxWidth; - for (var i = 0; i < textLines.Count; i++) + if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - var line = textLines[i]; - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - - if (lineWhitespaceWidth > whitespaceWidth) - { - whitespaceWidth = lineWhitespaceWidth; - } + justificationWidth = width; } - var justificationWidth = width - whitespaceWidth; - if (justificationWidth > 0) { var justificationProperties = new InterWordJustification(justificationWidth); - for (var i = 0; i < textLines.Count - 1; i++) + for (var i = 0; i < textLines.Count; i++) { var line = textLines[i]; @@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting /// The . private TextCollapsingProperties? GetCollapsingProperties(double width) { - if(_textTrimming == TextTrimming.None) + if (_textTrimming == TextTrimming.None) { return null; } - return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); + return _textTrimming.CreateCollapsingProperties( + new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection)); } public void Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 2e85b1e187..a21a5d45e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting /// Length of leading prefix. /// width in which collapsing is constrained to /// text run properties of ellipsis symbol + /// the flow direction of the collapes line. public TextLeadingPrefixCharacterEllipsis( string ellipsis, int prefixLength, double width, - TextRunProperties textRunProperties) + TextRunProperties textRunProperties, + FlowDirection flowDirection) { if (_prefixLength < 0) { @@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting _prefixLength = prefixLength; Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 187b3154ad..b3321d4d9f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting currentX += drawableTextRun.Size.Width; } - if(lastRunIndex - 1 < 0) + if (lastRunIndex - 1 < 0) { break; } @@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting directionalWidth -= drawableTextRun.Size.Width; } - if(firstRunIndex + 1 == _textRuns.Length) + if (firstRunIndex + 1 == _textRuns.Length) { break; } @@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override void Dispose() @@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting } } - if (index == lastRunIndex) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength += textRun.GlyphRun.Metrics.NewLineLength; - } - widthIncludingWhitespace += textRun.Size.Width; break; @@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - } - if (drawableTextRun.Size.Height > height) { height = drawableTextRun.Size.Height; @@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting } } + width = widthIncludingWhitespace; + + for (var i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextRun shapedText) + { + var glyphRun = shapedText.GlyphRun; + var glyphRunMetrics = glyphRun.Metrics; + + newLineLength += glyphRunMetrics.NewLineLength; + + if (glyphRunMetrics.TrailingWhitespaceLength == 0) + { + break; + } + + trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; + + var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + + width -= whitespaceWidth; + } + } + var start = GetParagraphOffsetX(width, widthIncludingWhitespace); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) @@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, start); case TextAlignment.Right: - return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + return Math.Max(0, _paragraphWidth - width); default: return 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index ccae99cc75..8a6607bce2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -12,10 +12,13 @@ /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) + /// The flow direction of the collapsed line. + public TextTrailingCharacterEllipsis(string ellipsis, double width, + TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -24,6 +27,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c622c76a60..5252766382 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -12,14 +12,17 @@ /// Text used as collapsing symbol. /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. + /// flow direction of the collapsed line. public TextTrailingWordEllipsis( string ellipsis, double width, - TextRunProperties textRunProperties + TextRunProperties textRunProperties, + FlowDirection flowDirection ) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -28,6 +31,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 7ba25eb005..19e6a70357 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) { - return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 2edbaabbc6..8a3c5aa397 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -17,10 +17,10 @@ namespace Avalonia.Media { if (_isWordBased) { - return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } - return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 2be3e9a94e..6bfe20271f 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,11 +1,14 @@ using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush + public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender { /// /// Defines the property. @@ -43,10 +46,23 @@ namespace Avalonia.Media set { SetValue(VisualProperty, value); } } - /// - IImmutableBrush IMutableBrush.ToImmutable() + ISceneBrushContent? ISceneBrush.CreateContent() { - return new ImmutableVisualBrush(this); + if (Visual == null) + return null; + + if (Visual is IVisualBrushInitialize initialize) + initialize.EnsureInitialized(); + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + new(Visual.Bounds.Size), false); } } } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8509067cd0..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Platform/IGeometryImpl.cs b/src/Avalonia.Base/Platform/IGeometryImpl.cs index 5826cfb2ff..d1964bf07e 100644 --- a/src/Avalonia.Base/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Base/Platform/IGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Metadata; @@ -47,7 +48,7 @@ namespace Avalonia.Platform /// The stroke to use. /// The point. /// true if the geometry contains the point; otherwise, false. - bool StrokeContains(IPen pen, Point point); + bool StrokeContains(IPen? pen, Point point); /// /// Makes a clone of the geometry with the specified transform. @@ -87,6 +88,7 @@ namespace Avalonia.Platform /// If ture, the resulting snipped path will start with a BeginFigure call. /// The resulting snipped path. /// If the snipping operation is successful. - bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry); } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 73e9e58da4..31ad84341d 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -14,11 +14,7 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// - /// A render to be used to render visual brushes. May be null if no visual brushes are - /// to be drawn. - /// - IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); + IDrawingContextImpl CreateDrawingContext(); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5bf9ff9d9a..543fb0ab74 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFile : IStorageBookmarkFile { - public BclStorageFile(string fileName) - { - FileInfo = new FileInfo(fileName); - } - public BclStorageFile(FileInfo fileInfo) { FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 1e21c197bb..d8e3d91f75 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFolder : IStorageBookmarkFolder { - public BclStorageFolder(string path) - { - DirectoryInfo = new DirectoryInfo(path); - if (!DirectoryInfo.Exists) - { - throw new ArgumentException("Directory must exist"); - } - } - public BclStorageFolder(DirectoryInfo directoryInfo) { DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 55e84ee937..a8cbffb417 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO; internal static class StorageProviderHelpers { + public static IStorageItem? TryCreateBclStorageItem(string path) + { + var directory = new DirectoryInfo(path); + if (directory.Exists) + { + return new BclStorageFolder(directory); + } + + var file = new FileInfo(path); + if (file.Exists) + { + return new BclStorageFile(file); + } + + return null; + } + public static Uri FilePathToUri(string path) { var uriPath = new StringBuilder(path) diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs index 6f97916a26..ed061aa2d5 100644 --- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -12,6 +12,8 @@ public class PickerOptions /// /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// Can be obtained from previously picked folder or using + /// or . /// public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index 6f8b945cd6..1febb4506a 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -11,12 +11,24 @@ public static class StorageProviderExtensions /// public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); + } + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } /// public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); + } + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } diff --git a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs index e1ff0970c2..a841803ee1 100644 --- a/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs +++ b/src/Avalonia.Base/PropertyStore/BindingEntryBase.cs @@ -16,27 +16,37 @@ namespace Avalonia.PropertyStore private IDisposable? _subscription; private bool _hasValue; private TValue? _value; - private TValue? _defaultValue; - private bool _isDefaultValueInitialized; + private UncommonFields? _uncommon; protected BindingEntryBase( + AvaloniaObject target, ValueFrame frame, AvaloniaProperty property, IObservable> source) + : this(target, frame, property, (object)source) { - Frame = frame; - Source = source; - Property = property; } protected BindingEntryBase( + AvaloniaObject target, ValueFrame frame, AvaloniaProperty property, IObservable source) + : this(target, frame, property, (object)source) + { + } + + private BindingEntryBase( + AvaloniaObject target, + ValueFrame frame, + AvaloniaProperty property, + object source) { Frame = frame; - Source = source; Property = property; + Source = source; + if (property.GetMetadata(target.GetType()).EnableDataValidation == true) + _uncommon = new() { _hasDataValidation = true }; } public bool HasValue @@ -68,6 +78,20 @@ namespace Avalonia.PropertyStore return _value!; } + public bool GetDataValidationState(out BindingValueType state, out Exception? error) + { + if (_uncommon?._hasDataValidation == true) + { + state = _uncommon._dataValidationState; + error = _uncommon._dataValidationError; + return true; + } + + state = BindingValueType.Value; + error = null; + return false; + } + public void Start() => Start(true); public void OnCompleted() => BindingCompleted(); @@ -111,16 +135,28 @@ namespace Avalonia.PropertyStore { static void Execute(BindingEntryBase instance, BindingValue value) { - if (instance.Frame.Owner is null) + if (instance.Frame.Owner is not { } valueStore) return; - LoggingUtils.LogIfNecessary(instance.Frame.Owner.Owner, instance.Property, value); + var owner = valueStore.Owner; + var property = instance.Property; + var originalType = value.Type; + + LoggingUtils.LogIfNecessary(owner, property, value); - var effectiveValue = value.HasValue ? value.Value : instance.GetCachedDefaultValue(); + if (!value.HasValue && value.Type != BindingValueType.DataValidationError) + value = value.WithValue(instance.GetCachedDefaultValue()); - if (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, effectiveValue)) + if (instance._uncommon?._hasDataValidation == true) { - instance._value = effectiveValue; + instance._uncommon._dataValidationState = value.Type; + instance._uncommon._dataValidationError = value.Error; + } + + if (value.HasValue && + (!instance._hasValue || !EqualityComparer.Default.Equals(instance._value, value.Value))) + { + instance._value = value.Value; instance._hasValue = true; if (instance._subscription is not null && instance._subscription != s_creatingQuiet) instance.Frame.Owner?.OnBindingValueChanged(instance, instance.Frame.Priority); @@ -152,13 +188,23 @@ namespace Avalonia.PropertyStore private TValue GetCachedDefaultValue() { - if (!_isDefaultValueInitialized) + if (_uncommon?._isDefaultValueInitialized != true) { - _defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); - _isDefaultValueInitialized = true; + _uncommon ??= new(); + _uncommon._defaultValue = GetDefaultValue(Frame.Owner!.Owner.GetType()); + _uncommon._isDefaultValueInitialized = true; } - return _defaultValue!; + return _uncommon._defaultValue!; + } + + private class UncommonFields + { + public TValue? _defaultValue; + public bool _isDefaultValueInitialized; + public bool _hasDataValidation; + public BindingValueType _dataValidationState; + public Exception? _dataValidationError; } } } diff --git a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs index cbe2435953..4bf98e3f7b 100644 --- a/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectBindingObserver.cs @@ -9,11 +9,13 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; public DirectBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; Property = property; } @@ -33,10 +35,17 @@ namespace Avalonia.PropertyStore { _subscription?.Dispose(); _subscription = null; + OnCompleted(); + } + + public void OnCompleted() + { _owner.OnLocalValueBindingCompleted(Property, this); + + if (_hasDataValidation) + _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); } - public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); public void OnError(Exception error) => OnCompleted(); public void OnNext(T value) diff --git a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs index 5d60b44bef..1cf108df9b 100644 --- a/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/DirectUntypedBindingObserver.cs @@ -10,11 +10,13 @@ namespace Avalonia.PropertyStore IDisposable { private readonly ValueStore _owner; + private readonly bool _hasDataValidation; private IDisposable? _subscription; public DirectUntypedBindingObserver(ValueStore owner, DirectPropertyBase property) { _owner = owner; + _hasDataValidation = property.GetMetadata(owner.Owner.GetType())?.EnableDataValidation ?? false; Property = property; } @@ -30,6 +32,9 @@ namespace Avalonia.PropertyStore _subscription?.Dispose(); _subscription = null; _owner.OnLocalValueBindingCompleted(Property, this); + + if (_hasDataValidation) + _owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); } public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs index 78f0ad46b7..11a4dd7893 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue.cs @@ -11,9 +11,6 @@ namespace Avalonia.PropertyStore /// internal abstract class EffectiveValue { - private IValueEntry? _valueEntry; - private IValueEntry? _baseValueEntry; - /// /// Gets the current effective value as a boxed value. /// @@ -29,6 +26,16 @@ namespace Avalonia.PropertyStore /// public BindingPriority BasePriority { get; protected set; } + /// + /// Gets the active value entry for the current effective value. + /// + public IValueEntry? ValueEntry { get; private set; } + + /// + /// Gets the active value entry for the current base value. + /// + public IValueEntry? BaseValueEntry { get; private set; } + /// /// Gets a value indicating whether the was overridden by a call to /// . @@ -63,14 +70,14 @@ namespace Avalonia.PropertyStore { if (Priority == BindingPriority.Unset) { - _valueEntry?.Unsubscribe(); - _valueEntry = null; + ValueEntry?.Unsubscribe(); + ValueEntry = null; } if (BasePriority == BindingPriority.Unset) { - _baseValueEntry?.Unsubscribe(); - _baseValueEntry = null; + BaseValueEntry?.Unsubscribe(); + BaseValueEntry = null; } } @@ -135,40 +142,34 @@ namespace Avalonia.PropertyStore // value, then the current entry becomes our base entry. if (Priority > BindingPriority.LocalValue && Priority < BindingPriority.Inherited) { - Debug.Assert(_valueEntry is not null); - _baseValueEntry = _valueEntry; - _valueEntry = null; + Debug.Assert(ValueEntry is not null); + BaseValueEntry = ValueEntry; + ValueEntry = null; } - if (_valueEntry != entry) + if (ValueEntry != entry) { - _valueEntry?.Unsubscribe(); - _valueEntry = entry; + ValueEntry?.Unsubscribe(); + ValueEntry = entry; } } else if (Priority <= BindingPriority.Animation) { // We've received a non-animation value and have an active animation value, so the // new entry becomes our base entry. - if (_baseValueEntry != entry) + if (BaseValueEntry != entry) { - _baseValueEntry?.Unsubscribe(); - _baseValueEntry = entry; + BaseValueEntry?.Unsubscribe(); + BaseValueEntry = entry; } } - else if (_valueEntry != entry) + else if (ValueEntry != entry) { // Both the current value and the new value are non-animation values, so the new // entry replaces the existing entry. - _valueEntry?.Unsubscribe(); - _valueEntry = entry; + ValueEntry?.Unsubscribe(); + ValueEntry = entry; } } - - protected void UnsubscribeValueEntries() - { - _valueEntry?.Unsubscribe(); - _baseValueEntry?.Unsubscribe(); - } } } diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index c469034f9b..0788b39459 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Avalonia.Data; +using static Avalonia.Rendering.Composition.Animations.PropertySetSnapshot; namespace Avalonia.PropertyStore { @@ -61,6 +62,12 @@ namespace Avalonia.PropertyStore UpdateValueEntry(value, priority); SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority, false); + + if (priority > BindingPriority.LocalValue && + value.GetDataValidationState(out var state, out var error)) + { + owner.Owner.OnUpdateDataValidation(value.Property, state, error); + } } public void SetLocalValueAndRaise( @@ -128,12 +135,10 @@ namespace Avalonia.PropertyStore public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) { - UnsubscribeValueEntries(); - DisposeAndRaiseUnset(owner, (StyledProperty)property); - } + ValueEntry?.Unsubscribe(); + BaseValueEntry?.Unsubscribe(); - public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property) - { + var p = (StyledProperty)property; BindingPriority priority; T oldValue; @@ -150,9 +155,16 @@ namespace Avalonia.PropertyStore if (!EqualityComparer.Default.Equals(oldValue, Value)) { - owner.Owner.RaisePropertyChanged(property, Value, oldValue, priority, true); + owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true); if (property.Inherits) - owner.OnInheritedEffectiveValueDisposed(property, Value); + owner.OnInheritedEffectiveValueDisposed(p, Value); + } + + if (ValueEntry?.GetDataValidationState(out _, out _) ?? + BaseValueEntry?.GetDataValidationState(out _, out _) ?? + false) + { + owner.Owner.OnUpdateDataValidation(p, BindingValueType.UnsetValue, null); } } diff --git a/src/Avalonia.Base/PropertyStore/IValueEntry.cs b/src/Avalonia.Base/PropertyStore/IValueEntry.cs index 271d85f8bc..5898bef491 100644 --- a/src/Avalonia.Base/PropertyStore/IValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IValueEntry.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; namespace Avalonia.PropertyStore { @@ -22,6 +23,16 @@ namespace Avalonia.PropertyStore /// object? GetValue(); + /// + /// Gets the data validation state if supported. + /// + /// The binding validation state. + /// The current binding error, if any. + /// + /// True if the entry supports data validation, otherwise false. + /// + bool GetDataValidationState(out BindingValueType state, out Exception? error); + /// /// Called when the value entry is removed from the value store. /// diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs index d8a353dc70..16b96eff5d 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; namespace Avalonia.PropertyStore { @@ -27,5 +28,12 @@ namespace Avalonia.PropertyStore object? IValueEntry.GetValue() => _value; T IValueEntry.GetValue() => _value; + + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } } } diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 7e9f3ab312..222d857aa3 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable> source) { - var e = new TypedBindingEntry(this, property, source); + var e = new TypedBindingEntry(Owner!.Owner, this, property, source); Add(e); return e; } @@ -27,7 +27,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable source) { - var e = new TypedBindingEntry(this, property, source); + var e = new TypedBindingEntry(Owner!.Owner, this, property, source); Add(e); return e; } @@ -36,7 +36,7 @@ namespace Avalonia.PropertyStore StyledProperty property, IObservable 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 98be861afa..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,9 +41,9 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; @@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index f5312ad39b..0000000000 --- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - [Unstable] - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 8e5dc38317..4a12e78817 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -14,19 +14,8 @@ namespace Avalonia.Rendering /// a simple tree traversal. /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer + internal class ImmediateRenderer { - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(Visual visual, IRenderTarget target) - { - using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); - Render(context, visual, visual.Bounds); - } - /// /// Renders a visual to a drawing context. /// @@ -36,28 +25,6 @@ namespace Avalonia.Rendering { Render(context, visual, visual.Bounds); } - - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? default; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (brush.Visual is { } visual) - { - Render(new DrawingContext(context), visual, visual.Bounds); - } - } - - internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) - { - Render(context, visual, visual.Bounds); - } private static Rect GetTransformedBounds(Visual visual) { @@ -75,7 +42,7 @@ namespace Avalonia.Rendering } - private static void Render(DrawingContext context, Visual visual, Rect clipRect) + public static void Render(DrawingContext context, Visual visual, Rect clipRect) { var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; @@ -117,7 +84,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index e81966ce81..62fc73db44 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : DrawOperation + internal abstract class BrushDrawOperation : DrawOperationWithTransform { - public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) + public IImmutableBrush? Brush { get; } + + public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush) : base(bounds, transform) { - Aux = aux; + Brush = brush; } - /// - /// Auxiliary data required to draw the brush - /// - public IDisposable? Aux { get; } - public override void Dispose() { - Aux?.Dispose(); + (Brush as ISceneBrushContent)?.Dispose(); base.Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index e1bfaa4aa3..782e287989 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a clip push or pop. /// - internal class ClipNode : IDrawOperation + internal class ClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index b7311936d3..ff2616bfe4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -4,30 +4,19 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperation + internal sealed class CustomDrawOperation : DrawOperationWithTransform { - public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) : base(custom.Bounds, transform) { - Transform = transform; Custom = custom; } - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - return Custom.HitTest(p * Transform.Invert()); - } - - return false; - } + public override bool HitTest(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; Custom.Render(context); } @@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph Transform == transform && Custom?.Equals(custom) == true; } - public interface ICustomDrawOperation : IDrawOperation, IEquatable + public interface ICustomDrawOperation : IEquatable, IDisposable { - + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index c49e7705e0..5b93cd8cfc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph { } } + + internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform + { + protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform) + { + Transform = transform; + } + + public Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index 4600653b9d..d5f0270cb2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph { public EllipseNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - Rect rect, - IDisposable? aux = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) + Rect rect) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; } - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// public ImmutablePen? Pen { get; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the rect of the ellipse to draw. /// @@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph rect.Equals(Rect); } - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawEllipse(Brush, Pen, Rect); - } + public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); public override bool HitTest(Point p) { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - var center = Rect.Center; var strokeThickness = Pen?.Thickness ?? 0; @@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph return false; } + + public override void Dispose() + { + (Brush as ISceneBrushContent)?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 82f8fc2d56..e1f79e0e10 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a rectangle draw. /// - internal class ExperimentalAcrylicNode : DrawOperation + internal class ExperimentalAcrylicNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect) : base(rect.Rect, transform) { - Transform = transform; Material = material.ToImmutable(); Rect = rect; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - public IExperimentalAcrylicMaterial Material { get; } /// @@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if(context is IDrawingContextWithAcrylicLikeSupport idc) { idc.DrawRectangle(Material, Rect); @@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) - { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) - { - p *= Transform.Invert(); - - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } - - return false; - } + public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 842edf2bcb..8575e61de4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a geometry clip push or pop. /// - internal class GeometryClipNode : IDrawOperation + internal class GeometryClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index cf53b86fa7..3ab535897a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph /// The geometry. /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - IGeometryImpl geometry, - IDisposable? aux) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) + IGeometryImpl geometry) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } /// public override bool HitTest(Point p) { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index a2d914bdd7..4d8759f545 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, - IBrush foreground, - IRef glyphRun, - IDisposable? aux = null) - : base(new Rect(glyphRun.Item.Size), transform, aux) + IImmutableBrush foreground, + IRef glyphRun) + : base(new Rect(glyphRun.Item.Size), transform, foreground) { - Transform = transform; - Foreground = foreground.ToImmutable(); GlyphRun = glyphRun.Clone(); } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the foreground brush. - /// - public IBrush Foreground { get; } - + + /// /// Gets the glyph run to draw. /// public IRef GlyphRun { get; } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun); - } + public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); /// /// Determines if this draw operation equals another. @@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && - Equals(foreground, Foreground) && + Equals(foreground, Brush) && Equals(glyphRun.Item, GlyphRun.Item); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); public override void Dispose() { GlyphRun?.Dispose(); + base.Dispose(); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 2bfd2080c3..6a1aefe6b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation : IDisposable + internal interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. @@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph /// The drawing context. void Render(IDrawingContextImpl context); } + + internal interface IDrawOperationWithTransform : IDrawOperation + { + /// + /// Gets the transform with which the node will be drawn. + /// + Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 339881e675..dd9787e8d1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : DrawOperation + internal class ImageNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform) { - Transform = transform; Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } + } /// /// Gets the image to draw. @@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BitmapInterpolationMode BitmapInterpolationMode { get; } - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - /// /// Determines if this draw operation equals another. /// @@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 0af8ba2752..f21791d038 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a line draw. /// - internal class LineNode : BrushDrawOperation + internal class LineNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IPen pen, Point p1, - Point p2, - IDisposable? aux = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) + Point p2) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { - Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the stroke pen. /// @@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawLine(Pen, P1, P2); } public override bool HitTest(Point p) { - if (!Transform.HasInverse) - return false; - - p *= Transform.Invert(); - var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 3ecc07fa54..e10d712c2d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Auxiliary data required to draw the brush. - public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(default, Matrix.Identity, aux) + public OpacityMaskNode(IImmutableBrush mask, Rect bounds) + : base(default, Matrix.Identity, mask) { - Mask = mask.ToImmutable(); MaskBounds = bounds; } - /// - /// Initializes a new instance of the class that represents an - /// opacity mask pop. - /// - public OpacityMaskNode() - : base(default, Matrix.Identity, null) - { - } - - /// - /// Gets the mask to be pushed or null if the operation represents a pop. - /// - public IBrush? Mask { get; } - /// /// Gets the bounds of the opacity mask or null if the operation represents a pop. /// @@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; + public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; /// public override void Render(IDrawingContextImpl context) { - if (Mask != null) - { - context.PushOpacityMask(Mask, MaskBounds!.Value); - } - else - { - context.PopOpacityMask(); - } + context.PushOpacityMask(Brush!, MaskBounds!.Value); } } + + internal class OpacityMaskPopNode : DrawOperation + { + public OpacityMaskPopNode() : base(default, Matrix.Identity) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) => context.PopOpacityMask(); + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index f2ffd7411c..cee9ce9df7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows, - IDisposable? aux = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) + BoxShadows boxShadows) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; BoxShadows = boxShadows; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - - context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - } + public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// public override bool HitTest(Point p) { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) + if (Brush != null) { - p *= Transform.Invert(); - - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); - return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); - } + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); + return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); } - - return false; } } } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5b8dac2f53..82e948eea8 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -28,7 +28,7 @@ namespace Avalonia public class StyledElement : Animatable, IDataContextProvider, ILogical, - IResourceHost, + IThemeVariantHost, IStyleHost, IStyleable, ISetLogicalParent, @@ -46,6 +46,7 @@ namespace Avalonia defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, + enableDataValidation: false, notifying: DataContextNotifying); /// @@ -75,23 +76,6 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); - - /// - /// Defines the RequestedThemeVariant property. - /// - public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - defaultValue: ThemeVariant.Default); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -160,6 +144,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -278,15 +265,6 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } - /// - /// Gets the UI theme that is currently used by the element, which might be different than the . - /// - /// - /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. - /// Otherwise, current OS theme variant is returned. - /// - public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); - /// /// Gets the styled element's logical children. /// @@ -325,6 +303,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -394,7 +375,7 @@ namespace Avalonia /// public bool ApplyStyling() { - if (_initCount == 0 && (!_stylesApplied || !_themeApplied)) + if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); @@ -644,13 +625,19 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); - else if (change.Property == RequestedThemeVariantProperty) + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) { if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ActualThemeVariantProperty, themeVariant); + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); else - ClearValue(ActualThemeVariantProperty); + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index c71973fde8..6f10de3651 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -16,11 +16,13 @@ namespace Avalonia /// The default value of the property. /// The default binding mode. /// A value coercion callback. + /// Whether the property is interested in data validation. public StyledPropertyMetadata( Optional defaultValue = default, BindingMode defaultBindingMode = BindingMode.Default, - Func? coerce = null) - : base(defaultBindingMode) + Func? coerce = null, + bool enableDataValidation = false) + : base(defaultBindingMode, enableDataValidation) { _defaultValue = defaultValue; CoerceValue = coerce; diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index 33d4cd0824..8bdcec2e53 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using Avalonia.LogicalTree; namespace Avalonia.Styling.Activators @@ -13,6 +14,7 @@ namespace Avalonia.Styling.Activators private readonly int _step; private readonly int _offset; private readonly bool _reversed; + private int _index = -1; public NthChildActivator( ILogical control, @@ -28,24 +30,51 @@ namespace Avalonia.Styling.Activators protected override bool EvaluateIsActive() { - return NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + var index = _index >= 0 ? _index : _provider.GetChildIndex(_control); + return NthChildSelector.Evaluate(index, _provider, _step, _offset, _reversed).IsMatch; } - protected override void Initialize() => _provider.ChildIndexChanged += ChildIndexChanged; - protected override void Deinitialize() => _provider.ChildIndexChanged -= ChildIndexChanged; + protected override void Initialize() + { + _provider.ChildIndexChanged += ChildIndexChanged; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChanged; + } private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { // Run matching again if: - // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. - // 2. e.Child is null, when all children indices were changed. - // 3. Subscribed child index was changed. - if (_reversed - || e.Child is null - || e.Child == _control) + // 1. Subscribed child index was changed + // 2. Child indexes were reset + // 3. We're a reversed (nth-last-child) selector and total count has changed + if ((e.Child == _control || e.Action == ChildIndexChangedAction.ChildIndexesReset) || + (_reversed && e.Action == ChildIndexChangedAction.TotalCountChanged)) { + // We're using the _index field to pass the index of the child to EvaluateIsActive + // *only* when the active state is re-evaluated via this event handler. The docs + // for EvaluateIsActive say: + // + // > This method should read directly from its inputs and not rely on any + // > subscriptions to fire in order to be up-to-date. + // + // Which is good advice in general, however in this case we need to break the rule + // and use the value from the event subscription instead of calling + // IChildIndexProvider.GetChildIndex. This is because this event can be fired during + // the process of realizing an element of a virtualized list; in this case calling + // GetChildIndex may not return the correct index as the element isn't yet realized. + _index = e.Index; ReevaluateIsActive(); + _index = -1; } } + + private void TotalCountChanged(object? sender, EventArgs e) + { + if (_reversed) + ReevaluateIsActive(); + } } } diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs deleted file mode 100644 index 2467d99b3b..0000000000 --- a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Metadata; - -namespace Avalonia.Styling; - -/// -/// Interface for an application host element with a root theme variant. -/// -[Unstable] -public interface IGlobalThemeVariantProvider : IResourceHost -{ - /// - /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. - /// - ThemeVariant ActualThemeVariant { get; } - - /// - /// Raised when the theme variant is changed on the element or an ancestor of the element. - /// - event EventHandler? ActualThemeVariantChanged; -} diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs new file mode 100644 index 0000000000..01583148a8 --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for the host element with a theme variant. +/// +[Unstable] +public interface IThemeVariantHost : IResourceHost +{ + /// + /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty. + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index ccfc2c781d..532179bb2c 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -61,7 +61,7 @@ namespace Avalonia.Styling { return subscribe ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) - : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + : Evaluate(childIndexProvider.GetChildIndex(logical), childIndexProvider, Step, Offset, _reversed); } else { @@ -70,10 +70,9 @@ namespace Avalonia.Styling } internal static SelectorMatch Evaluate( - ILogical logical, IChildIndexProvider childIndexProvider, + int index, IChildIndexProvider childIndexProvider, int step, int offset, bool reversed) { - var index = childIndexProvider.GetChildIndex(logical); if (index < 0) { return SelectorMatch.NeverThisInstance; diff --git a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs index 826b45582d..be5a999771 100644 --- a/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterBindingInstance.cs @@ -15,7 +15,7 @@ namespace Avalonia.Styling AvaloniaProperty property, BindingMode mode, IObservable source) - : base(instance, property, source) + : base(target, instance, property, source) { _target = target; _mode = mode; diff --git a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs index 7a39407ba2..7604c26244 100644 --- a/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterTemplateInstance.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Data; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -19,6 +20,13 @@ namespace Avalonia.Styling public object? GetValue() => _value ??= _template.Build(); + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } + void IValueEntry.Unsubscribe() { } } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index 093597c6a0..9b009be6d2 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -90,6 +90,13 @@ namespace Avalonia.Styling object? IValueEntry.GetValue() => Value; + bool IValueEntry.GetDataValidationState(out BindingValueType state, out Exception? error) + { + state = BindingValueType.Value; + error = null; + return false; + } + private AvaloniaProperty EnsureProperty() { return Property ?? throw new InvalidOperationException("Setter.Property must be set."); @@ -99,7 +106,8 @@ namespace Avalonia.Styling { if (!Property!.IsDirect) { - var i = binding.Initiate(target, Property)!; + var hasDataValidation = Property.GetMetadata(target.GetType()).EnableDataValidation ?? false; + var i = binding.Initiate(target, Property, enableDataValidation: hasDataValidation)!; var mode = i.Mode; if (mode == BindingMode.Default) diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 8218533f4f..389136b0f5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -6,11 +6,26 @@ using Avalonia.Platform; namespace Avalonia.Styling; /// -/// Specifies a UI theme variant that should be used for the +/// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] public sealed record ThemeVariant { + /// + /// Defines the ActualThemeVariant property. + /// + internal static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + "ActualThemeVariant", + inherits: true); + + /// + /// Defines the RequestedThemeVariant property. + /// + internal static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + "RequestedThemeVariant", defaultValue: Default); + /// /// Creates a new instance of the /// diff --git a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs index 827a02334a..30b7738409 100644 --- a/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs +++ b/src/Avalonia.Base/Threading/ThreadSafeObjectPool.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Avalonia.Threading { - public class ThreadSafeObjectPool where T : class, new() + internal class ThreadSafeObjectPool where T : class, new() { private Stack _stack = new Stack(); public static ThreadSafeObjectPool Default { get; } = new ThreadSafeObjectPool(); @@ -17,11 +17,14 @@ namespace Avalonia.Threading } } - public void Return(T obj) + public void ReturnAndSetNull(ref T? obj) { + if (obj == null) + return; lock (_stack) { _stack.Push(obj); + obj = null; } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 06a77f0894..f5db7c0855 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -336,7 +336,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index f9b84793c6..fcf72385b2 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -423,7 +423,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex() { - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs index d906cd359c..5a4ddd36f4 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls.Primitives internal void InvalidateChildIndex(DataGridRow row) { - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(row, row.Index)); } /// diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index dd8575c989..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -1,6 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.6 0.8 @@ -9,19 +51,6 @@ M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z - - - - - - - - - - - @@ -29,23 +58,10 @@ - + - - - - - - - - - @@ -565,5 +581,6 @@ + diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 951e60c25b..3d3d01e06e 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -536,11 +536,12 @@ namespace Avalonia.Controls internal void OnElementPrepared(Control element, VirtualizationInfo virtInfo) { + var index = virtInfo.Index; + _viewportManager.OnElementPrepared(element, virtInfo); if (ElementPrepared != null) { - var index = virtInfo.Index; if (_elementPreparedArgs == null) { @@ -554,7 +555,7 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index)); } internal void OnElementClearing(Control element) @@ -573,7 +574,7 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, -1)); } internal void OnElementIndexChanged(Control element, int oldIndex, int newIndex) @@ -592,7 +593,7 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, newIndex)); } private void OnDataSourcePropertyChanged(ItemsSourceView? oldValue, ItemsSourceView? newValue) diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6d3ba3cf8a..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -50,13 +50,13 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - StyledElement.ActualThemeVariantProperty.AddOwner(); + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); /// public event EventHandler? ResourcesChanged; @@ -95,11 +95,8 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// - public ThemeVariant ActualThemeVariant - { - get => GetValue(ActualThemeVariantProperty); - } + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// /// Gets the current instance of the class. @@ -256,7 +253,7 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) - .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() diff --git a/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs new file mode 100644 index 0000000000..9cc0f17818 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs @@ -0,0 +1,21 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ImageAutomationPeer : ControlAutomationPeer + { + public ImageAutomationPeer(Control owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Image"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Image; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs new file mode 100644 index 0000000000..b0f83c1f2a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/RadioButtonAutomationPeer.cs @@ -0,0 +1,63 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; + +namespace Avalonia.Controls.Automation.Peers +{ + public class RadioButtonAutomationPeer : ToggleButtonAutomationPeer, ISelectionItemProvider + { + public RadioButtonAutomationPeer(RadioButton owner) : base(owner) + { + owner.PropertyChanged += (a, e) => + { + if (e.Property == RadioButton.IsCheckedProperty) + { + RaiseToggleStatePropertyChangedEvent((bool?)e.OldValue, (bool?)e.NewValue); + } + }; + } + + override protected string GetClassNameCore() + { + return "RadioButton"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.RadioButton; + } + + public bool IsSelected => ((RadioButton)Owner).IsChecked == true; + + public ISelectionProvider? SelectionContainer => null; + + public void AddToSelection() + { + if (((RadioButton)Owner).IsChecked != true) + throw new InvalidOperationException("Operation cannot be performed"); + } + + public void RemoveFromSelection() + { + if (((RadioButton)Owner).IsChecked == true) + throw new InvalidOperationException("Operation cannot be performed"); + } + + public void Select() + { + if (!IsEnabled()) + throw new InvalidOperationException("Element is disabled thus it cannot be selected"); + + ((RadioButton)Owner).IsChecked = true; + } + + internal virtual void RaiseToggleStatePropertyChangedEvent(bool? oldValue, bool? newValue) + { + RaisePropertyChangedEvent( + SelectionItemPatternIdentifiers.IsSelectedProperty, + oldValue == true, + newValue == true); + } + } +} diff --git a/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs new file mode 100644 index 0000000000..418ae1f1fe --- /dev/null +++ b/src/Avalonia.Controls/Automation/SelectionItemPatternIdentifiers.cs @@ -0,0 +1,16 @@ +using Avalonia.Automation.Provider; + +namespace Avalonia.Automation +{ + /// + /// Contains values used as identifiers by . + /// + public static class SelectionItemPatternIdentifiers + { + /// Indicates the element is currently selected. + public static AutomationProperty IsSelectedProperty { get; } = new AutomationProperty(); + + /// Indicates the element is currently selected. + public static AutomationProperty SelectionContainerProperty { get; } = new AutomationProperty(); + } +} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ab7c9948c4..325593508c 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - + /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -403,7 +403,7 @@ namespace Avalonia.Controls { if (_focusAdorner == null) { - var template = GetValue(FocusAdornerProperty); + var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner; if (template != null) { diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index 2ad6a58d38..668de5bca9 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -191,7 +191,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnCollapsing(RoutedEventArgs eventArgs) + protected virtual void OnCollapsing(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } @@ -207,7 +207,7 @@ namespace Avalonia.Controls /// /// Invoked just before the event. /// - protected virtual void OnExpanding(RoutedEventArgs eventArgs) + protected virtual void OnExpanding(CancelRoutedEventArgs eventArgs) { RaiseEvent(eventArgs); } diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index e1f840672d..dbffb803a3 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -82,7 +82,7 @@ namespace Avalonia.Controls public sealed override void Render(DrawingContext context) { - if (context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + if (context is IDrawingContextWithAcrylicLikeSupport idc) { var cornerRadius = CornerRadius; diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 3e76835e92..b14cc78e60 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,5 +1,6 @@ using Avalonia.Automation; using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; @@ -130,5 +131,10 @@ namespace Avalonia.Controls return new Size(); } } + + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ImageAutomationPeer(this); + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ce12d5f2bf..9483f98881 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -102,7 +102,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; @@ -218,7 +217,6 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } - /// public event EventHandler HorizontalSnapPointsChanged { @@ -495,6 +493,7 @@ namespace Avalonia.Controls else if (change.Property == ItemCountProperty) { UpdatePseudoClasses(change.GetNewValue()); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); } else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { @@ -579,7 +578,7 @@ namespace Avalonia.Controls internal void RegisterItemsPresenter(ItemsPresenter presenter) { Presenter = presenter; - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); } internal void PrepareItemContainer(Control container, object? item, int index) @@ -601,17 +600,14 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { - _containerBeingPrepared = new(index, container); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); - _containerBeingPrepared = null; - + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); _scrollViewer?.RegisterAnchorCandidate(container); } internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex) { ContainerIndexChangedOverride(container, oldIndex, newIndex); - _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container)); + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex)); } internal void ClearItemContainer(Control container) @@ -742,9 +738,6 @@ namespace Avalonia.Controls int IChildIndexProvider.GetChildIndex(ILogical child) { - if (_containerBeingPrepared?.Item2 == child) - return _containerBeingPrepared.Item1; - return child is Control container ? IndexFromContainer(container) : -1; } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 5588bde7c0..1670e496b4 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,6 +13,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -85,16 +86,16 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent PointerEnteredItemEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerEnteredItemEvent = + RoutedEvent.Register( nameof(PointerEnteredItem), RoutingStrategies.Bubble); /// /// Defines the event. /// - public static readonly RoutedEvent PointerExitedItemEvent = - RoutedEvent.Register( + public static readonly RoutedEvent PointerExitedItemEvent = + RoutedEvent.Register( nameof(PointerExitedItem), RoutingStrategies.Bubble); @@ -184,7 +185,7 @@ namespace Avalonia.Controls /// /// A bubbling version of the event for menu items. /// - public event EventHandler? PointerEnteredItem + public event EventHandler? PointerEnteredItem { add { AddHandler(PointerEnteredItemEvent, value); } remove { RemoveHandler(PointerEnteredItemEvent, value); } @@ -196,7 +197,7 @@ namespace Avalonia.Controls /// /// A bubbling version of the event for menu items. /// - public event EventHandler? PointerExitedItem + public event EventHandler? PointerExitedItem { add { AddHandler(PointerExitedItemEvent, value); } remove { RemoveHandler(PointerExitedItemEvent, value); } @@ -437,20 +438,14 @@ namespace Avalonia.Controls protected override void OnPointerEntered(PointerEventArgs e) { base.OnPointerEntered(e); - - var point = e.GetCurrentPoint(null); - RaiseEvent(new PointerEventArgs(PointerEnteredItemEvent, this, e.Pointer, (Visual?)VisualRoot, point.Position, - e.Timestamp, point.Properties, e.KeyModifiers)); + RaiseEvent(new RoutedEventArgs(PointerEnteredItemEvent)); } /// protected override void OnPointerExited(PointerEventArgs e) { base.OnPointerExited(e); - - var point = e.GetCurrentPoint(null); - RaiseEvent(new PointerEventArgs(PointerExitedItemEvent, this, e.Pointer, (Visual?)VisualRoot, point.Position, - e.Timestamp, point.Properties, e.KeyModifiers)); + RaiseEvent(new RoutedEventArgs(PointerExitedItemEvent)); } /// @@ -686,6 +681,12 @@ namespace Avalonia.Controls /// The event args. private void PopupOpened(object? sender, EventArgs e) { + // If we're using overlay popups, there's a chance we need to do a layout pass before + // the child items are added to the visual tree. If we don't do this here, then + // selection breaks. + if (Presenter?.IsAttachedToVisualTree == false) + UpdateLayout(); + var selected = SelectedIndex; if (selected != -1) @@ -705,6 +706,11 @@ namespace Avalonia.Controls SelectedItem = null; } + private void UpdateLayout() + { + (VisualRoot as ILayoutRoot)?.LayoutManager.ExecuteLayoutPass(); + } + void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e); void IClickableControl.RaiseClick() 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/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 4dd868253e..d2b23a7ac3 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -301,7 +301,7 @@ namespace Avalonia.Controls.Platform e.Handled = true; } - protected internal virtual void PointerEntered(object? sender, PointerEventArgs e) + protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e) { var item = GetMenuItem(e.Source as Control); @@ -368,7 +368,7 @@ namespace Avalonia.Controls.Platform } } - protected internal virtual void PointerExited(object? sender, PointerEventArgs e) + protected internal virtual void PointerExited(object? sender, RoutedEventArgs e) { var item = GetMenuItem(e.Source as Control); 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 611d57a980..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) diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index e16633483b..7ed055f2e5 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -51,7 +51,7 @@ namespace Avalonia.Controls.Primitives } /// - protected internal override Interactive? InteractiveParent => (Interactive?)VisualParent; + protected internal override Interactive? InteractiveParent => Parent as Interactive; /// public void Dispose() => Hide(); 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/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/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/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.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 3b1c6cc7b1..51af5862cf 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,16 +5,40 @@ enable + + + + + + + - + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index e7c07dcbf9..b853626d45 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -7,19 +7,20 @@ namespace Avalonia.FreeDesktop class DBusCallQueue { private readonly Func _errorHandler; + private readonly Queue _q = new(); - record Item(Func Callback) + private bool _processing; + + private record Item(Func Callback) { public Action? OnFinish; } - private Queue _q = new Queue(); - private bool _processing; public DBusCallQueue(Func errorHandler) { _errorHandler = errorHandler; } - + public void Enqueue(Func cb) { _q.Enqueue(new Item(cb)); @@ -42,7 +43,7 @@ namespace Avalonia.FreeDesktop Process(); return tcs.Task; } - + public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -62,7 +63,7 @@ namespace Avalonia.FreeDesktop return tcs.Task; } - async void Process() + private async void Process() { if(_processing) return; diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs deleted file mode 100644 index 24db614a02..0000000000 --- a/src/Avalonia.FreeDesktop/DBusFileChooser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.FileChooser")] - internal interface IFileChooser : IDBusObject - { - Task OpenFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - internal class FileChooserProperties - { - public uint Version { get; set; } - } - - internal static class FileChooserExtensions - { - public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version"); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index fac77521dc..da74f15a3e 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,51 +1,12 @@ using System; using System.Threading; using Avalonia.Logging; -using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { internal static class DBusHelper { - /// - /// This class uses synchronous execution at DBus connection establishment stage - /// then switches to using AvaloniaSynchronizationContext - /// - private class DBusSyncContext : SynchronizationContext - { - private readonly object _lock = new(); - private SynchronizationContext? _ctx; - - public override void Post(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Post(d, state); - else - d(state); - } - } - - public override void Send(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Send(d, state); - else - d(state); - } - } - - public void Initialized() - { - lock (_lock) - _ctx = new AvaloniaSynchronizationContext(); - } - } - public static Connection? Connection { get; private set; } public static Connection? TryInitialize(string? dbusAddress = null) @@ -56,19 +17,14 @@ namespace Avalonia.FreeDesktop var oldContext = SynchronizationContext.Current; try { - - var dbusContext = new DBusSyncContext(); - SynchronizationContext.SetSynchronizationContext(dbusContext); - var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) { - AutoConnect = false, - SynchronizationContext = dbusContext + AutoConnect = false }); + // Connect synchronously - conn.ConnectAsync().Wait(); + conn.ConnectAsync().GetAwaiter().GetResult(); - // Initialize a brand new sync-context - dbusContext.Initialized(); Connection = conn; } catch (Exception e) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 7d97c7cd36..d3c14f285d 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme { @@ -46,20 +47,25 @@ namespace Avalonia.FreeDesktop.DBusIme public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { - _queue = new DBusCallQueue(QueueOnError); + _queue = new DBusCallQueue(QueueOnErrorAsync); Connection = connection; _knownNames = knownNames; - Watch(); + _ = WatchAsync(); } public ITextInputMethodClient Client => _client; - public bool IsActive => _client != null; + public bool IsActive => _client is not null; - async void Watch() + private async Task WatchAsync() { foreach (var name in _knownNames) - _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + { + var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } } protected abstract Task Connect(string name); @@ -67,9 +73,12 @@ namespace Avalonia.FreeDesktop.DBusIme protected string GetAppName() => Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - private async void OnNameChange(ServiceOwnerChangedEventArgs args) + private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { - if (args.NewOwner != null && _currentName == null) + if (e is not null) + return; + + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); if (!_connecting) @@ -89,10 +98,10 @@ namespace Avalonia.FreeDesktop.DBusIme return; } } - catch (Exception e) + catch (Exception ex) { Logger.TryGet(LogEventLevel.Error, "IME") - ?.Log(this, "Unable to create IME input context:\n" + e); + ?.Log(this, "Unable to create IME input context:\n" + ex); } } } @@ -105,7 +114,7 @@ namespace Avalonia.FreeDesktop.DBusIme } // IME has crashed - if (args.NewOwner == null && args.ServiceName == _currentName) + if (args.NewOwner is null && args.ServiceName == _currentName) { _currentName = null; foreach (var s in _disposables) @@ -116,11 +125,11 @@ namespace Avalonia.FreeDesktop.DBusIme Reset(); // Watch again - Watch(); + _ = WatchAsync(); } } - protected virtual Task Disconnect() + protected virtual Task DisconnectAsync() { return Task.CompletedTask; } @@ -136,13 +145,13 @@ namespace Avalonia.FreeDesktop.DBusIme _imeActive = null; } - async Task QueueOnError(Exception e) + private async Task QueueOnErrorAsync(Exception e) { Logger.TryGet(LogEventLevel.Error, "IME") ?.Log(this, "Error:\n" + e); try { - await Disconnect(); + await DisconnectAsync(); } catch (Exception ex) { @@ -157,23 +166,16 @@ namespace Avalonia.FreeDesktop.DBusIme protected void AddDisposable(IDisposable? d) { - if(d is { }) + if (d is { }) _disposables.Add(d); } - + public void Dispose() { foreach(var d in _disposables) d.Dispose(); _disposables.Clear(); - try - { - Disconnect().ContinueWith(_ => { }); - } - catch - { - // fire and forget - } + _ = DisconnectAsync(); _currentName = null; } @@ -182,13 +184,13 @@ namespace Avalonia.FreeDesktop.DBusIme protected abstract Task ResetContextCore(); protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); - void UpdateActive() + private void UpdateActive() { _queue.Enqueue(async () => { if(!IsConnected) return; - + var active = _windowActive && IsActive; if (active != _imeActive) { @@ -204,7 +206,7 @@ namespace Avalonia.FreeDesktop.DBusIme _windowActive = active; UpdateActive(); } - + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _client = client; @@ -227,7 +229,7 @@ namespace Avalonia.FreeDesktop.DBusIme // Error, disconnect catch (Exception e) { - await QueueOnError(e); + await QueueOnErrorAsync(e); return false; } } @@ -240,7 +242,7 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireCommit(string s) => _onCommit?.Invoke(s); - + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { @@ -249,8 +251,8 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); - - void UpdateCursorRect() + + private void UpdateCursorRect() { _queue.Enqueue(async () => { @@ -265,7 +267,7 @@ namespace Avalonia.FreeDesktop.DBusIme } }); } - + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) { _windowPosition = position; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs deleted file mode 100644 index 06afacaa29..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - [DBusInterface("org.fcitx.Fcitx.InputMethod")] - interface IFcitxInputMethod : IDBusObject - { - Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( - string Appname, int Pid); - } - - - [DBusInterface("org.fcitx.Fcitx.InputContext")] - interface IFcitxInputContext : IDBusObject - { - Task EnableICAsync(); - Task CloseICAsync(); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task MouseEventAsync(int X); - Task SetCursorLocationAsync(int X, int Y); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapacityAsync(uint Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action? onError = null); - Task WatchCloseIMAsync(Action handler, Action? onError = null); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputContext1")] - interface IFcitxInputContext1 : IDBusObject - { - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapabilityAsync(ulong Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputMethod1")] - interface IFcitxInputMethod1 : IDBusObject - { - Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs index 6510a5877a..fafc66b6b9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -2,45 +2,45 @@ using System; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { - enum FcitxKeyEventType + internal enum FcitxKeyEventType { FCITX_PRESS_KEY, FCITX_RELEASE_KEY - }; - + } + [Flags] - enum FcitxCapabilityFlags + internal enum FcitxCapabilityFlags { CAPACITY_NONE = 0, - CAPACITY_CLIENT_SIDE_UI = (1 << 0), - CAPACITY_PREEDIT = (1 << 1), - CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), - CAPACITY_PASSWORD = (1 << 3), - CAPACITY_FORMATTED_PREEDIT = (1 << 4), - CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), - CAPACITY_SURROUNDING_TEXT = (1 << 6), - CAPACITY_EMAIL = (1 << 7), - CAPACITY_DIGIT = (1 << 8), - CAPACITY_UPPERCASE = (1 << 9), - CAPACITY_LOWERCASE = (1 << 10), - CAPACITY_NOAUTOUPPERCASE = (1 << 11), - CAPACITY_URL = (1 << 12), - CAPACITY_DIALABLE = (1 << 13), - CAPACITY_NUMBER = (1 << 14), - CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), - CAPACITY_SPELLCHECK = (1 << 16), - CAPACITY_NO_SPELLCHECK = (1 << 17), - CAPACITY_WORD_COMPLETION = (1 << 18), - CAPACITY_UPPERCASE_WORDS = (1 << 19), - CAPACITY_UPPERCASE_SENTENCES = (1 << 20), - CAPACITY_ALPHA = (1 << 21), - CAPACITY_NAME = (1 << 22), - CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), - CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), - }; + CAPACITY_CLIENT_SIDE_UI = 1 << 0, + CAPACITY_PREEDIT = 1 << 1, + CAPACITY_CLIENT_SIDE_CONTROL_STATE = 1 << 2, + CAPACITY_PASSWORD = 1 << 3, + CAPACITY_FORMATTED_PREEDIT = 1 << 4, + CAPACITY_CLIENT_UNFOCUS_COMMIT = 1 << 5, + CAPACITY_SURROUNDING_TEXT = 1 << 6, + CAPACITY_EMAIL = 1 << 7, + CAPACITY_DIGIT = 1 << 8, + CAPACITY_UPPERCASE = 1 << 9, + CAPACITY_LOWERCASE = 1 << 10, + CAPACITY_NOAUTOUPPERCASE = 1 << 11, + CAPACITY_URL = 1 << 12, + CAPACITY_DIALABLE = 1 << 13, + CAPACITY_NUMBER = 1 << 14, + CAPACITY_NO_ON_SCREEN_KEYBOARD = 1 << 15, + CAPACITY_SPELLCHECK = 1 << 16, + CAPACITY_NO_SPELLCHECK = 1 << 17, + CAPACITY_WORD_COMPLETION = 1 << 18, + CAPACITY_UPPERCASE_WORDS = 1 << 19, + CAPACITY_UPPERCASE_SENTENCES = 1 << 20, + CAPACITY_ALPHA = 1 << 21, + CAPACITY_NAME = 1 << 22, + CAPACITY_GET_IM_INFO_ON_FOCUS = 1 << 23, + CAPACITY_RELATIVE_CURSOR_RECT = 1 << 24 + } [Flags] - enum FcitxKeyState + internal enum FcitxKeyState { FcitxKeyState_None = 0, FcitxKeyState_Shift = 1 << 0, @@ -63,5 +63,5 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx FcitxKeyState_Hyper = 1 << 27, FcitxKeyState_Meta = 1 << 28, FcitxKeyState_UsedMask = 0x5c001fff - }; + } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 6c503edb41..00d05e59a3 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -1,19 +1,20 @@ using System; using System.Threading.Tasks; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1? _modern; - private readonly IFcitxInputContext? _old; + private readonly OrgFcitxFcitxInputContext1? _modern; + private readonly OrgFcitxFcitxInputContext? _old; - public FcitxICWrapper(IFcitxInputContext old) + public FcitxICWrapper(OrgFcitxFcitxInputContext old) { _old = old; } - public FcitxICWrapper(IFcitxInputContext1 modern) + public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern) { _modern = modern; } @@ -21,32 +22,30 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { - if(_old!=null) + if (_old is not null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) - ?? _modern?.WatchCommitStringAsync(handler) - ?? Task.FromResult(default(IDisposable?)); + public ValueTask WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? new ValueTask(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) - { - return _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))) - ?? Task.FromResult(default(IDisposable?)); - } + public ValueTask WatchForwardKeyAsync(Action handler) => + _old?.WatchForwardKeyAsync(handler) + ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0))) + ?? new ValueTask(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 0f499c6066..1cf3507cc2 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,11 +1,11 @@ using System; using System.Diagnostics; -using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -14,32 +14,24 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; - public FcitxX11TextInputMethod(Connection connection) : base(connection, - "org.fcitx.Fcitx", - "org.freedesktop.portal.Fcitx" - ) - { - - } + public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } protected override async Task Connect(string name) { if (name == "org.fcitx.Fcitx") { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = Connection.CreateProxy(name, - "/inputcontext_" + resp.icid); - + var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = Connection.CreateProxy(name, resp.path); + var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } @@ -48,7 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -63,14 +55,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Math.Max(1, cursorRect.Height)) ?? Task.CompletedTask; - protected override Task SetActiveCore(bool active)=> (active + protected override Task SetActiveCore(bool active)=> (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) ?? Task.CompletedTask; - - protected override Task ResetContextCore() => _context?.ResetAsync() - ?? Task.CompletedTask; + protected override Task ResetContextCore() => _context?.ResetAsync() ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -87,17 +77,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - if (_context is { }) - { + if (_context is not null) return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, (uint)args.Timestamp).ConfigureAwait(false); - } - else - { - return false; - } + + return false; } - + public override void SetOptions(TextInputOptions options) => Enqueue(async () => { @@ -127,7 +113,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } }); - private void OnForward((uint keyval, uint state, int type) ev) + private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) { var state = (FcitxKeyState)ev.state; KeyModifiers mods = default; @@ -149,6 +135,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) + { + if (e is not null) + return; + + FireCommit(s); + } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs deleted file mode 100644 index 4ef034adb9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - [DBusInterface("org.freedesktop.IBus.InputContext")] - interface IIBusInputContext : IDBusObject - { - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); - Task SetCursorLocationAsync(int X, int Y, int W, int H); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCapabilitiesAsync(uint Caps); - Task PropertyActivateAsync(string Name, int State); - Task SetEngineAsync(string Name); - Task GetEngineAsync(); - Task DestroyAsync(); - Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action? onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); - Task WatchShowLookupTableAsync(Action handler, Action? onError = null); - Task WatchHideLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); - } - - - [DBusInterface("org.freedesktop.IBus.Portal")] - interface IIBusPortal : IDBusObject - { - Task CreateInputContextAsync(string Name); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index 3070f51a8e..1b430f9c90 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Button3Mask = 1 << 10, Button4Mask = 1 << 11, Button5Mask = 1 << 12, - + HandledMask = 1 << 24, ForwardMask = 1 << 25, IgnoredMask = ForwardMask, @@ -40,6 +40,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus CapLookupTable = 1 << 2, CapFocus = 1 << 3, CapProperty = 1 << 4, - CapSurroundingText = 1 << 5, + CapSurroundingText = 1 << 5 } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 2324ca44a7..59e9ecd1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -1,36 +1,38 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; + namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext? _context; + private OrgFreedesktopIBusService? _service; + private OrgFreedesktopIBusInputContext? _context; - public IBusX11TextInputMethod(Connection connection) : base(connection, - "org.freedesktop.portal.IBus") - { - } + public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var path = - await Connection.CreateProxy(name, "/org/freedesktop/IBus") - .CreateInputContextAsync(GetAppName()); - - _context = Connection.CreateProxy(name, path); + var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus"); + var path = await portal.CreateInputContextAsync(GetAppName()); + _service = new OrgFreedesktopIBusService(Connection, name, path); + _context = new OrgFreedesktopIBusInputContext(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); return true; } - private void OnForwardKey((uint keyval, uint keycode, uint state) k) + private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { + if (e is not null) + return; + var state = (IBusModifierMask)k.state; KeyModifiers mods = default; if (state.HasAllFlags(IBusModifierMask.ControlMask)) @@ -49,28 +51,25 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - - private void OnCommitText(object wtf) + private void OnCommitText(Exception? e, DBusVariantItem variantItem) { - // Hello darkness, my old friend - if (wtf.GetType().GetField("Item3") is { } prop) - { - var text = prop.GetValue(wtf) as string; - if (!string.IsNullOrEmpty(text)) - FireCommit(text!); - } + if (e is not null) + return; + + if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem) + FireCommit(stringItem.Value); } - protected override Task Disconnect() => _context?.DestroyAsync() - ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; protected override void OnDisconnected() { + _service = null; _context = null; base.OnDisconnected(); } - protected override Task SetCursorRectCore(PixelRect rect) + protected override Task SetCursorRectCore(PixelRect rect) => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) ?? Task.CompletedTask; @@ -96,20 +95,12 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - if(_context is { }) - { - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); - } - else - { - return Task.FromResult(false); - } - + return _context is not null ? _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state) : Task.FromResult(false); } public override void SetOptions(TextInputOptions options) { - // No-op, because ibus + // No-op, because ibus } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index d8874b6fae..8042d3bff2 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -2,44 +2,43 @@ using System; using System.Collections.Generic; using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.FreeDesktop.DBusIme.IBus; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { internal class X11DBusImeHelper { - private static readonly Dictionary> KnownMethods = - new Dictionary> + private static readonly Dictionary> KnownMethods = new() { - ["fcitx"] = conn => + ["fcitx"] = static conn => new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), - ["ibus"] = conn => + ["ibus"] = static conn => new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - - static Func? DetectInputMethod() + + private static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { var value = Environment.GetEnvironmentVariable(name); - + if (value == "none") return null; - - if (value != null && KnownMethods.TryGetValue(value, out var factory)) + + if (value is not null && KnownMethods.TryGetValue(value, out var factory)) return factory; } return null; } - + public static bool DetectAndRegister() { var factory = DetectInputMethod(); - if (factory != null) + if (factory is not null) { var conn = DBusHelper.TryInitialize(); - if (conn != null) + if (conn is not null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); return true; diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index 7e22988270..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusMenu -{ - - [DBusInterface("org.freedesktop.DBus.Properties")] - interface IFreeDesktopDBusProperties : IDBusObject - { - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [DBusInterface("com.canonical.dbusmenu")] - interface IDBusMenu : IFreeDesktopDBusProperties - { - Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); - Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); - Task GetPropertyAsync(int Id, string Name); - Task EventAsync(int Id, string EventId, object Data, uint Timestamp); - Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); - Task AboutToShowAsync(int Id); - Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); - } - - [Dictionary] - class DBusMenuProperties - { - public uint Version { get; set; } = default; - public string? TextDirection { get; set; } = default; - public string? Status { get; set; } = default; - public string[]? IconThemePath { get; set; } = default; - } - - - [DBusInterface("com.canonical.AppMenu.Registrar")] - interface IRegistrar : IDBusObject - { - Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); - Task UnregisterWindowAsync(uint WindowId); - Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); - Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index cfbafc53e5..d08828f796 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,97 +2,116 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using Avalonia.Reactive; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus; -#pragma warning disable 1998 +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusMenuExporter { - public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) - { - if (DBusHelper.Connection == null) - return null; + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => + DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid); - return new DBusMenuExporterImpl(DBusHelper.Connection, xid); - } - - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConnection) - { - return new DBusMenuExporterImpl(currentConnection, path); - } + public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => + new DBusMenuExporterImpl(currentConnection, path); - public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString("N"); + public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { - private readonly Connection _dbus; + private readonly Dictionary _idsToItems = new(); + private readonly Dictionary _itemsToIds = new(); + private readonly HashSet _menus = new(); private readonly uint _xid; - private IRegistrar? _registrar; + private readonly bool _appMenu = true; + private ComCanonicalAppMenuRegistrar? _registrar; + private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; - private NativeMenu? _menu; - private readonly Dictionary _idsToItems = new Dictionary(); - private readonly Dictionary _itemsToIds = new Dictionary(); - private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool _appMenu = true; - - public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + + public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _dbus = dbus; + Connection = connection; _xid = (uint)xid.ToInt32(); - ObjectPath = GenerateDBusMenuObjPath; + Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + public DBusMenuExporterImpl(Connection connection, string path) { - _dbus = dbus; + Connection = connection; _appMenu = false; - ObjectPath = path; + Path = path; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } - - async void Init() + + protected override Connection Connection { get; } + + public override string Path { get; } + + protected override (uint revision, (int, Dictionary, DBusVariantItem[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + { + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + return (_revision, layout); + } + + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); + + protected override DBusVariantItem OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0)); + + protected override void OnEvent(int id, string eventId, DBusVariantItem data, uint timestamp) => + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); + + protected override int[] OnEventGroup((int, string, DBusVariantItem, uint)[] events) + { + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); + return Array.Empty(); + } + + protected override bool OnAboutToShow(int id) => false; + + protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => + (Array.Empty(), Array.Empty()); + + private async Task InitializeAsync() { + Connection.AddMethodHandler(this); + if (!_appMenu) + return; + + _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); try { - if (_appMenu) - { - await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection?.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed && _registrar is { }) - await _registrar.RegisterWindowAsync(_xid, ObjectPath); - } - else - { - await _dbus.RegisterObjectAsync(this); - } + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, Path); } - catch (Exception e) + catch { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) - ?.Log(this, e.Message); - // It's not really important if this code succeeds, // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that // menu will be actually exported + _registrar = null; } } @@ -101,29 +120,28 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - _dbus.UnregisterObject(this); // Fire and forget - _registrar?.UnregisterWindowAsync(_xid); + _ = _registrar?.UnregisterWindowAsync(_xid); } public bool IsNativeMenuExported { get; private set; } + public event EventHandler? OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu? menu) { - if (menu == null) - menu = new NativeMenu(); + menu ??= new NativeMenu(); - if (_menu != null) + if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; _menu = menu; ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; - + DoLayoutReset(); } - + /* This is basic initial implementation, so we don't actually track anything and just reset the whole layout on *ANY* change @@ -131,10 +149,10 @@ namespace Avalonia.FreeDesktop This is not how it should work and will prevent us from implementing various features, but that's the fastest way to get things working, so... */ - void DoLayoutReset() + private void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; foreach(var menu in _menus) ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; @@ -142,10 +160,10 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - LayoutUpdated?.Invoke((_revision, 0)); + EmitLayoutUpdated(_revision, 0); } - void QueueReset() + private void QueueReset() { if(_resetQueued) return; @@ -163,10 +181,10 @@ namespace Avalonia.FreeDesktop private void EnsureSubscribed(NativeMenu? menu) { - if(menu!=null && _menus.Add(menu)) + if (menu is not null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; } - + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -190,258 +208,137 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public ObjectPath ObjectPath { get; } - - - async Task IFreeDesktopDBusProperties.GetAsync(string prop) - { - if (prop == "Version") - return 2; - if (prop == "Status") - return "normal"; - return 0; - } - - async Task IFreeDesktopDBusProperties.GetAllAsync() - { - return new DBusMenuProperties - { - Version = 2, - Status = "normal", - }; - } - - private static string[] AllProperties = new[] - { + private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - - object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + + private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return "separator"; + return new DBusVariantItem("s", new DBusStringItem("separator")); } else if (it is NativeMenuItem item) { if (name == "type") - { return null; - } if (name == "label") - return item?.Header ?? ""; + return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "")); if (name == "enabled") { - if (item == null) - return null; - if (item.Menu != null && item.Menu.Items.Count == 0) - return false; - if (item.IsEnabled == false) - return false; + if (item.Menu is not null && item.Menu.Items.Count == 0) + return new DBusVariantItem("b", new DBusBoolItem(false)); + if (!item.IsEnabled) + return new DBusVariantItem("b", new DBusBoolItem(false)); return null; } if (name == "shortcut") { - if (item?.Gesture == null) + if (item.Gesture is null) return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new List(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - lst.Add("Control"); + lst.Add(new DBusStringItem("Control")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - lst.Add("Alt"); + lst.Add(new DBusStringItem("Alt")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - lst.Add("Shift"); + lst.Add(new DBusStringItem("Shift")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - lst.Add("Super"); - lst.Add(item.Gesture.Key.ToString()); - return new[] { lst.ToArray() }; + lst.Add(new DBusStringItem("Super")); + lst.Add(new DBusStringItem(item.Gesture.Key.ToString())); + return new DBusVariantItem("aas", new DBusArrayItem(DBusType.Array, new[] { new DBusArrayItem(DBusType.String, lst) })); } if (name == "toggle-type") { if (item.ToggleType == NativeMenuItemToggleType.CheckBox) - return "checkmark"; + return new DBusVariantItem("s", new DBusStringItem("checkmark")); if (item.ToggleType == NativeMenuItemToggleType.Radio) - return "radio"; + return new DBusVariantItem("s", new DBusStringItem("radio")); } - if (name == "toggle-state") - { - if (item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; - } - + if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) + return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0)); + if (name == "icon-data") { - if (item.Icon != null) + if (item.Icon is not null) { var loader = AvaloniaLocator.Current.GetService(); - if (loader != null) + if (loader is not null) { var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); using var ms = new MemoryStream(); icon.Save(ms); - return ms.ToArray(); + return new DBusVariantItem("ay", + new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x)))); } } } - + if (name == "children-display") - return menu != null ? "submenu" : null; + return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null; } return null; } - private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { - if (names?.Length > 0 != true) - names = AllProperties; - _reusablePropertyList.Clear(); + if (names.Length == 0) + names = s_allProperties; + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); - if (v != null) - _reusablePropertyList.Add(new KeyValuePair(n, v)); + if (v is not null) + properties.Add(n, v); } - return _reusablePropertyList.ToArray(); - } - - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( - int ParentId, int RecursionDepth, string[] PropertyNames) - { - var menu = GetMenu(ParentId); - var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => - { - OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); - }); - } - return Task.FromResult(rv); + return properties; } - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private (int, Dictionary, DBusVariantItem[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { - var id = item == null ? 0 : GetId(item); + var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = (depth == 0 || menu == null) ? Array.Empty() : new object[menu.Items.Count]; - if(menu != null) + var children = depth == 0 || menu is null ? Array.Empty() : new DBusVariantItem[menu.Items.Count]; + if (menu is not null) + { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - - children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[] + { + new DBusInt32Item(layout.Item1), + new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))), + new DBusArrayItem(DBusType.Variant, layout.Item3) + })); } - - return (id, props, children); - } - - public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) - { - var arr = new (int, KeyValuePair[])[Ids.Length]; - for (var c = 0; c < Ids.Length; c++) - { - var id = Ids[c]; - var item = GetMenu(id); - var props = GetProperties(item, PropertyNames); - arr[c] = (id, props); } - return Task.FromResult(arr); - } - - public async Task GetPropertyAsync(int Id, string Name) - { - return GetProperty(GetMenu(Id), Name) ?? 0; + return (id, props, children); } - - public void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId) { if (eventId == "clicked") { var item = GetMenu(id).item; - - if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) - { - if (menuItem?.IsEnabled == true) - bridge?.RaiseClicked(); - } + if (item is NativeMenuItem { IsEnabled: true } and INativeMenuItemExporterEventsImplBridge bridge) + bridge.RaiseClicked(); } } - - public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) - { - HandleEvent(Id, EventId, Data, Timestamp); - return Task.CompletedTask; - } - - public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) - { - foreach (var e in Events) - HandleEvent(e.id, e.eventId, e.data, e.timestamp); - return Task.FromResult(Array.Empty()); - } - - public async Task AboutToShowAsync(int Id) - { - return false; - } - - public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) - { - return (Array.Empty(), Array.Empty()); - } - - #region Events - - private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> - ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)>? LayoutUpdated; - private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } - private event Action PropertiesChanged { add { } remove { } } - - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) - { - ItemsPropertiesUpdated += handler; - return Disposable.Create(() => ItemsPropertiesUpdated -= handler); - } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) - { - LayoutUpdated += handler; - return Disposable.Create(() => LayoutUpdated -= handler); - } - - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) - { - ItemActivationRequested+= handler; - return Disposable.Create(() => ItemActivationRequested -= handler); - } - - async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) - { - PropertiesChanged += handler; - return Disposable.Create(() => PropertiesChanged -= handler); - } - - #endregion } } } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 039fc7c088..a25bb68458 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -2,44 +2,35 @@ using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Tmds.DBus.SourceGenerator; -namespace Avalonia.FreeDesktop; - -internal class DBusPlatformSettings : DefaultPlatformSettings +namespace Avalonia.FreeDesktop { - private readonly IDBusSettings? _settings; - private PlatformColorValues? _lastColorValues; - - public DBusPlatformSettings() + internal class DBusPlatformSettings : DefaultPlatformSettings { - _settings = DBusHelper.TryInitialize()? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; - if (_settings is not null) + public DBusPlatformSettings() { - _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + if (DBusHelper.Connection is null) + return; - _ = TryGetInitialValue(); + _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + _ = TryGetInitialValueAsync(); } - } - - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } - private async Task TryGetInitialValue() - { - var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - if (colorSchemeTask.Status == TaskStatus.RanToCompletion) + public override PlatformColorValues GetColorValues() { - _lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); + return _lastColorValues ?? base.GetColorValues(); } - else + + private async Task TryGetInitialValueAsync() { try { - var value = await colorSchemeTask; + var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); _lastColorValues = GetColorValuesFromSetting(value); OnColorValuesChanged(_lastColorValues); } @@ -49,29 +40,31 @@ internal class DBusPlatformSettings : DefaultPlatformSettings Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); } } - } - - private void SettingsChangedHandler((string @namespace, string key, object value) tuple) - { - if (tuple.@namespace == "org.freedesktop.appearance" - && tuple.key == "color-scheme") + + private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(tuple.value); - OnColorValuesChanged(_lastColorValues); + if (exception is not null) + return; + + if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _lastColorValues = GetColorValuesFromSetting(value); + OnColorValuesChanged(_lastColorValues); + } } - } - - private static PlatformColorValues GetColorValuesFromSetting(object value) - { - var isDark = value?.ToString() == "1"; - return new PlatformColorValues + + private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } } } diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs deleted file mode 100644 index d84905324f..0000000000 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.Request")] - internal interface IRequest : IDBusObject - { - Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusSettings.cs b/src/Avalonia.FreeDesktop/DBusSettings.cs deleted file mode 100644 index 05911981c7..0000000000 --- a/src/Avalonia.FreeDesktop/DBusSettings.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus; - -namespace Avalonia.FreeDesktop; - -[DBusInterface("org.freedesktop.portal.Settings")] -internal interface IDBusSettings : IDBusObject -{ - Task<(string @namespace, IDictionary)> ReadAllAsync(string[] namespaces); - - Task ReadAsync(string @namespace, string key); - - Task WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action? onError = null); -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 905ce1f272..20583dd6ac 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -2,46 +2,42 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; - -using Tmds.DBus; +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); - - internal static async Task TryCreate(IPlatformHandle handle) + internal static async Task TryCreateAsync(IPlatformHandle handle) { - if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) + if (DBusHelper.Connection is null) + return null; + + var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + try { - try - { - await fileChooser.GetVersionAsync(); - return new DBusSystemDialog(fileChooser, handle); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); - return null; - } + await dbusFileChooser.GetVersionAsync(); + } + catch + { + return null; } - return null; + return new DBusSystemDialog(DBusHelper.Connection, handle, dbusFileChooser); } - private readonly IFileChooser _fileChooser; + private readonly Connection _connection; + private readonly OrgFreedesktopPortalFileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle) + private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooser fileChooser) { + _connection = connection; _fileChooser = fileChooser; _handle = handle; } @@ -56,115 +52,124 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); - if (filters.Any()) - { + if (filters is not null) chooserOptions.Add("filters", filters); - } - chooserOptions.Add("multiple", options.AllowMultiple); + chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple))); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); - return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); + var uris = await tsc.Task ?? Array.Empty(); + return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); } public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); - if (filters.Any()) - { + if (filters is not null) chooserOptions.Add("filters", filters); - } if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", currentName); - if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath)); - objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); + if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) + chooserOptions.Add("current_folder", new DBusVariantItem("s", new DBusStringItem(folderPath))); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); + var uris = await tsc.Task; var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null; if (path is null) - { return null; - } - else - { - // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. - path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); - return new BclStorageFile(new FileInfo(path)); - } + // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. + path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); + return new BclStorageFile(new FileInfo(path)); } public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; - var chooserOptions = new Dictionary + var chooserOptions = new Dictionary { - { "directory", true }, - { "multiple", options.AllowMultiple } + { "directory", new DBusVariantItem("b", new DBusBoolItem(true)) }, + { "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) } }; + var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); + }); + var uris = await tsc.Task ?? Array.Empty(); return uris - .Select(path => new Uri(path).LocalPath) + .Select(static path => new Uri(path).LocalPath) // WSL2 freedesktop allows to select files as well in directory picker, filter it out. .Where(Directory.Exists) - .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); + .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } - - private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) + + private static DBusVariantItem? ParseFilters(IReadOnlyList? fileTypes) { - // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; + // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] if (fileTypes is null) - { - return Array.Empty<(string name, (uint style, string extension)[])>(); - } + return null; + + var filters = new DBusArrayItem(DBusType.Struct, new List()); - var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint globStyle = 0u; - const uint mimeStyle = 1u; - - var extensions = Enumerable.Empty<(uint, string)>(); - - if (fileType.Patterns is { } patterns) - { - extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - } - else if (fileType.MimeTypes is { } mimeTypes) - { - extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); - } - - if (extensions.Any()) - { - filters.Add((fileType.Name, extensions.ToArray())); - } + var extensions = new List(); + if (fileType.Patterns?.Count > 0) + extensions.AddRange( + fileType.Patterns.Select(static pattern => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) }))); + else if (fileType.MimeTypes?.Count > 0) + extensions.AddRange( + fileType.MimeTypes.Select(static mimeType => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) }))); + else + continue; + + filters.Add(new DBusStructItem( + new DBusItem[] + { + new DBusStringItem(fileType.Name), + new DBusArrayItem(DBusType.Struct, extensions) + })); } - return filters.ToArray(); + return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index b44762161b..afbee77067 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,30 +1,26 @@ -#nullable enable - -using System; +using System; using System.Diagnostics; -using Avalonia.Reactive; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusTrayIconImpl : ITrayIconImpl { private static int s_trayIconInstanceId; + public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private IDisposable? _serviceWatchDisposable; + private readonly OrgFreedesktopDBus? _dBus; + private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private DbusPixmap _icon; + private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; + private (int, int, byte[]) _icon; private string? _sysTrayServiceName; private string? _tooltipText; @@ -51,6 +47,7 @@ namespace Avalonia.FreeDesktop IsActive = true; + _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -60,23 +57,10 @@ namespace Avalonia.FreeDesktop private void InitializeSNWService() { - if (_connection is null || _isDisposed) return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); - + if (_connection is null || _isDisposed) return; - } + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); _serviceConnected = true; } @@ -84,23 +68,24 @@ namespace Avalonia.FreeDesktop { try { - _serviceWatchDisposable = - await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2)); + var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); + OnNameChange(nameOwner); } - catch (Exception e) + catch { + _serviceWatchDisposable = null; Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); + ?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable."); } } - private void OnNameChange(ServiceOwnerChangedEventArgs obj) + private void OnNameChange(string? newOwner) { if (_isDisposed) return; - if (!_serviceConnected & obj.NewOwner != null) + if (!_serviceConnected & newOwner is not null) { _serviceConnected = true; InitializeSNWService(); @@ -108,55 +93,45 @@ namespace Avalonia.FreeDesktop DestroyTrayIcon(); if (_isVisible) - { CreateTrayIcon(); - } } - else if (_serviceConnected & obj.NewOwner is null) + else if (_serviceConnected & newOwner is null) { DestroyTrayIcon(); _serviceConnected = false; } } - private void CreateTrayIcon() + private async void CreateTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null) return; +#if NET5_0_OR_GREATER + var pid = Environment.ProcessId; +#else var pid = Process.GetCurrentProcess().Id; +#endif var tid = s_trayIconInstanceId++; _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath); - try - { - _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - _connection.RegisterServiceAsync(_sysTrayServiceName); - _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, $"Error creating a DBus tray icon: {e}."); - - _serviceConnected = false; - } + _connection.AddMethodHandler(_statusNotifierItemDbusObj); + await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; } private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - _connection.UnregisterServiceAsync(_sysTrayServiceName); + _dBus!.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -164,7 +139,6 @@ namespace Avalonia.FreeDesktop IsActive = false; _isDisposed = true; DestroyTrayIcon(); - _connection?.Dispose(); _serviceWatchDisposable?.Dispose(); } @@ -175,13 +149,14 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon(EmptyPixmap); return; } var x11iconData = IconConverterDelegate(icon); - if (x11iconData.Length == 0) return; + if (x11iconData.Length == 0) + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -199,7 +174,7 @@ namespace Avalonia.FreeDesktop pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } - _icon = new DbusPixmap(w, h, pixByteArray); + _icon = (w, h, pixByteArray); _statusNotifierItemDbusObj?.SetIcon(_icon); } @@ -237,113 +212,50 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem + internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { - private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); - - _backingProperties = new StatusNotifierItemProperties - { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") - }; - + Connection = connection; + BackingProperties.Menu = dbusMenuPath; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); + BackingProperties.IconName = string.Empty; + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.IconThemePath = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; InvalidateAll(); } - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; + protected override Connection Connection { get; } - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } - - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + public override string Path => "/StatusNotifierItem"; - public void InvalidateAll() - { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } + public event Action? ActivationDelegate; - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } + protected override void OnContextMenu(int x, int y) { } - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } + protected override void OnActivate(int x, int y) => ActivationDelegate?.Invoke(); - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); - } + protected override void OnSecondaryActivate(int x, int y) { } - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } + protected override void OnScroll(int delta, string orientation) { } - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) + public void InvalidateAll() { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + EmitNewTitle(); + EmitNewIcon(); + EmitNewAttentionIcon(); + EmitNewOverlayIcon(); + EmitNewToolTip(); + EmitNewStatus(BackingProperties.Status); } - public Task GetAsync(string prop) + public void SetIcon((int, int, byte[]) dbusPixmap) { - return Task.FromResult(prop switch - { - nameof(_backingProperties.Category) => _backingProperties.Category, - nameof(_backingProperties.Id) => _backingProperties.Id, - nameof(_backingProperties.Menu) => _backingProperties.Menu, - nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, - nameof(_backingProperties.Status) => _backingProperties.Status, - nameof(_backingProperties.Title) => _backingProperties.Title, - nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, - _ => null - }); - } - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) - { - _backingProperties.IconPixmap = new[] { dbusPixmap }; + BackingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); } @@ -352,102 +264,12 @@ namespace Avalonia.FreeDesktop if (text is null) return; - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - + BackingProperties.Id = text; + BackingProperties.Category = "ApplicationStatus"; + BackingProperties.Status = text; + BackingProperties.Title = text; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - [Dictionary] - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; - - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; - - public ToolTip ToolTip; - } - - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; - - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { - } - - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) - { - First = first; - Second = second; - Third = third; - Fourth = fourth; - } - } - - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - - public DbusPixmap(int width, int height, byte[] data) - { - Width = width; - Height = height; - Data = data; - } - - public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); - } } diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBus.xml b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml new file mode 100644 index 0000000000..a7ecce70f2 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml new file mode 100644 index 0000000000..de6868cb3e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml @@ -0,0 +1,437 @@ + + + + + + + + Name + Type + Description + Default Value + + + type + String + Can be one of: + - "standard": an item which can be clicked to trigger an action or + show another menu + - "separator": a separator + + Vendor specific types can be added by prefixing them with + "x--". + + "standard" + + + label + string + Text of the item, except that: + -# two consecutive underscore characters "__" are displayed as a + single underscore, + -# any remaining underscore characters are not displayed at all, + -# the first of those remaining underscore characters (unless it is + the last character in the string) indicates that the following + character is the access key. + + "" + + + enabled + boolean + Whether the item can be activated or not. + true + + + visible + boolean + True if the item is visible in the menu. + true + + + icon-name + string + Icon name of the item, following the freedesktop.org icon spec. + "" + + + icon-data + binary + PNG data of the icon. + Empty + + + shortcut + array of arrays of strings + The shortcut of the item. Each array represents the key press + in the list of keypresses. Each list of strings contains a list of + modifiers and then the key that is used. The modifier strings + allowed are: "Control", "Alt", "Shift" and "Super". + + - A simple shortcut like Ctrl+S is represented as: + [["Control", "S"]] + - A complex shortcut like Ctrl+Q, Alt+X is represented as: + [["Control", "Q"], ["Alt", "X"]] + Empty + + + toggle-type + string + + If the item can be toggled, this property should be set to: + - "checkmark": Item is an independent togglable item + - "radio": Item is part of a group where only one item can be + toggled at a time + - "": Item cannot be toggled + + "" + + + toggle-state + int + + Describe the current state of a "togglable" item. Can be one of: + - 0 = off + - 1 = on + - anything else = indeterminate + + Note: + The implementation does not itself handle ensuring that only one + item in a radio group is set to "on", or that a group does not have + "on" and "indeterminate" items simultaneously; maintaining this + policy is up to the toolkit wrappers. + + -1 + + + children-display + string + + If the menu item has children this property should be set to + "submenu". + + "" + + + disposition + string + + How the menuitem feels the information it's displaying to the + user should be presented. + - "normal" a standard menu item + - "informative" providing additional information to the user + - "warning" looking at potentially harmful results + - "alert" something bad could potentially happen + + "normal" + + + + Vendor specific properties can be added by prefixing them with + "x--". + ]]> + + + + + Provides the version of the DBusmenu API that this API is + implementing. + + + + + + Represents the way the text direction of the application. This + allows the server to handle mismatches intelligently. For left- + to-right the string is "ltr" for right-to-left it is "rtl". + + + + + + Tells if the menus are in a normal state or they believe that they + could use some attention. Cases for showing them would be if help + were referring to them or they accessors were being highlighted. + This property can have two values: "normal" in almost all cases and + "notice" when they should have a higher priority to be shown. + + + + + + A list of directories that should be used for finding icons using + the icon naming spec. Idealy there should only be one for the icon + theme, but additional ones are often added by applications for + app specific icons. + + + + + + + + Provides the layout and propertiers that are attached to the entries + that are in the layout. It only gives the items that are children + of the item that is specified in @a parentId. It will return all of the + properties or specific ones depending of the value in @a propertyNames. + + The format is recursive, where the second 'v' is in the same format + as the original 'a(ia{sv}av)'. Its content depends on the value + of @a recursionDepth. + + + The ID of the parent node for the layout. For + grabbing the layout from the root node use zero. + + + + The amount of levels of recursion to use. This affects the + content of the second variant array. + - -1: deliver all the items under the @a parentId. + - 0: no recursion, the array will be empty. + - n: array will contains items up to 'n' level depth. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + The revision number of the layout. For matching + with layoutUpdated signals. + + + The layout, as a recursive structure. + + + + + + Returns the list of items which are children of @a parentId. + + + + A list of ids that we should be finding the properties + on. If the list is empty, all menu items should be sent. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + + An array of property values. + An item in this area is represented as a struct following + this format: + @li id unsigned the item id + @li properties map(string => variant) the requested item properties + + + + + + + Get a signal property on a single item. This is not useful if you're + going to implement this interface, it should only be used if you're + debugging via a commandline tool. + + + the id of the item which received the event + + + the name of the property to get + + + the value of the property + + + + + -" + ]]> + + the id of the item which received the event + + + the type of event + + + event-specific data + + + The time that the event occured if available or the time the message was sent if not + + + + + + Used to pass a set of events as a single message for possibily several + different menuitems. This is done to optimize DBus traffic. + + + + An array of all the events that should be passed. This tuple should + match the parameters of the 'Event' signal. Which is roughly: + id, eventID, data and timestamp. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + This is called by the applet to notify the application that it is about + to show the menu under the specified item. + + + + Which menu item represents the parent of the item about to be shown. + + + + + Whether this AboutToShow event should result in the menu being updated. + + + + + + + A function to tell several menus being shown that they are about to + be shown to the user. This is likely only useful for programitc purposes + so while the return values are returned, in general, the singular function + should be used in most user interacation scenarios. + + + + The IDs of the menu items who's submenus are being shown. + + + + + The IDs of the menus that need updates. Note: if no update information + is needed the DBus message should set the no reply flag. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + + Triggered when there are lots of property updates across many items + so they all get grouped into a single dbus message. The format is + the ID of the item with a hashtable of names and values for those + properties. + + + + + + + Triggered by the application to notify display of a layout update, up to + revision + + + The revision of the layout that we're currently on + + + + If the layout update is only of a subtree, this is the + parent item for the entries that have changed. It is zero if + the whole layout should be considered invalid. + + + + + + The server is requesting that all clients displaying this + menu open it to the user. This would be for things like + hotkeys that when the user presses them the menu should + open and display itself to the user. + + + ID of the menu that should be activated + + + The time that the event occured + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml new file mode 100644 index 0000000000..7866a74639 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml new file mode 100644 index 0000000000..2eb1a7a0b8 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000000..42a71707b6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window.  This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml new file mode 100644 index 0000000000..b30d94cebf --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml new file mode 100644 index 0000000000..6cb130d48a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml new file mode 100644 index 0000000000..b8d60f0d37 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml new file mode 100644 index 0000000000..0cc358a09a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml new file mode 100644 index 0000000000..376ad424d4 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml new file mode 100644 index 0000000000..2ae3546955 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000000..c1abb4eb7b --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000000..669997a3df --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 147955b6a3..df22f46323 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -15,18 +15,18 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { var symlinkSize = Encoding.UTF8.GetByteCount(path); - var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated + const int BufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); - var buffer = ArrayPool.Shared.Rent(bufferSize); + var buffer = ArrayPool.Shared.Rent(BufferSize); try { Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; - var size = readlink(symlink, buffer, bufferSize); - Debug.Assert(size < bufferSize); // if this fails, we need to increase the buffer size (dynamically?) + var size = readlink(symlink, buffer, BufferSize); + Debug.Assert(size < BufferSize); // if this fails, we need to increase the buffer size (dynamically?) return Encoding.UTF8.GetString(buffer, 0, (int)size); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 68466fe381..31aaebcdc7 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -325,7 +325,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } @@ -392,7 +392,7 @@ namespace Avalonia.Headless } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect rect) { } @@ -491,7 +491,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 9f1c8883aa..5a6b0df801 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { @@ -56,8 +56,13 @@ namespace Avalonia.Native { if(fmt.String == NSPasteboardTypeString) rv.Add(DataFormats.Text); - if(fmt.String == NSFilenamesPboardType) - rv.Add(DataFormats.FileNames); + if (fmt.String == NSFilenamesPboardType) + { +#pragma warning disable CS0618 // Type or member is obsolete + rv.Add(DataFormats.FileNames); +#pragma warning restore CS0618 // Type or member is obsolete + rv.Add(DataFormats.Files); + } } } } @@ -74,7 +79,13 @@ namespace Avalonia.Native public IEnumerable GetFileNames() { using (var strings = _native.GetStrings(NSFilenamesPboardType)) - return strings.ToStringArray(); + return strings?.ToStringArray(); + } + + public IEnumerable GetFiles() + { + return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + .Where(f => f is not null); } public unsafe Task SetDataObjectAsync(IDataObject data) @@ -102,8 +113,12 @@ namespace Avalonia.Native { if (format == DataFormats.Text) return await GetTextAsync(); +#pragma warning disable CS0618 // Type or member is obsolete if (format == DataFormats.FileNames) return GetFileNames(); +#pragma warning restore CS0618 // Type or member is obsolete + if (format == DataFormats.Files) + return GetFiles(); using (var n = _native.GetBytes(format)) return n.Bytes; } @@ -131,20 +146,16 @@ namespace Avalonia.Native public bool Contains(string dataFormat) => Formats.Contains(dataFormat); - public string GetText() - { - // bad idea in general, but API is synchronous anyway - return _clipboard.GetTextAsync().Result; - } - - public IEnumerable GetFileNames() => _clipboard.GetFileNames(); - public object Get(string dataFormat) { if (dataFormat == DataFormats.Text) - return GetText(); + return _clipboard.GetTextAsync().Result; + if (dataFormat == DataFormats.Files) + return _clipboard.GetFiles(); +#pragma warning disable CS0618 if (dataFormat == DataFormats.FileNames) - return GetFileNames(); +#pragma warning restore CS0618 + return _clipboard.GetFileNames(); return null; } } diff --git a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml b/src/Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml similarity index 50% rename from src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml rename to src/Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml index c3f489da80..c4b91b4822 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FocusAdorner.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/AdornerLayer.xaml @@ -1,14 +1,11 @@ - - - 0 - 2 - 1 - - - - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 532b0cff1b..31b0a01b21 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -4,6 +4,7 @@ + @@ -74,6 +75,5 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/AdornerLayer.xaml b/src/Avalonia.Themes.Simple/Controls/AdornerLayer.xaml new file mode 100644 index 0000000000..1f3acb07dc --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/AdornerLayer.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/FocusAdorner.xaml b/src/Avalonia.Themes.Simple/Controls/FocusAdorner.xaml deleted file mode 100644 index f1d5f5f2ac..0000000000 --- a/src/Avalonia.Themes.Simple/Controls/FocusAdorner.xaml +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 479db9ed09..dc533488c9 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -3,6 +3,7 @@ + @@ -72,6 +73,5 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml index 3c621a981d..2a7dc081f0 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml @@ -2,8 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="using:Avalonia.Controls.Converters"> - + @@ -26,37 +25,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -75,66 +44,75 @@ - - - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml index d6f293a730..f839e9a598 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml @@ -17,8 +17,6 @@ 320 48 - 00:00:00.2 00:00:00.1 0.1,0.9,0.2,1.0 @@ -240,7 +238,9 @@ diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 56fd2f14ef..13068832fb 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -115,7 +115,7 @@ namespace Avalonia.X11 using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) - using (var ctx = renderTarget.CreateDrawingContext(null)) + using (var ctx = renderTarget.CreateDrawingContext()) { var r = new Rect(_pixelSize.ToSize(1)); ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 51db815b31..84a1d35712 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -43,7 +43,7 @@ namespace Avalonia.X11 _bdata = new uint[_width * _height]; using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) using(var rt = cpuContext.CreateRenderTarget(new[]{this})) - using (var ctx = rt.CreateDrawingContext(null)) + using (var ctx = rt.CreateDrawingContext()) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 9f09212814..6634ab4d7b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -212,10 +212,10 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - _storageProvider = new CompositeStorageProvider(new Func>[] + _storageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null), - () => GtkSystemDialog.TryCreate(this), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => GtkSystemDialog.TryCreate(this) }); } diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3bb7260e55..76947c949c 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -106,6 +106,8 @@ namespace Avalonia.Browser InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnPointerCancel, OnWheel); + InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent); + var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = DomHelper.ObserveDpi(OnDpiChanged); @@ -293,6 +295,59 @@ namespace Avalonia.Browser return modifiers; } + public bool OnDragEvent(JSObject args) + { + var eventType = args?.GetPropertyAsString("type") switch + { + "dragenter" => RawDragEventType.DragEnter, + "dragover" => RawDragEventType.DragOver, + "dragleave" => RawDragEventType.DragLeave, + "drop" => RawDragEventType.Drop, + _ => (RawDragEventType)(int)-1 + }; + var dataObject = args?.GetPropertyAsJSObject("dataTransfer"); + if (args is null || eventType < 0 || dataObject is null) + { + return false; + } + + // If file is dropped, we need storage js to be referenced. + // TODO: restructure JS files, so it's not needed. + _ = AvaloniaModule.ImportStorage(); + + var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")); + var modifiers = GetModifiers(args); + + var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none"; + var effectAllowed = DragDropEffects.None; + if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Copy; + } + if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Link; + } + if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move; + } + if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link; + } + if (effectAllowed == DragDropEffects.None) + { + return false; + } + + var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed); + dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); + + return eventType is RawDragEventType.Drop or RawDragEventType.DragOver + && dropEffect != DragDropEffects.None; + } + private bool OnKeyDown (string code, string key, int modifier) { var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); diff --git a/src/Browser/Avalonia.Browser/BrowserDataObject.cs b/src/Browser/Avalonia.Browser/BrowserDataObject.cs new file mode 100644 index 0000000000..f1e30ee3fe --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserDataObject.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using Avalonia.Browser.Interop; +using Avalonia.Browser.Storage; +using Avalonia.Input; +using Avalonia.Platform.Storage; + +namespace Avalonia.Browser; + +internal class BrowserDataObject : IDataObject +{ + private readonly JSObject _dataObject; + + public BrowserDataObject(JSObject dataObject) + { + _dataObject = dataObject; + } + + public IEnumerable GetDataFormats() + { + var types = new HashSet(_dataObject.GetPropertyAsStringArray("types")); + var dataFormats = new HashSet(types.Count); + + foreach (var type in types) + { + if (type.StartsWith("text/", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Text); + } + else if (type.Equals("Files", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Files); + } + dataFormats.Add(type); + } + + // If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list". + // With "text/uri-list" browser can add actual file as well. + var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count"); + if (filesCount > 0) + { + dataFormats.Add(DataFormats.Files); + } + + return dataFormats; + } + + public bool Contains(string dataFormat) + { + return GetDataFormats().Contains(dataFormat); + } + + public object? Get(string dataFormat) + { + if (dataFormat == DataFormats.Files) + { + var files = _dataObject.GetPropertyAsJSObject("files"); + if (files is not null) + { + return StorageHelper.FilesToItemsArray(files) + .Select(reference => reference.GetPropertyAsString("kind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } + + return null; + } + + if (dataFormat == DataFormats.Text) + { + if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData) + { + return textData; + } + } + + if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data) + { + return data; + } + + return null; + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index f1cd441f45..1bf4636f61 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -164,6 +164,15 @@ namespace Avalonia.Browser return false; } + + public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect) + { + var device = AvaloniaLocator.Current.GetRequiredService(); + var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers); + Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}"); + Input?.Invoke(eventArgs); + return eventArgs.Effects; + } public void Dispose() { diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index b94fe2df9e..c4f5e90777 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -24,6 +24,6 @@ namespace Avalonia.Browser public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - public Task GetDataAsync(string format) => Task.FromResult(new()); + public Task GetDataAsync(string format) => Task.FromResult(null); } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index f1936a8d97..394f191dab 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -1,24 +1,29 @@ -using System.Runtime.InteropServices.JavaScript; +using System; +using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; namespace Avalonia.Browser.Interop; internal static partial class AvaloniaModule { - public const string MainModuleName = "avalonia"; - public const string StorageModuleName = "storage"; - - public static Task ImportMain() + private static readonly Lazy s_importMain = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); - } + }); - public static Task ImportStorage() + private static readonly Lazy s_importStorage = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); - } + }); + + public const string MainModuleName = "avalonia"; + public const string StorageModuleName = "storage"; + + public static Task ImportMain() => s_importMain.Value; + + public static Task ImportStorage() => s_importStorage.Value; [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] public static partial bool IsMobile(); diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs new file mode 100644 index 0000000000..6e3b41c05b --- /dev/null +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Browser.Interop; + +internal static partial class GeneralHelpers +{ + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); + public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); + public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); + + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodString(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); +} diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index a816e39da8..a978c18f9b 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -43,13 +43,16 @@ internal static partial class InputHelper [JSMarshalAs>] Func wheel); - [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeInputEvents( JSObject htmlElement, [JSMarshalAs>] Func input); + [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeDropEvents(JSObject containerElement, + [JSMarshalAs>] Func dragEvent); + [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)] [return: JSMarshalAs>] public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 11beba6f2c..2d96ee8d1f 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -46,6 +46,9 @@ internal static partial class StorageHelper [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); + + [JSImport("StorageItems.filesToItemsArray", AvaloniaModule.StorageModuleName)] + public static partial JSObject[] FilesToItemsArray(JSObject item); [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions); diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index 3c04935f0d..a169966188 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -21,7 +21,7 @@ namespace Avalonia.Browser.Skia return null; } - public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session) { return null; } diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 5b76d53a9d..fc32b3b4f7 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices.JavaScript; -using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Platform.Storage; @@ -18,15 +16,13 @@ internal class BrowserStorageProvider : IStorageProvider internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; - private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); - public bool CanOpen => true; public bool CanSave => StorageHelper.HasNativeFilePicker(); public bool CanPickFolder => true; public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter); @@ -60,7 +56,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices); @@ -88,7 +84,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; try @@ -104,14 +100,14 @@ internal class BrowserStorageProvider : IStorageProvider public async Task OpenFileBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFile(item) : null; } public async Task OpenFolderBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFolder(item) : null; } @@ -128,7 +124,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch { WellKnownFolder.Desktop => "desktop", diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 3fb4124c96..80faca7a50 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -5,6 +5,7 @@ import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; +import { GeneralHelpers } from "./avalonia/generalHelpers"; export { Caniuse, @@ -15,5 +16,6 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper + NavigationHelper, + GeneralHelpers }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts new file mode 100644 index 0000000000..fa001006ab --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -0,0 +1,19 @@ +export class GeneralHelpers { + public static itemsArrayAt(instance: any, key: string): any[] { + const items = instance[key]; + if (!items) { + return []; + } + + const retItems = []; + for (let i = 0; i < items.length; i++) { + retItems[i] = items[i]; + } + return retItems; + } + + public static callMethod(instance: any, name: string /*, args */): any { + const args = Array.prototype.slice.call(arguments, 2); + return instance[name].apply(instance, args); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 0f0e5eb512..fb94352192 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -174,6 +174,28 @@ export class InputHelper { }; } + public static subscribeDropEvents( + element: HTMLInputElement, + dragEvent: (args: any) => boolean + ) { + const dragHandler = (args: Event) => { + if (dragEvent(args as any)) { + args.preventDefault(); + } + }; + element.addEventListener("dragover", dragHandler); + element.addEventListener("dragenter", dragHandler); + element.addEventListener("dragleave", dragHandler); + element.addEventListener("drop", dragHandler); + + return () => { + element.removeEventListener("dragover", dragHandler); + element.removeEventListener("dragenter", dragHandler); + element.removeEventListener("dragleave", dragHandler); + element.removeEventListener("drop", dragHandler); + }; + } + public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] { return pointerEvent.getCoalescedEvents(); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index 8f47e61100..f444717094 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -3,8 +3,9 @@ import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFile import { Caniuse } from "../avalonia"; export class StorageItem { - constructor( + private constructor( public handle?: FileSystemFileHandle | FileSystemDirectoryHandle, + private readonly file?: File, private readonly bookmarkId?: string, public wellKnownType?: WellKnownDirectory ) { @@ -14,6 +15,9 @@ export class StorageItem { if (this.handle) { return this.handle.name; } + if (this.file) { + return this.file.name; + } return this.wellKnownType ?? ""; } @@ -21,14 +25,29 @@ export class StorageItem { if (this.handle) { return this.handle.kind; } + if (this.file) { + return "file"; + } return "directory"; } + public static createFromHandle(handle: FileSystemFileHandle | FileSystemDirectoryHandle, bookmarkId?: string) { + return new StorageItem(handle, undefined, bookmarkId, undefined); + } + + public static createFromFile(file: File) { + return new StorageItem(undefined, file, undefined, undefined); + } + public static createWellKnownDirectory(type: WellKnownDirectory) { - return new StorageItem(undefined, undefined, type); + return new StorageItem(undefined, undefined, undefined, type); } public static async openRead(item: StorageItem): Promise { + if (item.file) { + return item.file; + } + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } @@ -41,7 +60,7 @@ export class StorageItem { public static async openWrite(item: StorageItem): Promise { if (!item.handle || item.kind !== "file") { - throw new Error("StorageItem is not a file"); + throw new Error("StorageItem is not a writeable file"); } await item.verityPermissions("readwrite"); @@ -52,8 +71,9 @@ export class StorageItem { public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { // getFile can fail with an exception depending if we use polyfill with a save file dialog or not. try { - const file = item.handle instanceof FileSystemFileHandle && - await item.handle.getFile(); + const file = item.handle && "getFile" in item.handle + ? await item.handle.getFile() + : item.file; if (!file) { return null; @@ -144,4 +164,16 @@ export class StorageItems { public static itemsArray(instance: StorageItems): StorageItem[] { return instance.items; } + + public static filesToItemsArray(files: File[]): StorageItem[] { + if (!files) { + return []; + } + + const retItems = []; + for (let i = 0; i < files.length; i++) { + retItems[i] = StorageItem.createFromFile(files[i]); + } + return retItems; + } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index 750c38b8ea..7a29992674 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -19,7 +19,7 @@ export class StorageProvider { }; const handle = await showDirectoryPicker(options as any); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openFileDialog( @@ -33,7 +33,7 @@ export class StorageProvider { }; const handles = await showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle))); + return new StorageItems(handles.map((handle: FileSystemFileHandle) => StorageItem.createFromHandle(handle))); } public static async saveFileDialog( @@ -48,14 +48,14 @@ export class StorageProvider { // Always prefer native save file picker, as polyfill solutions are not reliable. const handle = await (globalThis as any).showSaveFilePicker(options); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openBookmark(key: string): Promise { const connection = await avaloniaDb.connect(); try { const handle = await connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); + return handle && StorageItem.createFromHandle(handle, key); } finally { connection.close(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index cdd344becc..bef32766dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -34,7 +34,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); - var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant; + var themeVariant = (provideTarget.TargetObject as IThemeVariantHost)?.ActualThemeVariant; var targetType = provideTarget.TargetProperty switch { diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 4c3cfe2ef4..ab9f9ea413 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 40d7e10ae3..170cc9d420 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; using Avalonia.Media; using SkiaSharp; -#nullable enable - namespace Avalonia.Skia { /// @@ -13,23 +10,24 @@ namespace Avalonia.Skia { public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath; - var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath; + var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; + var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, GeometryCombineMode.Xor => SKPathOp.Xor, GeometryCombineMode.Exclude => SKPathOp.Difference, - _ => SKPathOp.Union, + _ => SKPathOp.Union }; - var path = path1.Op(path2, op); + var path = path1?.Op(path2, op); EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = path?.Bounds.ToAvaloniaRect() ?? default; } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath? EffectivePath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index eededb2836..db7b068543 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,7 +10,9 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Skia.Helpers; using SkiaSharp; +using ISceneBrush = Avalonia.Media.ISceneBrush; namespace Avalonia.Skia { @@ -19,27 +21,27 @@ namespace Avalonia.Skia /// internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { - private IDisposable[] _disposables; + private IDisposable?[]? _disposables; private readonly Vector _dpi; - private readonly Stack _maskStack = new Stack(); - private readonly Stack _opacityStack = new Stack(); - private readonly Stack _blendingModeStack = new Stack(); + private readonly Stack _maskStack = new(); + private readonly Stack _opacityStack = new(); + private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; private Matrix _currentTransform; private bool _disposed; - private GRContext _grContext; - public GRContext GrContext => _grContext; - private ISkiaGpu _gpu; + private GRContext? _grContext; + public GRContext? GrContext => _grContext; + private readonly ISkiaGpu? _gpu; private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get(); private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get(); private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get(); - private static SKShader s_acrylicNoiseShader; - private readonly ISkiaGpuRenderSession _session; - private bool _leased = false; + private static SKShader? s_acrylicNoiseShader; + private readonly ISkiaGpuRenderSession? _session; + private bool _leased; + private bool _useOpacitySaveLayer; /// /// Context create info. @@ -49,23 +51,18 @@ namespace Avalonia.Skia /// /// Canvas to draw to. /// - public SKCanvas Canvas; + public SKCanvas? Canvas; /// /// Surface to draw to. /// - public SKSurface Surface; + public SKSurface? Surface; /// /// Dpi of drawings. /// public Vector Dpi; - - /// - /// Visual brush renderer. - /// - public IVisualBrushRenderer VisualBrushRenderer; - + /// /// Render text without Lcd rendering. /// @@ -74,17 +71,17 @@ namespace Avalonia.Skia /// /// GPU-accelerated context (optional) /// - public GRContext GrContext; + public GRContext? GrContext; /// /// Skia GPU provider context (optional) /// - public ISkiaGpu Gpu; + public ISkiaGpu? Gpu; - public ISkiaGpuRenderSession CurrentSession; + public ISkiaGpuRenderSession? CurrentSession; } - class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature + private class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature { private readonly DrawingContextImpl _context; @@ -99,10 +96,11 @@ namespace Avalonia.Skia return new ApiLease(_context); } - class ApiLease : ISkiaSharpApiLease + private class ApiLease : ISkiaSharpApiLease { - private DrawingContextImpl _context; + private readonly DrawingContextImpl _context; private readonly SKMatrix _revertTransform; + private bool _isDisposed; public ApiLease(DrawingContextImpl context) { @@ -112,15 +110,18 @@ namespace Avalonia.Skia } public SKCanvas SkCanvas => _context.Canvas; - public GRContext GrContext => _context.GrContext; - public SKSurface SkSurface => _context.Surface; + public GRContext? GrContext => _context.GrContext; + public SKSurface? SkSurface => _context.Surface; public double CurrentOpacity => _context._currentOpacity; public void Dispose() { - _context.Canvas.SetMatrix(_revertTransform); - _context._leased = false; - _context = null; + if (!_isDisposed) + { + _context.Canvas.SetMatrix(_revertTransform); + _context._leased = false; + _isDisposed = true; + } } } } @@ -130,10 +131,12 @@ namespace Avalonia.Skia /// /// Create info. /// Array of elements to dispose after drawing has finished. - public DrawingContextImpl(CreateInfo createInfo, params IDisposable[] disposables) + public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposables) { + Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas + ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + _dpi = createInfo.Dpi; - _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; @@ -141,15 +144,9 @@ namespace Avalonia.Skia if (_grContext != null) Monitor.Enter(_grContext); Surface = createInfo.Surface; - Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas; _session = createInfo.CurrentSession; - if (Canvas == null) - { - throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); - } - if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) { _postTransform = @@ -157,13 +154,20 @@ namespace Avalonia.Skia } Transform = Matrix.Identity; + + var options = AvaloniaLocator.Current.GetService(); + + if(options != null) + { + _useOpacitySaveLayer = options.UseOpacitySaveLayer; + } } /// /// Skia canvas. /// public SKCanvas Canvas { get; } - public SKSurface Surface { get; } + public SKSurface? Surface { get; } private void CheckLease() { @@ -187,7 +191,7 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity))); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); @@ -205,87 +209,89 @@ namespace Avalonia.Skia } /// - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { CheckLease(); - if (pen is null) - { - return; - } - - using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))) is { } stroke) { - if (paint.Paint is object) + using (stroke) { - Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, paint.Paint); + Canvas.DrawLine((float)p1.X, (float)p1.Y, (float)p2.X, (float)p2.Y, stroke.Paint); } } } /// - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { CheckLease(); var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default) - using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, - size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0))) : default) + if (brush is not null) { - if (fill.Paint != null) + using (var fill = CreatePaint(_fillPaint, brush, size)) { Canvas.DrawPath(impl.EffectivePath, fill.Paint); } + } - if (stroke.Paint != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) + { + using (stroke) { Canvas.DrawPath(impl.EffectivePath, stroke.Paint); } } } - struct BoxShadowFilter : IDisposable + private struct BoxShadowFilter : IDisposable { - public SKPaint Paint; - private SKImageFilter _filter; - public SKClipOperation ClipOperation; + public readonly SKPaint Paint; + private readonly SKImageFilter? _filter; + public readonly SKClipOperation ClipOperation; + + private BoxShadowFilter(SKPaint paint, SKImageFilter? filter, SKClipOperation clipOperation) + { + Paint = paint; + _filter = filter; + ClipOperation = clipOperation; + } - static float SkBlurRadiusToSigma(double radius) { + private static float SkBlurRadiusToSigma(double radius) { if (radius <= 0) return 0.0f; return 0.288675f * (float)radius + 0.5f; } + public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity) { var ac = shadow.Color; - SKImageFilter filter = null; - filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); + var filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)); paint.Reset(); paint.IsAntialias = true; paint.Color = color; paint.ImageFilter = filter; - - return new BoxShadowFilter - { - Paint = paint, _filter = filter, - ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference - }; + + var clipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference; + + return new BoxShadowFilter(paint, filter, clipOperation); } public void Dispose() { - Paint.Reset(); - Paint = null; + Paint?.Reset(); _filter?.Dispose(); } } - static SKRect AreaCastingShadowInHole( + private static SKRect AreaCastingShadowInHole( SKRect hole_rect, float shadow_blur, float shadow_spread, @@ -306,18 +312,16 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + public void DrawRectangle(IExperimentalAcrylicMaterial? material, RoundedRect rect) { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; CheckLease(); var rc = rect.Rect.ToSKRect(); - var isRounded = rect.IsRounded; - var needRoundRect = rect.IsRounded; - SKRoundRect skRoundRect = null; + SKRoundRect? skRoundRect = null; - if (needRoundRect) + if (rect.IsRounded) { skRoundRect = SKRoundRectCache.Shared.Get(); skRoundRect.SetRectRadii(rc, @@ -334,7 +338,7 @@ namespace Avalonia.Skia { using (var paint = CreateAcrylicPaint(_fillPaint, material)) { - if (isRounded) + if (skRoundRect is not null) { Canvas.DrawRoundRect(skRoundRect, paint.Paint); SKRoundRectCache.Shared.Return(skRoundRect); @@ -349,7 +353,7 @@ namespace Avalonia.Skia } /// - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; @@ -362,7 +366,7 @@ namespace Avalonia.Skia var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); - SKRoundRect skRoundRect = null; + SKRoundRect? skRoundRect = null; if (needRoundRect) { skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect); @@ -372,7 +376,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && !boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -412,15 +416,15 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size)) + using (var fill = CreatePaint(_fillPaint, brush, rect.Rect.Size)) { if (isRounded) { - Canvas.DrawRoundRect(skRoundRect, paint.Paint); + Canvas.DrawRoundRect(skRoundRect, fill.Paint); } else { - Canvas.DrawRect(rc, paint.Paint); + Canvas.DrawRect(rc, fill.Paint); } } } @@ -429,7 +433,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; @@ -454,30 +458,28 @@ namespace Avalonia.Skia } } - if (pen?.Brush != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) + using (stroke) { - if (paint.Paint is object) + if (isRounded) { - if (isRounded) - { - Canvas.DrawRoundRect(skRoundRect, paint.Paint); - } - else - { - Canvas.DrawRect(rc, paint.Paint); - } + Canvas.DrawRoundRect(skRoundRect, stroke.Paint); + } + else + { + Canvas.DrawRect(rc, stroke.Paint); } } } - if(isRounded) + if (skRoundRect is not null) SKRoundRectCache.Shared.Return(skRoundRect); } /// - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { if (rect.Height <= 0 || rect.Width <= 0) return; @@ -487,26 +489,24 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) + using (var fill = CreatePaint(_fillPaint, brush, rect.Size)) { - Canvas.DrawOval(rc, paint.Paint); + Canvas.DrawOval(rc, fill.Paint); } } - if (pen?.Brush != null) + if (pen is not null + && TryCreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { - using (var paint = CreatePaint(_strokePaint, pen, rect.Size.Inflate(new Thickness(pen?.Thickness / 2 ?? 0)))) + using (stroke) { - if (paint.Paint is object) - { - Canvas.DrawOval(rc, paint.Paint); - } + Canvas.DrawOval(rc, stroke.Paint); } } } /// - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { CheckLease(); @@ -571,18 +571,35 @@ namespace Avalonia.Skia } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { CheckLease(); - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; + + if(_useOpacitySaveLayer) + { + var rect = bounds.ToSKRect(); + Canvas.SaveLayer(rect, new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity)}); + } + else + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } } /// public void PopOpacity() { CheckLease(); - _currentOpacity = _opacityStack.Pop(); + + if(_useOpacitySaveLayer) + { + Canvas.Restore(); + } + else + { + _currentOpacity = _opacityStack.Pop(); + } } /// @@ -660,7 +677,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); - Canvas.SaveLayer(paint); + Canvas.SaveLayer(bounds.ToSKRect(), paint); _maskStack.Push(CreatePaint(paint, mask, bounds.Size)); } @@ -711,14 +728,12 @@ namespace Avalonia.Skia } } -#nullable enable public object? GetFeature(Type t) { if (t == typeof(ISkiaSharpApiLeaseFeature)) return new SkiaLeaseFeature(this); return null; } -#nullable restore /// /// Configure paint wrapper for using gradient brush. @@ -888,7 +903,7 @@ namespace Avalonia.Skia paintWrapper.AddDisposable(intermediate); - using (var context = intermediate.CreateDrawingContext(null)) + using (var context = intermediate.CreateDrawingContext()) { var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); @@ -950,39 +965,102 @@ namespace Avalonia.Skia } } - /// - /// Configure paint wrapper to use visual brush. - /// - /// Paint wrapper. - /// Visual brush. - /// Visual brush renderer. - /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, IVisualBrushRenderer visualBrushRenderer, ref IDrawableBitmapImpl tileBrushImage) + private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) { - if (_visualBrushRenderer == null) - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } - - var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); + if(content.UseScalableRasterization) + ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize); + else + ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize); + } + + private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - var intermediate = CreateRenderTarget(intermediateSize, false); + using var intermediate = CreateRenderTarget(intermediateSize, false); - using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) + using (var ctx = intermediate.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); - - visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } - tileBrushImage = intermediate; - paintWrapper.AddDisposable(tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate); + } + } + + private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var contentSize = rect.Size; + if (contentSize.Width <= 0 || contentSize.Height <= 0) + { + paintWrapper.Paint.Color = SKColor.Empty; + return; + } + + var tileBrush = content.Brush; + var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y); + + var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize); + transform *= calc.IntermediateTransform; + + using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); + using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) + { + ctx.PushClip(calc.IntermediateClip); + content.Render(ctx, transform); + ctx.PopClip(); + } + + using var picture = pictureTarget.GetPicture(); + + var paintTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); + + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + paintTransform = SKMatrix.Concat(paintTransform, + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + + if (tileBrush.Transform is { }) + { + var origin = tileBrush.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(origin); + var brushTransform = (-offset) * tileBrush.Transform.Value * (offset); + + paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix()); + } + + using (var shader = picture.ToShader(tileX, tileY, paintTransform, + new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height))) + { + paintWrapper.Paint.FilterQuality = SKFilterQuality.None; + paintWrapper.Paint.Shader = shader; } } - static SKColorFilter CreateAlphaColorFilter(double opacity) + private static SKColorFilter CreateAlphaColorFilter(double opacity) { if (opacity > 1) opacity = 1; @@ -997,7 +1075,7 @@ namespace Avalonia.Skia return SKColorFilter.CreateTable(a, c, c, c); } - static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + private static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) { var ca = leftColor / 255d; var aa = leftAlpha / 255d; @@ -1007,7 +1085,7 @@ namespace Avalonia.Skia return (byte)(r * 255); } - static Color Blend(Color left, Color right) + private static Color Blend(Color left, Color right) { var aa = left.A / 255d; var ab = right.A / 255d; @@ -1025,8 +1103,6 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = _currentOpacity; - var tintOpacity = material.BackgroundSource == AcrylicBackgroundSource.Digger ? material.TintOpacity : 1; @@ -1075,7 +1151,7 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = brush.Opacity * _currentOpacity; + double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity); if (brush is ISolidColorBrush solid) { @@ -1094,16 +1170,29 @@ namespace Avalonia.Skia } var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; var tileBrushImage = default(IDrawableBitmapImpl); - if (visualBrush != null) + if (brush is ISceneBrush sceneBrush) { - ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + using (var content = sceneBrush.CreateContent()) + { + if (content != null) + { + ConfigureSceneBrushContent(ref paintWrapper, content, targetSize); + return paintWrapper; + } + else + paint.Color = default; + } + } + else if (brush is ISceneBrushContent sceneBrushContent) + { + ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize); + return paintWrapper; } else { - tileBrushImage = (IDrawableBitmapImpl)(tileBrush as IImageBrush)?.Source?.PlatformImpl.Item; + tileBrushImage = (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item as IDrawableBitmapImpl; } if (tileBrush != null && tileBrushImage != null) @@ -1125,16 +1214,16 @@ namespace Avalonia.Skia /// Source pen. /// Target size. /// - private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize) + private PaintWrapper? TryCreatePaint(SKPaint paint, IPen pen, Size targetSize) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. - if (pen.Thickness == 0d) + if (pen.Brush is not { } brush || pen.Thickness == 0d) { - return default; + return null; } - var rv = CreatePaint(paint, pen.Brush, targetSize); + var rv = CreatePaint(paint, brush, targetSize); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -1253,9 +1342,9 @@ namespace Avalonia.Skia //We are saving memory allocations there public readonly SKPaint Paint; - private IDisposable _disposable1; - private IDisposable _disposable2; - private IDisposable _disposable3; + private IDisposable? _disposable1; + private IDisposable? _disposable2; + private IDisposable? _disposable3; public PaintWrapper(SKPaint paint) { diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index f6a4307ece..a35113dc26 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -26,11 +26,11 @@ namespace Avalonia.Skia return _skFontManager.FontFamilies; } - [ThreadStatic] private static string[] t_languageTagBuffer; + [ThreadStatic] private static string[]? t_languageTagBuffer; public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface fontKey) + FontFamily? fontFamily, CultureInfo? culture, out Typeface fontKey) { SKFontStyle skFontStyle; @@ -53,20 +53,13 @@ namespace Avalonia.Skia break; } - if (culture == null) - { - culture = CultureInfo.CurrentUICulture; - } - - if (t_languageTagBuffer == null) - { - t_languageTagBuffer = new string[2]; - } + culture ??= CultureInfo.CurrentUICulture; + t_languageTagBuffer ??= new string[2]; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) + if (fontFamily is not null && fontFamily.FamilyNames.HasFallbacks) { var familyNames = fontFamily.FamilyNames; @@ -104,9 +97,9 @@ namespace Avalonia.Skia public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) { - SKTypeface skTypeface = null; + SKTypeface? skTypeface = null; - if(typeface.FontFamily.Key != null) + if(typeface.FontFamily.Key is not null null) { var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 05fad25f1b..a22b67e09e 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Reactive; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; @@ -15,9 +16,9 @@ namespace Avalonia.Skia private readonly IFramebufferPlatformSurface _platformSurface; private SKImageInfo _currentImageInfo; private IntPtr _currentFramebufferAddress; - private SKSurface _framebufferSurface; - private PixelFormatConversionShim _conversionShim; - private IDisposable _preFramebufferCopyHandler; + private SKSurface? _framebufferSurface; + private PixelFormatConversionShim? _conversionShim; + private IDisposable? _preFramebufferCopyHandler; /// /// Create new framebuffer render target using a target surface. @@ -35,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -54,7 +55,6 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true }; @@ -81,6 +81,7 @@ namespace Avalonia.Skia /// /// Desired image info. /// Backing framebuffer. + [MemberNotNull(nameof(_framebufferSurface))] private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) { if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index d6f19612c1..2828f9a9c1 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using Avalonia.Media; using SkiaSharp; -#nullable enable - namespace Avalonia.Skia { /// @@ -22,8 +20,10 @@ namespace Avalonia.Skia for (var i = 0; i < count; ++i) { - if (children[i]?.PlatformImpl is GeometryImpl child) - path.AddPath(child.EffectivePath); + if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + { + path.AddPath(effectivePath); + } } EffectivePath = path; diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 51386d2a45..34270c2078 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -11,20 +12,9 @@ namespace Avalonia.Skia internal abstract class GeometryImpl : IGeometryImpl { private PathCache _pathCache; - private SKPathMeasure _pathMeasureCache; + private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure - { - get - { - if (_pathMeasureCache is null) - { - _pathMeasureCache = new SKPathMeasure(EffectivePath); - } - - return _pathMeasureCache; - } - } + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); /// public abstract Rect Bounds { get; } @@ -37,11 +27,11 @@ namespace Avalonia.Skia if (EffectivePath is null) return 0; - return (double)CachedPathMeasure?.Length; + return CachedPathMeasure.Length; } } - public abstract SKPath EffectivePath { get; } + public abstract SKPath? EffectivePath { get; } /// public bool FillContains(Point point) @@ -50,7 +40,7 @@ namespace Avalonia.Skia } /// - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { // Skia requires to compute stroke path to check for point containment. // Due to that we are caching using stroke width. @@ -98,21 +88,26 @@ namespace Avalonia.Skia /// Path to check. /// Point. /// True, if point is contained in a path. - private static bool PathContainsCore(SKPath path, Point point) + private static bool PathContainsCore(SKPath? path, Point point) { - return path.Contains((float)point.X, (float)point.Y); + return path is not null && path.Contains((float)point.X, (float)point.Y); } /// - public IGeometryImpl Intersect(IGeometryImpl geometry) + public IGeometryImpl? Intersect(IGeometryImpl geometry) { - var result = EffectivePath.Op(((GeometryImpl)geometry).EffectivePath, SKPathOp.Intersect); + if (EffectivePath is { } path + && (geometry as GeometryImpl)?.EffectivePath is { } otherPath + && path.Op(otherPath, SKPathOp.Intersect) is { } result) + { + return new StreamGeometryImpl(result); + } - return result == null ? null : new StreamGeometryImpl(result); + return null; } /// - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); @@ -161,7 +156,7 @@ namespace Avalonia.Skia } public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, - out IGeometryImpl segmentGeometry) + [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { if (EffectivePath is null) { @@ -203,7 +198,7 @@ namespace Avalonia.Skia /// /// Cached contour path. /// - public SKPath CachedStrokePath { get; private set; } + public SKPath? CachedStrokePath { get; private set; } /// /// Cached geometry render bounds. @@ -244,6 +239,7 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); + CachedStrokePath = null; CachedGeometryRenderBounds = default; _cachedStrokeWidth = default; } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index cfd6fc12f8..079eea7bef 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Avalonia.Platform; using SkiaSharp; -#nullable enable namespace Avalonia.Skia { @@ -10,7 +9,7 @@ namespace Avalonia.Skia { public GlyphRunImpl(SKTextBlob textBlob, Size size, Point baselineOrigin) { - TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); + TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); Size = size; diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index a8dd289a13..3093455bec 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -140,7 +140,7 @@ namespace Avalonia.Skia return Font.GetHorizontalGlyphAdvances(glyphIndices); } - private Blob GetTable(Face face, Tag tag) + private Blob? GetTable(Face face, Tag tag) { var size = Typeface.GetTableSize(tag); @@ -166,8 +166,8 @@ namespace Avalonia.Skia return; } - Font?.Dispose(); - Face?.Dispose(); + Font.Dispose(); + Face.Dispose(); } public void Dispose() diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index a5782037f3..e6e30a1203 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -15,14 +15,14 @@ namespace Avalonia.Skia /// /// Surfaces. /// Created render target or if it fails. - ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); + ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces); /// /// Creates an offscreen render target surface /// /// size in pixels. /// An optional custom render session. - ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session); + ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session); } public interface ISkiaSurface : IDisposable diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index e19379df09..4a3031d9ad 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -3,6 +3,7 @@ using Avalonia.OpenGL; using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; + namespace Avalonia.Skia { internal class FboSkiaSurface : ISkiaSurface @@ -14,6 +15,7 @@ namespace Avalonia.Skia private int _fbo; private int _depthStencil; private int _texture; + private SKSurface? _surface; private static readonly bool[] TrueFalse = new[] { true, false }; public FboSkiaSurface(GlSkiaGpu gpu, GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) @@ -89,7 +91,7 @@ namespace Avalonia.Skia var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 8, new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); - Surface = SKSurface.Create(_grContext, target, + _surface = SKSurface.Create(_grContext, target, surfaceOrigin, SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); CanBlit = gl.IsBlitFramebufferAvailable; } @@ -100,8 +102,8 @@ namespace Avalonia.Skia { using (_glContext.EnsureCurrent()) { - Surface?.Dispose(); - Surface = null; + _surface?.Dispose(); + _surface = null; var gl = _glContext.GlInterface; if (_fbo != 0) { @@ -113,11 +115,11 @@ namespace Avalonia.Skia } catch (PlatformGraphicsContextLostException) { - if (Surface != null) + if (_surface != null) // We need to dispose SKSurface _after_ GRContext.Abandon was called, // otherwise it will try to do OpenGL calls without a proper context - _gpu.AddPostDispose(Surface.Dispose); - Surface = null; + _gpu.AddPostDispose(_surface.Dispose); + _surface = null; } finally { @@ -125,8 +127,10 @@ namespace Avalonia.Skia } } - public SKSurface Surface { get; private set; } + public SKSurface Surface => _surface ?? throw new ObjectDisposedException(nameof(FboSkiaSurface)); + public bool CanBlit { get; } + public void Blit(SKCanvas canvas) { // This should set the render target as the current FBO diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs index 4bf43634ef..2b6caf34dc 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using Avalonia.OpenGL; @@ -150,6 +149,11 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) { + if (_image is null) + { + throw new NotSupportedException("Only supported with an external image"); + } + using (_gpu.EnsureCurrent()) { _image.AcquireKeyedMutex(acquireIndex); @@ -167,6 +171,11 @@ internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, IPlatformRenderInterfaceImportedSemaphore signalSemaphore) { + if (_image is null) + { + throw new NotSupportedException("Only supported with an external image"); + } + var wait = (GlSkiaImportedSemaphore)waitForSemaphore; var signal = (GlSkiaImportedSemaphore)signalSemaphore; using (_gpu.EnsureCurrent()) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index bf3e950e81..d403855094 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -10,15 +10,15 @@ using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + internal class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { - private GRContext _grContext; - private IGlContext _glContext; + private readonly GRContext _grContext; + private readonly IGlContext _glContext; public GRContext GrContext => _grContext; public IGlContext GlContext => _glContext; - private List _postDisposeCallbacks = new(); + private readonly List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; - private IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; + private readonly IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { @@ -41,7 +41,7 @@ namespace Avalonia.Skia } } - class SurfaceWrapper : IGlPlatformSurface + private class SurfaceWrapper : IGlPlatformSurface { private readonly object _surface; @@ -57,7 +57,7 @@ namespace Avalonia.Skia } } - public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { var customRenderTargetFactory = _glContext.TryGetFeature(); foreach (var surface in surfaces) @@ -75,7 +75,7 @@ namespace Avalonia.Skia return null; } - public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession? session) { // Only windows platform needs our FBO trickery if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -106,7 +106,7 @@ namespace Avalonia.Skia public bool CanCreateSharedContext => _glContext.CanCreateSharedContext; - public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + public IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null) => _glContext.CreateSharedContext(preferredVersions); public ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size) @@ -153,7 +153,7 @@ namespace Avalonia.Skia public bool IsLost => _glContext.IsLost; public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) return this; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6b4a7a3409..797c565ca1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var session = _renderTarget.BeginRenderingSession(); @@ -31,7 +31,6 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, Gpu = _skiaGpu, CurrentSession = session diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index ec33770356..ec24f8f624 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -15,13 +15,12 @@ namespace Avalonia.Skia.Helpers /// /// /// DrawingContext - public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer visualBrushRenderer = null) + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi) { var createInfo = new DrawingContextImpl.CreateInfo { Canvas = canvas, Dpi = dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, }; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index 4cb1430a3b..6adfc01951 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -60,5 +60,15 @@ namespace Avalonia.Skia.Helpers } } } + + // This method is here mostly for debugging purposes + internal static void SavePicture(SKPicture picture, float scale, string path) + { + var snapshotSize = new SKSizeI((int)Math.Ceiling(picture.CullRect.Width * scale), + (int)Math.Ceiling(picture.CullRect.Height * scale)); + using var snap = + SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale)); + SaveImage(snap, path); + } } } diff --git a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs index b3966c0324..66abd818e6 100644 --- a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs +++ b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs @@ -14,7 +14,7 @@ public interface ISkiaSharpApiLeaseFeature public interface ISkiaSharpApiLease : IDisposable { SKCanvas SkCanvas { get; } - GRContext GrContext { get; } - SKSurface SkSurface { get; } + GRContext? GrContext { get; } + SKSurface? SkSurface { get; } double CurrentOpacity { get; } -} \ No newline at end of file +} diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index 4ab873fd8d..0627407509 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -100,7 +100,7 @@ namespace Avalonia.Skia _bitmap = scaledBmp; } - _bitmap!.SetImmutable(); + _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); @@ -134,7 +134,7 @@ namespace Avalonia.Skia data); _bitmap = tmp.Copy(); } - _bitmap!.SetImmutable(); + _bitmap.SetImmutable(); _image = SKImage.FromBitmap(_bitmap); if (_image == null) @@ -179,10 +179,13 @@ namespace Avalonia.Skia public PixelFormat? Format => _bitmap?.ColorType.ToAvalonia(); public ILockedFramebuffer Lock() { - if (_bitmap == null) - throw new NotSupportedException(); - return new LockedFramebuffer(_bitmap.GetPixels(), PixelSize, _bitmap.RowBytes, Dpi, - _bitmap.ColorType.ToAvalonia().Value, null); + if (_bitmap is null) + throw new NotSupportedException("A bitmap is needed for locking"); + + if (_bitmap.ColorType.ToAvalonia() is not { } format) + throw new NotSupportedException($"Unsupported format {_bitmap.ColorType}"); + + return new LockedFramebuffer(_bitmap.GetPixels(), PixelSize, _bitmap.RowBytes, Dpi, format, null); } } } diff --git a/src/Skia/Avalonia.Skia/PictureRenderTarget.cs b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs new file mode 100644 index 0000000000..280b7c27cd --- /dev/null +++ b/src/Skia/Avalonia.Skia/PictureRenderTarget.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Platform; +using Avalonia.Reactive; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class PictureRenderTarget : IDisposable +{ + private readonly ISkiaGpu? _gpu; + private readonly GRContext? _grContext; + private readonly Vector _dpi; + private SKPicture? _picture; + + public PictureRenderTarget(ISkiaGpu? gpu, GRContext? grContext, Vector dpi) + { + _gpu = gpu; + _grContext = grContext; + _dpi = dpi; + } + + public SKPicture GetPicture() + { + var rv = _picture ?? throw new InvalidOperationException(); + _picture = null; + return rv; + } + + public IDrawingContextImpl CreateDrawingContext(Size size) + { + var recorder = new SKPictureRecorder(); + var canvas = recorder.BeginRecording(new SKRect(0, 0, (float)(size.Width * _dpi.X / 96), + (float)(size.Height * _dpi.Y / 96))); + + canvas.RestoreToCount(-1); + canvas.ResetMatrix(); + + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = _dpi, + DisableTextLcdRendering = true, + GrContext = _grContext, + Gpu = _gpu, + }; + return new DrawingContextImpl(createInfo, Disposable.Create(() => + { + _picture = recorder.EndRecording(); + canvas.Dispose(); + recorder.Dispose(); + })); + } + + public void Dispose() => _picture?.Dispose(); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 8e9a19239b..ab1c6b8816 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -24,7 +24,7 @@ namespace Avalonia.Skia } - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) { if (graphicsContext == null) return new SkiaContext(null); @@ -225,7 +225,7 @@ namespace Avalonia.Skia throw new ArgumentNullException(nameof(glyphInfos)); } - var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; + var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; var font = SKFontCache.Shared.Get(); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 73f58e66bc..9ee17a09d6 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using SkiaSharp; @@ -6,20 +7,19 @@ namespace Avalonia.Skia { internal class SKTypefaceCollection { - private readonly ConcurrentDictionary _typefaces = - new ConcurrentDictionary(); + private readonly ConcurrentDictionary _typefaces = new(); public void AddTypeface(Typeface key, SKTypeface typeface) { _typefaces.TryAdd(key, typeface); } - public SKTypeface Get(Typeface typeface) + public SKTypeface? Get(Typeface typeface) { return GetNearestMatch(typeface); } - private SKTypeface GetNearestMatch(Typeface key) + private SKTypeface? GetNearestMatch(Typeface key) { if (_typefaces.Count == 0) { @@ -70,7 +70,7 @@ namespace Avalonia.Skia return typeface; } - SKTypeface skTypeface = null; + SKTypeface? skTypeface = null; foreach(var pair in _typefaces) { @@ -85,7 +85,7 @@ namespace Avalonia.Skia return skTypeface; } - private bool TryFindStretchFallback(Typeface key, out SKTypeface typeface) + private bool TryFindStretchFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) { typeface = null; var stretch = (int)key.Stretch; @@ -114,7 +114,7 @@ namespace Avalonia.Skia return false; } - private bool TryFindWeightFallback(Typeface key, out SKTypeface typeface) + private bool TryFindWeightFallback(Typeface key, [NotNullWhen(true)] out SKTypeface? typeface) { typeface = null; var weight = (int)key.Weight; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index b49efd59cd..d064f49ae4 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -23,7 +23,7 @@ namespace Avalonia.Skia /// public static SKTypefaceCollection GetOrAddTypefaceCollection(FontFamily fontFamily) { - return s_cachedCollections.GetOrAdd(fontFamily, x => CreateCustomFontCollection(fontFamily)); + return s_cachedCollections.GetOrAdd(fontFamily, CreateCustomFontCollection); } /// @@ -33,10 +33,15 @@ namespace Avalonia.Skia /// private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFamily) { - var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key); - var typeFaceCollection = new SKTypefaceCollection(); + if (fontFamily.Key is not { } fontFamilyKey) + { + return typeFaceCollection; + } + + var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamilyKey); + var assetLoader = AvaloniaLocator.Current.GetRequiredService(); foreach (var asset in fontAssets) diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 0cf66767cb..51e182f7e3 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -9,9 +9,9 @@ namespace Avalonia.Skia; internal class SkiaContext : IPlatformRenderInterfaceContext { - private ISkiaGpu _gpu; + private ISkiaGpu? _gpu; - public SkiaContext(ISkiaGpu gpu) + public SkiaContext(ISkiaGpu? gpu) { _gpu = gpu; } @@ -25,10 +25,10 @@ internal class SkiaContext : IPlatformRenderInterfaceContext /// public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { - if (!(surfaces is IList)) + if (surfaces is not IList) surfaces = surfaces.ToList(); - var gpuRenderTarget = _gpu?.TryCreateRenderTarget(surfaces); - if (gpuRenderTarget != null) + + if (_gpu?.TryCreateRenderTarget(surfaces) is { } gpuRenderTarget) { return new SkiaGpuRenderTarget(_gpu, gpuRenderTarget); } @@ -43,7 +43,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } - public bool IsLost => _gpu.IsLost; + public bool IsLost => _gpu?.IsLost ?? false; - public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); + public object? TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); } diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index b3c3056a58..84ad547d6c 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,5 +16,13 @@ namespace Avalonia /// Setting this to null will give you the default Skia value. /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. + + /// + /// Use Skia's SaveLayer API to handling opacity. + /// + /// + /// Enabling this might have performance implications. + /// + public bool UseOpacitySaveLayer { get; set; } = false; } } diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 20dde27e9a..c66b53284a 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; @@ -111,7 +112,7 @@ namespace Avalonia.Skia return sm; } - public static SKColor ToSKColor(this Media.Color c) + public static SKColor ToSKColor(this Color c) { return new SKColor(c.R, c.G, c.B, c.A); } @@ -171,14 +172,14 @@ namespace Avalonia.Skia }; } - public static SKShaderTileMode ToSKShaderTileMode(this Media.GradientSpreadMethod m) + public static SKShaderTileMode ToSKShaderTileMode(this GradientSpreadMethod m) { switch (m) { default: - case Media.GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp; - case Media.GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror; - case Media.GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat; + case GradientSpreadMethod.Pad: return SKShaderTileMode.Clamp; + case GradientSpreadMethod.Reflect: return SKShaderTileMode.Mirror; + case GradientSpreadMethod.Repeat: return SKShaderTileMode.Repeat; } } @@ -215,7 +216,8 @@ namespace Avalonia.Skia }; } - public static SKPath Clone(this SKPath src) + [return: NotNullIfNotNull(nameof(src))] + public static SKPath? Clone(this SKPath? src) { return src != null ? new SKPath(src) : null; } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index df847d2224..0c3289767e 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath?.Clone(), Bounds); + return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); } /// diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index e5fb182a3b..92210c30e2 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,7 +1,6 @@ using System; using System.IO; using Avalonia.Reactive; -using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Skia.Helpers; @@ -17,24 +16,26 @@ namespace Avalonia.Skia private readonly ISkiaSurface _surface; private readonly SKCanvas _canvas; private readonly bool _disableLcdRendering; - private readonly GRContext _grContext; - private readonly ISkiaGpu _gpu; + private readonly GRContext? _grContext; + private readonly ISkiaGpu? _gpu; - class SkiaSurfaceWrapper : ISkiaSurface + private class SkiaSurfaceWrapper : ISkiaSurface { - public SKSurface Surface { get; private set; } + private SKSurface? _surface; + + public SKSurface Surface => _surface ?? throw new ObjectDisposedException(nameof(SkiaSurfaceWrapper)); public bool CanBlit => false; public void Blit(SKCanvas canvas) => throw new NotSupportedException(); public SkiaSurfaceWrapper(SKSurface surface) { - Surface = surface; + _surface = surface; } public void Dispose() { - Surface?.Dispose(); - Surface = null; + _surface?.Dispose(); + _surface = null; } } @@ -51,18 +52,25 @@ namespace Avalonia.Skia _grContext = createInfo.GrContext; _gpu = createInfo.Gpu; - if (!createInfo.DisableManualFbo) - _surface = _gpu?.TryCreateSurface(PixelSize, createInfo.Session); - if (_surface == null) - _surface = new SkiaSurfaceWrapper(CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, - createInfo.Format)); + ISkiaSurface? surface = null; - _canvas = _surface?.Surface.Canvas; + if (!createInfo.DisableManualFbo) + surface = _gpu?.TryCreateSurface(PixelSize, createInfo.Session); - if (_surface == null || _canvas == null) + if (surface is null) { - throw new InvalidOperationException("Failed to create Skia render target surface"); + if (CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, createInfo.Format) + is { } skSurface) + { + surface = new SkiaSurfaceWrapper(skSurface); + } } + + if (surface?.Surface.Canvas is not { } canvas) + throw new InvalidOperationException("Failed to create Skia render target surface"); + + _surface = surface; + _canvas = canvas; } /// @@ -73,7 +81,7 @@ namespace Avalonia.Skia /// Height. /// Format. /// - private static SKSurface CreateSurface(GRContext gpu, int width, int height, PixelFormat? format) + private static SKSurface? CreateSurface(GRContext? gpu, int width, int height, PixelFormat? format) { var imageInfo = MakeImageInfo(width, height, format); if (gpu != null) @@ -89,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -98,7 +106,6 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, @@ -218,11 +225,11 @@ namespace Avalonia.Skia /// /// GPU-accelerated context (optional) /// - public GRContext GrContext; + public GRContext? GrContext; - public ISkiaGpu Gpu; + public ISkiaGpu? Gpu; - public ISkiaGpuRenderSession Session; + public ISkiaGpuRenderSession? Session; public bool DisableManualFbo; } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index a21038839c..e7dd4fb6da 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia var usedCulture = culture ?? CultureInfo.CurrentCulture; - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, i => new Language(usedCulture)); + buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); var font = ((GlyphTypefaceImpl)typeface).Font; @@ -170,7 +170,7 @@ namespace Avalonia.Skia return segment.Array.AsMemory(); } - if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager memoryManager, out start, out length)) + if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager? memoryManager, out start, out length)) { return memoryManager.Memory; } diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index 64d5b58970..fb3c2e403f 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -19,14 +19,14 @@ namespace Avalonia.Skia Transform = transform; var transformedPath = source.EffectivePath.Clone(); - transformedPath.Transform(transform.ToSKMatrix()); + transformedPath?.Transform(transform.ToSKMatrix()); EffectivePath = transformedPath; - Bounds = transformedPath.TightBounds.ToAvaloniaRect(); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; } /// - public override SKPath EffectivePath { get; } + public override SKPath? EffectivePath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index 56e627f2d8..8ea7434c23 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -15,7 +15,7 @@ namespace Avalonia.Skia { private static readonly SKBitmapReleaseDelegate s_releaseDelegate = ReleaseProc; private readonly SKBitmap _bitmap; - private readonly object _lock = new object(); + private readonly object _lock = new(); /// /// Create a WriteableBitmap from given stream. @@ -205,8 +205,8 @@ namespace Avalonia.Skia _bitmap.NotifyPixelsChanged(); _parent.Version++; Monitor.Exit(_parent._lock); - _bitmap = null; - _parent = null; + _bitmap = null!; + _parent = null!; } /// diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 02932c52da..10f9239b1a 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => + return new DrawingContextImpl( null, target, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 984a24fb30..0af326d6a8 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1 { } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var locked = _surface.Lock(); if (locked.Format == PixelFormat.Rgb565) @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(visualBrushRenderer); + .CreateDrawingContext(); } public bool IsCorrupted => false; @@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1 _target = target; } - public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public override IDrawingContextImpl CreateDrawingContext() { - return base.CreateDrawingContext(visualBrushRenderer, () => + return base.CreateDrawingContext(() => { using (var l = WicImpl.Lock(BitmapLockFlags.Read)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 3506abc63b..f9b5953e3f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media /// internal class DrawingContextImpl : IDrawingContextImpl { - private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; @@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( - IVisualBrushRenderer visualBrushRenderer, ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { - _visualBrushRenderer = visualBrushRenderer; _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; @@ -441,14 +438,15 @@ namespace Avalonia.Direct2D1.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { if (opacity < 1) { var parameters = new LayerParameters { - ContentBounds = PrimitiveExtensions.RectangleInfinite, + ContentBounds = bounds.ToDirect2D(), MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = (float)opacity, }; @@ -490,7 +488,8 @@ namespace Avalonia.Direct2D1.Media var radialGradientBrush = brush as IRadialGradientBrush; var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; - var visualBrush = brush as IVisualBrush; + var sceneBrush = brush as ISceneBrush; + var sceneBrushContent = brush as ISceneBrushContent; if (solidColorBrush != null) { @@ -517,11 +516,13 @@ namespace Avalonia.Direct2D1.Media (BitmapImpl)imageBrush.Source.PlatformImpl.Item, destinationSize); } - else if (visualBrush != null) + else if (sceneBrush != null || sceneBrushContent != null) { - if (_visualBrushRenderer != null) + sceneBrushContent ??= sceneBrush.CreateContent(); + if (sceneBrushContent != null) { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + var rect = sceneBrushContent.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { @@ -532,28 +533,26 @@ namespace Avalonia.Direct2D1.Media var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); using (var intermediate = new BitmapRenderTarget( - _deviceContext, - CompatibleRenderTargetOptions.None, - pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) + _deviceContext, + CompatibleRenderTargetOptions.None, + pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + sceneBrushContent.Render(ctx, + rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } return new ImageBrushImpl( - visualBrush, + sceneBrushContent.Brush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), destinationSize); } + } } - else - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } } return new SolidColorBrushImpl(null, _deviceContext); diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 829b887d9d..a08c96c40c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, calc.IntermediateSize.ToSharpDX()); - using (var context = new RenderTarget(result).CreateDrawingContext(null)) + using (var context = new RenderTarget(result).CreateDrawingContext()) { var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2dbc1d67d1..6b1ca911fb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); + return new DrawingContextImpl( this, _renderTarget, null, () => Version++); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index d6b1e618e5..fa40e75fa7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - => CreateDrawingContext(visualBrushRenderer, null); + public virtual IDrawingContextImpl CreateDrawingContext() + => CreateDrawingContext(null); public bool IsCorrupted => false; - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) + public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) { - return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => + return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => { Version++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 8d5062336c..4392e35058 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); + return new DrawingContextImpl(this, _renderTarget); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 531c4119af..385120505c 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var size = GetWindowSize(); var dpi = GetWindowDpi(); @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 Resize(); } - return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); + return new DrawingContextImpl(this, _deviceContext, _swapChain); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 48ebd4068e..3eeedc4b5d 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -41,6 +41,8 @@ namespace Avalonia.Win32.Automation { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple }, { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired }, { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection }, + { SelectionItemPatternIdentifiers.IsSelectedProperty, UiaPropertyId.SelectionItemIsSelected }, + { SelectionItemPatternIdentifiers.SelectionContainerProperty, UiaPropertyId.SelectionItemSelectionContainer } }; private static ConditionalWeakTable s_nodes = new(); diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 5fc4f21b2e..00fdeb2a1d 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -29,7 +29,10 @@ namespace Avalonia.Win32 private static readonly List s_formatList = new() { new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning disable CS0618 // Type or member is obsolete new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning restore CS0618 // Type or member is obsolete }; diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 272300cbf3..a215a0a322 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; using Avalonia.MicroCom; +using Avalonia.Platform.Storage; using Avalonia.Win32.Interop; using FORMATETC = Avalonia.Win32.Interop.FORMATETC; @@ -124,16 +125,6 @@ namespace Avalonia.Win32 return _wrapped.GetDataFormats(); } - IEnumerable? IDataObject.GetFileNames() - { - return _wrapped.GetFileNames(); - } - - string? IDataObject.GetText() - { - return _wrapped.GetText(); - } - object? IDataObject.Get(string dataFormat) { return _wrapped.Get(dataFormat); @@ -260,8 +251,12 @@ namespace Avalonia.Win32 object data = _wrapped.Get(dataFormat)!; if (dataFormat == DataFormats.Text || data is string) return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete if (dataFormat == DataFormats.FileNames && data is IEnumerable files) return WriteFileListToHGlobal(ref hGlobal, files); +#pragma warning restore CS0618 // Type or member is obsolete + if (dataFormat == DataFormats.Files && data is IEnumerable items) + return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!); if (data is Stream stream) { var length = (int)(stream.Length - stream.Position); diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 247d0340c3..824303b7fa 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; +using Avalonia.Platform.Storage.FileIO; using Avalonia.Utilities; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -34,16 +35,6 @@ namespace Avalonia.Win32 return GetDataFormatsCore().Distinct(); } - public string? GetText() - { - return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); - } - - public IEnumerable? GetFileNames() - { - return (IEnumerable?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); - } - public object? Get(string dataFormat) { return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); @@ -67,8 +58,15 @@ namespace Avalonia.Win32 { if (format == DataFormats.Text) return ReadStringFromHGlobal(medium.unionmember); +#pragma warning disable CS0618 if (format == DataFormats.FileNames) +#pragma warning restore CS0618 return ReadFileNamesFromHGlobal(medium.unionmember); + if (format == DataFormats.Files) + return ReadFileNamesFromHGlobal(medium.unionmember) + .Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + .Where(f => f is not null); byte[] data = ReadBytesFromHGlobal(medium.unionmember); diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index e5c2fc6cf6..2d9331b5dc 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 30da940514..7e63987d1b 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -7,11 +7,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj new file mode 100644 index 0000000000..31b8d08541 --- /dev/null +++ b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs new file mode 100644 index 0000000000..0a27602604 --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -0,0 +1,795 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Avalonia.Analyzers; + +public partial class AvaloniaPropertyAnalyzer +{ + public class CompileAnalyzer + { + /// + /// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them. + /// + private ImmutableDictionary _avaloniaPropertyDescriptions = null!; + + /// + /// Maps properties onto all AvaloniaProperty objects that they may be intended to represent. + /// + private ImmutableDictionary> _clrPropertyToAvaloniaProperties = null!; + + private readonly INamedTypeSymbol _stringType; + private readonly INamedTypeSymbol _avaloniaObjectType; + private readonly ImmutableHashSet _getValueMethods; + private readonly ImmutableHashSet _setValueMethods; + private readonly ImmutableHashSet _allGetSetMethods; + private readonly INamedTypeSymbol _avaloniaPropertyType; + private readonly INamedTypeSymbol _styledPropertyType; + private readonly INamedTypeSymbol _attachedPropertyType; + private readonly INamedTypeSymbol _directPropertyType; + private readonly INamedTypeSymbol? _userControlType; + private readonly INamedTypeSymbol? _topLevelType; + private readonly ImmutableHashSet _allAvaloniaPropertyTypes; + private readonly ImmutableDictionary _propertyValueTypeParams; + private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; + private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; + private readonly ImmutableHashSet _allAvaloniaPropertyMethods; + private readonly ImmutableDictionary _ownerTypeParams; + private readonly ImmutableDictionary _valueTypeParams; + private readonly ImmutableDictionary _hostTypeParams; + private readonly ImmutableDictionary _inheritsParams; + private readonly ImmutableDictionary _ownerParams; + + public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) + { + var methodComparer = SymbolEqualityComparer.Default; + + _stringType = GetTypeOrThrow("System.String"); + _avaloniaObjectType = avaloniaObjectType; + _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(methodComparer); + _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(methodComparer); + _allGetSetMethods = _getValueMethods.Concat(_setValueMethods).ToImmutableHashSet(methodComparer); + + _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); + _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); + _attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1"); + _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2"); + + _userControlType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl"); + _topLevelType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.TopLevel"); + + _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() + .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer); + + _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableHashSet(SymbolEqualityComparer.Default); + + _propertyValueTypeParams = _allAvaloniaPropertyTypes.Select(p => p.TypeParameters.First(t => t.Name == "TValue")) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => p.ContainingType, SymbolEqualityComparer.Default); + + _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes + .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); + + _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(methodComparer); + + _ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters); + _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters); + _hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters); + _inheritsParams = GetParamDictionary("inherits", m => m.Parameters); + _ownerParams = GetParamDictionary("ownerType", m => m.Parameters); + + RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); + + context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); + context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); + context.RegisterOperationAction(AnalyzePropertyStorageAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzePropertyWrapperAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); + + context.RegisterSymbolAction(AnalyzeWrapperCrlProperty, SymbolKind.Property); + + if (context.Compilation.Language == LanguageNames.CSharp) + { + context.RegisterCodeBlockAction(AnalyzePropertyMethods); + } + + INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); + + ImmutableDictionary GetParamDictionary(string name, Func> methodSymbolSelector) where TSymbol : ISymbol => _allAvaloniaPropertyMethods + .Select(m => methodSymbolSelector(m).SingleOrDefault(p => p.Name == name)) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => (IMethodSymbol)p.ContainingSymbol, SymbolEqualityComparer.Default); + } + + private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + private bool IsAvaloniaPropertyStorage(IPropertySymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + + private void RegisterAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) + { + var namespaceStack = new Stack(); + namespaceStack.Push(compilation.GlobalNamespace); + + var types = new List(); + + while (namespaceStack.Count > 0) + { + var current = namespaceStack.Pop(); + + types.AddRange(current.GetTypeMembers()); + + foreach (var child in current.GetNamespaceMembers()) + { + namespaceStack.Push(child); + } + } + + var avaloniaPropertyStorageSymbols = new ConcurrentBag(); + + var propertyDescriptions = new ConcurrentDictionary(SymbolEqualityComparer.Default); + + // key initializes value + var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); + + var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; + + var semanticModels = new ConcurrentDictionary(); + + Parallel.ForEach(types, parallelOptions, type => + { + try + { + foreach (var member in type.GetMembers()) + { + switch (member) + { + case IFieldSymbol fieldSymbol when IsAvaloniaPropertyStorage(fieldSymbol): + avaloniaPropertyStorageSymbols.Add(fieldSymbol); + break; + case IPropertySymbol propertySymbol when IsAvaloniaPropertyStorage(propertySymbol): + avaloniaPropertyStorageSymbols.Add(propertySymbol); + break; + } + } + + foreach (var constructor in type.StaticConstructors) + { + foreach (var syntaxRef in constructor.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) + { + var (node, model) = GetNodeAndModel(syntaxRef); + + foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression))) + { + var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; + + if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target) + { + RegisterAssignment(target, assignmentOperation.Value); + } + } + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new AvaloniaAnalysisException($"Failed to find AvaloniaProperty objects in {type}.", ex); + } + }); + + Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol => + { + foreach (var syntaxRef in symbol.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) + { + var (node, model) = GetNodeAndModel(syntaxRef); + + var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault(); + + if (operation == null) + { + return; + } + + RegisterAssignment(symbol, operation.Value); + } + }); + + // we have recorded every Register and AddOwner call. Now follow assignment chains. + Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root => + { + var propertyDescription = propertyDescriptions[root]; + var owner = propertyDescription.AssignedTo[root]; + + var current = root; + do + { + var target = fieldInitializations[current]; + + propertyDescription.SetAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type + propertyDescriptions[target] = propertyDescription; + + fieldInitializations.TryGetValue(target, out current); + } + while (current != null); + }); + + var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); + + var propertyDescriptionsByName = propertyDescriptions.Values.ToLookup(p => p.Name, p => (property: p, owners: p.OwnerTypes.Select(t => t.Type).ToImmutableHashSet(SymbolEqualityComparer.Default))); + + // Detect CLR properties that provide syntatic wrapping around an AvaloniaProperty (or potentially multiple, which leads to a warning diagnostic) + Parallel.ForEach(propertyDescriptions.Values, propertyDescription => + { + var nameMatches = propertyDescriptionsByName[propertyDescription.Name]; + + foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default)) + { + if (ownerType.GetMembers(propertyDescription.Name).OfType().SingleOrDefault() is not { IsStatic: false } clrProperty) + { + continue; + } + + propertyDescription.AddPropertyWrapper(clrProperty); + clrPropertyWrapCandidates.Add((clrProperty, propertyDescription)); + + var current = ownerType.BaseType; + while (current != null) + { + foreach (var otherProp in nameMatches.Where(t => t.owners.Contains(current)).Select(t => t.property)) + { + clrPropertyWrapCandidates.Add((clrProperty, otherProp)); + } + + current = current.BaseType; + } + } + }); + + // convert our dictionaries to immutable form + _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer.Default) + .ToImmutableDictionary(g => g.Key, g => g.Distinct().ToImmutableArray(), SymbolEqualityComparer.Default); + _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); + + void RegisterAssignment(ISymbol target, IOperation value) + { + switch (ResolveOperationSource(value)) + { + case IInvocationOperation invocation: + RegisterInitializer_Invocation(invocation, target, propertyDescriptions); + break; + case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field): + fieldInitializations[fieldRef.Field] = target; + break; + case IPropertyReferenceOperation propRef when IsAvaloniaPropertyStorage(propRef.Property): + fieldInitializations[propRef.Property] = target; + break; + } + } + + (SyntaxNode, SemanticModel) GetNodeAndModel(SyntaxReference syntaxRef) => + (syntaxRef.GetSyntax(cancellationToken), semanticModels.GetOrAdd(syntaxRef.SyntaxTree, st => compilation.GetSemanticModel(st))); + } + + // This method handles registration of a new AvaloniaProperty, and calls to AddOwner. + private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions) + { + try + { + if (invocation.TargetMethod.ReturnType is not INamedTypeSymbol propertyType) + { + return; + } + + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_avaloniaPropertyRegisterMethods.Contains(originalMethod)) // This is a call to one of the AvaloniaProperty.Register* methods + { + TypeReference ownerTypeRef; + + if (_ownerTypeParams.TryGetValue(originalMethod, out var ownerTypeParam)) + { + ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); + } + else if (_ownerParams.TryGetValue(originalMethod, out var ownerParam) && // try extracting the runtime argument + ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value) is ITypeOfOperation { Type: ITypeSymbol type } typeOf) + { + ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); + } + else + { + return; + } + + TypeReference valueTypeRef; + if (_valueTypeParams.TryGetValue(originalMethod, out var valueTypeParam)) + { + valueTypeRef = TypeReference.FromInvocationTypeParameter(invocation, valueTypeParam); + } + else + { + return; + } + + string name; + switch (ResolveOperationSource(invocation.Arguments[0].Value)) + { + case ILiteralOperation literal when SymbolEquals(literal.Type, _stringType): + name = (string)literal.ConstantValue.Value!; + break; + case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference: + name = propertyReference.Property.Name; + break; + case IFieldReferenceOperation fieldRef when SymbolEquals(fieldRef.Type, _stringType) && fieldRef.ConstantValue is { HasValue: true } constantValue: + name = (string)fieldRef.ConstantValue.Value!; + break; + default: + return; + } + + var inherits = false; + if (_inheritsParams.TryGetValue(originalMethod, out var inheritsParam) && + invocation.Arguments[inheritsParam.Ordinal].Value is ILiteralOperation literalOp && + literalOp.ConstantValue.Value is bool constValue) + { + inherits = constValue; + } + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + if (_hostTypeParams.TryGetValue(originalMethod, out var hostTypeParam)) + { + hostTypeRef = TypeReference.FromInvocationTypeParameter(invocation, hostTypeParam); + } + else + { + hostTypeRef = new(_avaloniaObjectType, Location.None); + } + } + + var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, valueTypeRef.Type)); + description.Name = name; + description.HostType = hostTypeRef; + description.Inherits = inherits; + description.SetAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); + } + else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods + { + if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam)) + { + return; + } + + if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol) + { + return; + } + + var description = propertyDescriptions[target] = propertyDescriptions.GetOrAdd(sourceSymbol, s => + { + string inferredName = s.Name; + + var match = Regex.Match(s.Name, "(?.*)Property$"); + if (match.Success) + { + inferredName = match.Groups["name"].Value; + } + + if (!_propertyValueTypeParams.TryGetValue(propertyType.OriginalDefinition, out var propertyValueType)) + { + throw new InvalidOperationException($"{propertyType} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType})."); + } + + var valueType = propertyType.TypeArguments[propertyValueType.Ordinal]; + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration + } + + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + + // assume that the property is owned by its containing type at the point of assignment, until we find its registration + result.SetAssignment(s, new(s.ContainingType, Location.None)); + + return result; + }); + + var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); + description.SetAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to register the initializer of '{target}'.", ex); + } + } + + /// + private void AnalyzeFieldInitializer(OperationAnalysisContext context) + { + var operation = (IFieldInitializerOperation)context.Operation; + + foreach (var field in operation.InitializedFields) + { + try + { + if (!_avaloniaPropertyDescriptions.TryGetValue(field, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(field)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, field.Locations[0], field)); + } + + AnalyzeInitializer_Shared(context, field, description); + + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of field '{field}'.", ex); + } + } + } + + /// + private void AnalyzePropertyInitializer(OperationAnalysisContext context) + { + var operation = (IPropertyInitializerOperation)context.Operation; + + foreach (var property in operation.InitializedProperties) + { + try + { + if (!_avaloniaPropertyDescriptions.TryGetValue(property, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(property)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, property.Locations[0], property)); + } + + AnalyzeInitializer_Shared(context, property, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of property '{property}'.", ex); + } + } + } + + /// + private void AnalyzePropertyStorageAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + try + { + var (target, isValid) = ResolveOperationSource(operation.Target) switch + { + IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), + IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), + _ => (default(ISymbol), false), + }; + + if (target == null || !_avaloniaPropertyDescriptions.TryGetValue(target, out var description)) + { + return; + } + + if (!isValid) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, target.Locations[0], target)); + } + + AnalyzeInitializer_Shared(context, target, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process assignment '{operation}'.", ex); + } + } + + /// + /// + private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description) + { + if (!assignmentSymbol.Name.Contains(description.Name) && assignmentSymbol.DeclaredAccessibility != Accessibility.Private) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0], + description.Name, assignmentSymbol)); + } + + try + { + var ownerType = description.AssignedTo[assignmentSymbol]; + + if (ownerType.Type.TypeKind != TypeKind.Error && + !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && + !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) + { + context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type)); + } + } + catch (KeyNotFoundException) + { + throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded."); + } + } + + /// + private void AnalyzePropertyWrapperAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + if (ResolveOperationSource(operation) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) + { + // We can consider `new MyType(myValue)` functionally equivalent to `new MyType() { Value = myValue }`. Both set a local value with an external parameter. + return; + } + + if (ResolveOperationTarget(operation) is IPropertyReferenceOperation propertyRef && + propertyRef.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + _clrPropertyToAvaloniaProperties.TryGetValue(propertyRef.Property, out var propertyDescriptions) && + propertyDescriptions.Any(p => !SymbolEquals(p.PropertyType.OriginalDefinition, _directPropertyType))) + { + if (DerivesFrom(propertyRef.Instance.Type, _userControlType) || DerivesFrom(propertyRef.Instance.Type, _topLevelType)) + { + // Special case: don't warn about local value assignment on a UserControl or TopLevel type. + // 1. We don't want to annoy new users, who start with these two types and don't understand binding priorities yet + // 2. Such controls either have no consumers, or are treated largely as a black box (i.e. no styles setting dynamic values) + return; + } + + context.ReportDiagnostic(Diagnostic.Create(SettingOwnStyledPropertyValue, operation.Syntax.GetLocation())); + } + } + + /// + /// + /// + private void AnalyzeMethodInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_allGetSetMethods.Contains(originalMethod)) + { + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + GetReferencedProperty(invocation.Arguments[0]) is { } refProp && + refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && + !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && + !DerivesFrom(context.ContainingSymbol.ContainingType, refProp.description.HostType?.Type)) + { + context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(), + refProp.storageSymbol, context.ContainingSymbol.ContainingType)); + } + } + else if (_allAvaloniaPropertyMethods.Contains(originalMethod)) + { + if (!IsStaticConstructorOrInitializer()) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(), + originalMethod.ToDisplayString(TypeQualifiedName))); + } + + if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) && + invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is { } newOwnerType) + { + if (newOwnerType is INamedTypeSymbol { IsGenericType: true }) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); + } + + if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!) is { } refProp) + { + var ownerMatches = refProp.description.AssignedTo.Where(kvp => !SymbolEquals(kvp.Key, context.ContainingSymbol) && DerivesFrom(newOwnerType, kvp.Value.Type)).ToArray(); + + if (ownerMatches.Any()) + { + var ownerMatchesExceptBaseTypes = ownerMatches.Where(m => !DerivesFrom(context.ContainingSymbol.ContainingType, m.Key.ContainingType, includeSelf: false)).ToArray(); + var routesMessage = ownerMatchesExceptBaseTypes.Length switch + { + 0 => "its base type", + 1 => ownerMatchesExceptBaseTypes.Single().Key.ToString(), + _ => $"{ownerMatches.Length} routes\n\t{string.Join("\n\t", ownerMatches.Select(kvp => kvp.Key))}" + }; + + context.ReportDiagnostic(Diagnostic.Create(SuperfluousAddOwnerCall, invocation.Syntax.GetLocation(), ownerMatches.Select(kvp => kvp.Value.Location), + newOwnerType, refProp.storageSymbol, routesMessage)); + } + } + } + } + + bool IsStaticConstructorOrInitializer() => + context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.StaticConstructor } || + ResolveOperationTarget(invocation.Parent!) switch + { + IFieldInitializerOperation fieldInit when fieldInit.InitializedFields.All(f => f.IsStatic) => true, + IPropertyInitializerOperation propInit when propInit.InitializedProperties.All(p => p.IsStatic) => true, + _ => false, + }; + } + + private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation) + { + if (GetReferencedFieldOrProperty(operation) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result)) + { + return (result, storageSymbol); + } + else + { + return null; + } + } + + /// + /// + /// + /// + /// + private void AnalyzeWrapperCrlProperty(SymbolAnalysisContext context) + { + var property = (IPropertySymbol)context.Symbol; + + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) + { + return; // does not refer to an AvaloniaProperty + } + + try + { + if (candidateTargetProperties.Length > 1) + { + var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null).OrderBy(s => s!.Name); + context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations), + property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}")); + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties[0]; + var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType); + + if (avaloniaPropertyStorage == null) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] }, + avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage)); + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true)) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0], + avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}")); + } + + if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage)); + } + + VerifyAccessor(property.GetMethod, "readable", "get"); + + if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + VerifyAccessor(property.SetMethod, "writeable", "set"); + } + + void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) + { + if (method == null) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } + else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); + } + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to analyse wrapper property '{property}'.", ex); + } + } + + /// + private void AnalyzePropertyMethods(CodeBlockAnalysisContext context) + { + if (context.OwningSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property } method) + { + return; + } + + try + { + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) || + candidateTargetProperties.Length != 1) // a diagnostic about multiple candidates will have already been reported + { + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties.Single(); + + if (IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + return; + } + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType)) + { + return; // a diagnostic about this will have already been reported, and if the cast is implicit then this message would be confusing anyway + } + + var bodyNode = context.CodeBlock.ChildNodes().Single(); + + var operation = bodyNode.DescendantNodes() + .Where(n => n.IsKind(SyntaxKind.InvocationExpression)) // this line is specific to C# + .Select(n => (IInvocationOperation)context.SemanticModel.GetOperation(n)!) + .FirstOrDefault(); + + var isGetMethod = method.MethodKind == MethodKind.PropertyGet; + + var expectedInvocations = isGetMethod ? _getValueMethods : _setValueMethods; + + if (operation == null || bodyNode.ChildNodes().Count() != 1 || !expectedInvocations.Contains(operation.TargetMethod.OriginalDefinition)) + { + ReportSideEffects(); + return; + } + + if (operation.Arguments.Length != 0) + { + switch (ResolveOperationSource(operation.Arguments[0].Value)) + { + case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field): + case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property): + break; // the argument is a reference to the correct AvaloniaProperty object + default: + ReportSideEffects(operation.Arguments[0].Value.Syntax.GetLocation()); + return; + } + } + + if (!isGetMethod && + operation.Arguments.Length >= 2 && + operation.Arguments[1].Value.Kind != OperationKind.ParameterReference) // passing something other than `value` to SetValue + { + ReportSideEffects(operation.Arguments[1].Syntax.GetLocation()); + } + + void ReportSideEffects(Location? locationOverride = null) + { + var propertySourceName = avaloniaPropertyDescription.ClosestAssignmentFor(method.ContainingType)?.Name ?? "[unknown]"; + + context.ReportDiagnostic(Diagnostic.Create(AccessorSideEffects, locationOverride ?? context.CodeBlock.GetLocation(), + avaloniaPropertyDescription.Name, + isGetMethod ? "read" : "written to", + isGetMethod ? "get" : "set", + isGetMethod ? $"GetValue({propertySourceName})" : $"SetValue({propertySourceName}, value)")); + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex); + } + } + } +} diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs new file mode 100644 index 0000000000..d1d9071d17 --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Avalonia.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] +public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer +{ + private const string Category = "AvaloniaProperty"; + + private const string TypeMismatchTag = "TypeMismatch"; + private const string NameCollisionTag = "NameCollision"; + private const string AssociatedClrPropertyTag = "AssociatedClrProperty"; + private const string InappropriateReadWriteTag = "InappropriateReadWrite"; + + private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( + "AVP0001", + "Identification of the AvaloniaProperty associated with a CLR property", + "Associated AvaloniaProperty: {0} {1}", + Category, + DiagnosticSeverity.Info, + isEnabledByDefault: false, + "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( + "AVP1000", + "AvaloniaProperty objects should be stored appropriately", + "Incorrect AvaloniaProperty storage: {0} should be static and readonly", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be stored accordingly."); + + private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new( + "AVP1001", + "The same AvaloniaProperty should not be registered twice", + "Unsafe registration: {0} should be called only in static constructors or static initializers", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be created only once. To ensure this, only call Register or AddOwner in static constructors or static initializers."); + + private static readonly DiagnosticDescriptor PropertyOwnedByGenericType = new( + "AVP1002", + "AvaloniaProperty objects should not be owned by a generic type", + "Inadvisable registration: Generic types cannot be referenced from XAML. Create a non-generic type to be the owner of this AvaloniaProperty.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is sometimes necessary to refer to an AvaloniaProperty in XAML by providing its class name. This cannot be achieved if property's owner is a generic type." + + " Additionally, a new AvaloniaProperty object will be generated each time a new version of the generic owner type is constructed, which may be unexpected."); + + private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( + "AVP1010", + "AvaloniaProperty objects should be owned by the type in which they are stored", + "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", + TypeMismatchTag); + + private static readonly DiagnosticDescriptor UnexpectedPropertyAccess = new( + "AVP1011", + "An AvaloniaObject should own each AvaloniaProperty it reads or writes on itself", + "Unexpected property use: {0} is neither owned by nor attached to {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor SettingOwnStyledPropertyValue = new( + "AVP1012", + "An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", + "Inappropriate assignment: An AvaloniaObject should use SetCurrentValue when setting its own StyledProperty or AttachedProperty values", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The standard means of setting an AvaloniaProperty is to call the SetValue method (often via a CLR property setter). This will forcibly overwrite values from sources like styles and templates, " + + "which is something that should only be done by consumers of the control, not the control itself. Controls which want to set their own values should instead call the SetCurrentValue method, or " + + "refactor the property into a DirectProperty. An assignment is exempt from this diagnostic in two scenarios: when it is forwarding a constructor parameter, and when the target object is derived " + + "from UserControl or TopLevel.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new( + "AVP1013", + "AvaloniaProperty owners should not be added superfluously", + "Superfluous owner: {0} is already an owner of {1} via {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Ownership of an AvaloniaProperty is inherited along the type hierarchy. There is no need for a derived type to assert ownership over a base type's properties. This diagnostic can be a symptom of an incorrect property owner elsewhere.", + InappropriateReadWriteTag); + + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( + "AVP1020", + "AvaloniaProperty names should be unique within each class", + "Name collision: {0} has the same name as {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Querying for an AvaloniaProperty by name requires that each property associated with a type have a unique name.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AmbiguousPropertyName = new( + "AVP1021", + "There should be an unambiguous relationship between the CLR properties and Avalonia properties of a class", + "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its OverrideMetadata or OverrideDefaultValue methods.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor PropertyNameMismatch = new( + "AVP1022", + "An AvaloniaProperty object should be stored in a field or CLR property which reflects its name", + "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".\nPrivate symbols are exempt from this diagnostic.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AccessorSideEffects = new( + "AVP1030", + "StyledProperty accessors should not have side effects", + "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, consider: 1) adding a Coercion method, b) adding a static observer with AvaloniaProperty.Changed.AddClassHandler, and/or c) overriding the AvaloniaObject.OnPropertyChanged method.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor MissingAccessor = new( + "AVP1031", + "A CLR property should support the same get/set operations as its associated AvaloniaProperty", + "Missing accessor: {0} is {1}, but this CLR property lacks a {2} accessor", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor InconsistentAccessibility = new( + "AVP1032", + "A CLR property and its accessors should be equally accessible as its associated AvaloniaProperty", + "Inconsistent accessibility: CLR {0} accessibility does not match accessibility of {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different accessibility from its associated AvaloniaProperty is ineffective.", + AssociatedClrPropertyTag); + + private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( + "AVP1040", + "A CLR property type should match the associated AvaloniaProperty type", + "Type mismatch: CLR property type differs from the value type of {0} {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", + TypeMismatchTag, AssociatedClrPropertyTag); + + private static readonly SymbolDisplayFormat TypeQualifiedName = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + AssociatedAvaloniaProperty, + InappropriatePropertyAssignment, + InappropriatePropertyRegistration, + PropertyOwnedByGenericType, + OwnerDoesNotMatchOuterType, + UnexpectedPropertyAccess, + SettingOwnStyledPropertyValue, + SuperfluousAddOwnerCall, + DuplicatePropertyName, + AmbiguousPropertyName, + PropertyNameMismatch, + AccessorSideEffects, + MissingAccessor, + InconsistentAccessibility, + PropertyTypeMismatch); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName("Avalonia.AvaloniaObject") is { } avaloniaObjectType) + { + new CompileAnalyzer(c, avaloniaObjectType); + } + }); + } + + private static bool IsAvaloniaPropertyType(ITypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); + + private static bool IsAvaloniaPropertyType(ITypeSymbol type, IEnumerable propertyTypes) + { + type = type.OriginalDefinition; + + return propertyTypes.Any(t => SymbolEquals(type, t)); + } + + private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType, bool includeSelf = true) + { + if (baseType != null) + { + if (!includeSelf) + { + type = type?.BaseType; + } + + while (type != null) + { + if (SymbolEquals(type, baseType)) + { + return true; + } + + type = type.BaseType; + } + } + return false; + } + + /// + /// Follows assignments and conversions back to their source. + /// + private static IOperation ResolveOperationSource(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Operand; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Value; + break; + default: + return operation; + } + } + } + + private static IOperation ResolveOperationTarget(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Parent!; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Target; + break; + default: + return operation; + } + } + } + + private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation) => operation == null ? null : ResolveOperationSource(operation) switch + { + IFieldReferenceOperation fieldRef => fieldRef.Field, + IPropertyReferenceOperation propertyRef => propertyRef.Property, + IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value), + _ => null, + }; + + private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; + private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; + + private static bool SymbolEquals(ISymbol? x, ISymbol? y, bool includeNullability = false) + { + // The current version of Microsoft.CodeAnalysis includes an "IncludeNullability" comparer, + // but it overshoots the target and tries to compare EVERYTHING. This leads to two symbols for + // the same type not being equal if they were imported into different compile units (i.e. assemblies). + // So for now, we will just discard this parameter. + _ = includeNullability; + + return SymbolEqualityComparer.Default.Equals(x, y); + } + + private class AvaloniaPropertyDescription + { + /// + /// Gets the name that was assigned to this property when it was registered. + /// + /// + /// If the property was not registered within the current compile context, this value will be inferred from + /// the name of the field (or CLR property) in which the AvaloniaProperty object is stored. + /// + public string Name { get; set; } + + /// + /// Gets the type of the AvaloniaProperty itself: Styled, Direct, or Attached + /// + public INamedTypeSymbol PropertyType { get; } + + /// + /// Gets the TValue type that the property stores. + /// + public ITypeSymbol ValueType { get; } + + /// + /// Gets whether the value of this property is inherited from the parent AvaloniaObject. + /// + public bool Inherits { get; set; } + + /// + /// Gets the type which registered the property, and all types which have added themselves as owners. + /// + public IReadOnlyCollection OwnerTypes { get; private set; } + private ConcurrentBag? _ownerTypes = new(); + + /// + /// Gets the type to which an AttachedProperty is attached, or null if the property is StyledProperty or DirectProperty. + /// + public TypeReference? HostType { get; set; } + + /// + /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. + /// + public IReadOnlyDictionary AssignedTo { get; private set; } + private ConcurrentDictionary? _assignedTo = new(SymbolEqualityComparer.Default); + + /// + /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject. + /// + public IReadOnlyCollection PropertyWrappers { get; private set; } + private ConcurrentBag? _propertyWrappers = new(); + + public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, ITypeSymbol valueType) + { + Name = name; + PropertyType = propertyType; + ValueType = valueType; + + OwnerTypes = _ownerTypes; + PropertyWrappers = _propertyWrappers; + AssignedTo = _assignedTo; + } + + private const string SealedError = "PropertyDescription has been sealed."; + + public void AddOwner(TypeReference owner) => (_ownerTypes ?? throw new InvalidOperationException(SealedError)).Add(owner); + + public void AddPropertyWrapper(IPropertySymbol property) => (_propertyWrappers ?? throw new InvalidOperationException(SealedError)).Add(property); + + public void SetAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError))[assignmentTarget] = ownerType; + + public AvaloniaPropertyDescription Seal() + { + if (_ownerTypes == null || _propertyWrappers == null || _assignedTo == null) + { + return this; + } + + OwnerTypes = _ownerTypes.ToImmutableHashSet(); + _ownerTypes = null; + + PropertyWrappers = _propertyWrappers.ToImmutableHashSet(SymbolEqualityComparer.Default); + _propertyWrappers = null; + + AssignedTo = new ReadOnlyDictionary(_assignedTo); + _assignedTo = null; + + return this; + } + + /// + /// Searches the inheritance hierarchy of the given type for a field or property to which this AvaloniaProperty is assigned. + /// + public ISymbol? ClosestAssignmentFor(ITypeSymbol? type) + { + var assignmentsByType = AssignedTo.Keys.ToLookup(s => s.ContainingType, SymbolEqualityComparer.Default); + + while (type != null) + { + if (assignmentsByType.Contains(type)) + { + return assignmentsByType[type].First(); + } + type = type.BaseType; + } + + return null; + } + } + + private readonly struct TypeReference + { + public ITypeSymbol Type { get; } + public Location Location { get; } + + public TypeReference(ITypeSymbol type, Location location) + { + Type = type; + Location = location; + } + + public static TypeReference FromInvocationTypeParameter(IInvocationOperation invocation, ITypeParameterSymbol typeParameter) + { + var argument = invocation.TargetMethod.TypeArguments[typeParameter.Ordinal]; + + var typeArgumentSyntax = invocation.Syntax; + if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them + { + try + { + typeArgumentSyntax = invocation.Syntax.DescendantNodes() + .First(n => n.IsKind(SyntaxKind.TypeArgumentList)) + .DescendantNodes().ElementAt(typeParameter.Ordinal); + } + catch + { + // ignore, this is just a nicety + } + } + + return new TypeReference(argument, typeArgumentSyntax.GetLocation()); + } + } + + private class SymbolEqualityComparer : IEqualityComparer where T : ISymbol + { + public bool Equals(T x, T y) => SymbolEqualityComparer.Default.Equals(x, y); + public int GetHashCode(T obj) => SymbolEqualityComparer.Default.GetHashCode(obj); + + public static SymbolEqualityComparer Default { get; } = new(); + } +} + +[Serializable] +public class AvaloniaAnalysisException : Exception +{ + public AvaloniaAnalysisException(string message, Exception? innerException = null) : base(message, innerException) + { + } + + protected AvaloniaAnalysisException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index b6036bba8f..9f74d2fc08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -888,7 +888,8 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); var called = false; - var expectedMessageTemplate = "Error in binding to {Target}.{Property}: expected {ExpectedType}, got {Value} ({ValueType})"; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; + var message = "Unable to convert object 'foo' of type 'System.String' to type 'System.Double'."; LogCallback checkLogMessage = (level, area, src, mt, pv) => { @@ -898,9 +899,7 @@ namespace Avalonia.Base.UnitTests src == target && pv[0].GetType() == typeof(Class1) && (AvaloniaProperty)pv[1] == Class1.QuxProperty && - (Type)pv[2] == typeof(double) && - (string)pv[3] == "foo" && - (Type)pv[4] == typeof(string)) + (string)pv[2] == message) { called = true; } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index d48e58136a..12cd39046b 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -1,115 +1,212 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.UnitTests; using Xunit; +#nullable enable + namespace Avalonia.Base.UnitTests { public class AvaloniaObjectTests_DataValidation { - [Fact] - public void Binding_Non_Validated_Styled_Property_Does_Not_Call_UpdateDataValidation() + public abstract class TestBase + where T : AvaloniaProperty { - var target = new Class1(); - var source = new Subject>(); + [Fact] + public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetNonValidatedProperty(); - target.Bind(Class1.NonValidatedProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.BindingError(new Exception())); + source.OnNext(BindingValue.DataValidationError(new Exception())); + source.OnNext(6); - Assert.Empty(target.Notifications); - } + Assert.Empty(target.Notifications); + } - [Fact] - public void Binding_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() - { - var target = new Class1(); - var source = new Subject>(); + [Fact] + public void Binding_Validated_Property_Calls_UpdateDataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error1 = new Exception(); + var error2 = new Exception(); - target.Bind(Class1.NonValidatedDirectProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(6); + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error1)); + source.OnNext(BindingValue.BindingError(error2)); + source.OnNext(7); - Assert.Empty(target.Notifications); - } + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error1), + new(BindingValueType.BindingError, 0, error2), + new(BindingValueType.Value, 7, null), + }, target.Notifications); + } - [Fact] - public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() - { - var target = new Class1(); - var source = new Subject>(); - - target.Bind(Class1.ValidatedDirectIntProperty, source); - source.OnNext(6); - source.OnNext(BindingValue.BindingError(new Exception())); - source.OnNext(BindingValue.DataValidationError(new Exception())); - source.OnNext(7); - - var result = target.Notifications; - Assert.Equal(4, result.Count); - Assert.Equal(BindingValueType.Value, result[0].type); - Assert.Equal(6, result[0].value); - Assert.Equal(BindingValueType.BindingError, result[1].type); - Assert.Equal(BindingValueType.DataValidationError, result[2].type); - Assert.Equal(BindingValueType.Value, result[3].type); - Assert.Equal(7, result[3].value); + [Fact] + public void Binding_Validated_Property_Calls_UpdateDataValidation_Untyped() + { + var target = new Class1(); + var source = new Subject(); + var property = GetProperty(); + var error1 = new Exception(); + var error2 = new Exception(); + + target.Bind(property, source); + source.OnNext(6); + source.OnNext(new BindingNotification(error1, BindingErrorType.DataValidationError)); + source.OnNext(new BindingNotification(error2, BindingErrorType.Error)); + source.OnNext(7); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error1), + new(BindingValueType.BindingError, 0, error2), + new(BindingValueType.Value, 7, null), + }, target.Notifications); + } + + [Fact] + public void Binding_Overridden_Validated_Property_Calls_UpdateDataValidation() + { + var target = new Class2(); + var source = new Subject>(); + var property = GetNonValidatedProperty(); + + // Class2 overrides the non-validated property metadata to enable data validation. + target.Bind(property, source); + source.OnNext(1); + + Assert.Equal(1, target.Notifications.Count); + } + + [Fact] + public void Disposing_Binding_Subscription_Clears_DataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error = new Exception(); + var sub = target.Bind(property, source); + + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error)); + sub.Dispose(); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error), + new(BindingValueType.UnsetValue, 6, null), + }, target.Notifications); + } + + [Fact] + public void Completing_Binding_Clears_DataValidation() + { + var target = new Class1(); + var source = new Subject>(); + var property = GetProperty(); + var error = new Exception(); + + target.Bind(property, source); + source.OnNext(6); + source.OnNext(BindingValue.DataValidationError(error)); + source.OnCompleted(); + + Assert.Equal(new Notification[] + { + new(BindingValueType.Value, 6, null), + new(BindingValueType.DataValidationError, 6, error), + new(BindingValueType.UnsetValue, 6, null), + }, target.Notifications); + } + + protected abstract T GetProperty(); + protected abstract T GetNonValidatedProperty(); } - [Fact] - public void Binding_Overridden_Validated_Direct_Property_Calls_UpdateDataValidation() + public class DirectPropertyTests : TestBase> { - var target = new Class2(); - var source = new Subject>(); + [Fact] + public void Bound_Validated_String_Property_Can_Be_Set_To_Null() + { + var source = new ViewModel + { + StringValue = "foo", + }; - // Class2 overrides `NonValidatedDirectProperty`'s metadata to enable data validation. - target.Bind(Class1.NonValidatedDirectProperty, source); - source.OnNext(1); + var target = new Class1 + { + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; + + Assert.Equal("foo", target.ValidatedDirectString); - Assert.Equal(1, target.Notifications.Count); + source.StringValue = null; + + Assert.Null(target.ValidatedDirectString); + } + + protected override DirectPropertyBase GetProperty() => Class1.ValidatedDirectIntProperty; + protected override DirectPropertyBase GetNonValidatedProperty() => Class1.NonValidatedDirectIntProperty; } - [Fact] - public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + public class StyledPropertyTests : TestBase> { - var source = new ViewModel + [Fact] + public void Bound_Validated_String_Property_Can_Be_Set_To_Null() { - StringValue = "foo", - }; + var source = new ViewModel + { + StringValue = "foo", + }; - var target = new Class1 - { - [!Class1.ValidatedDirectStringProperty] = new Binding + var target = new Class1 { - Path = nameof(ViewModel.StringValue), - Source = source, - }, - }; + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; - Assert.Equal("foo", target.ValidatedDirectString); + Assert.Equal("foo", target.ValidatedDirectString); - source.StringValue = null; + source.StringValue = null; - Assert.Null(target.ValidatedDirectString); + Assert.Null(target.ValidatedDirectString); + } + + protected override StyledProperty GetProperty() => Class1.ValidatedStyledIntProperty; + protected override StyledProperty GetNonValidatedProperty() => Class1.NonValidatedStyledIntProperty; } + private record class Notification(BindingValueType type, object? value, Exception? error); + private class Class1 : AvaloniaObject { - public static readonly StyledProperty NonValidatedProperty = - AvaloniaProperty.Register( - nameof(NonValidated)); - - public static readonly DirectProperty NonValidatedDirectProperty = + public static readonly DirectProperty NonValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(NonValidatedDirect), - o => o.NonValidatedDirect, - (o, v) => o.NonValidatedDirect = v); + nameof(NonValidatedDirectInt), + o => o.NonValidatedDirectInt, + (o, v) => o.NonValidatedDirectInt = v); public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( @@ -118,27 +215,30 @@ namespace Avalonia.Base.UnitTests (o, v) => o.ValidatedDirectInt = v, enableDataValidation: true); - public static readonly DirectProperty ValidatedDirectStringProperty = - AvaloniaProperty.RegisterDirect( + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( nameof(ValidatedDirectString), o => o.ValidatedDirectString, (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); + public static readonly StyledProperty NonValidatedStyledIntProperty = + AvaloniaProperty.Register( + nameof(NonValidatedStyledInt)); + + public static readonly StyledProperty ValidatedStyledIntProperty = + AvaloniaProperty.Register( + nameof(ValidatedStyledInt), + enableDataValidation: true); + private int _nonValidatedDirect; private int _directInt; - private string _directString; + private string? _directString; - public int NonValidated - { - get { return GetValue(NonValidatedProperty); } - set { SetValue(NonValidatedProperty, value); } - } - - public int NonValidatedDirect + public int NonValidatedDirectInt { get { return _directInt; } - set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } + set { SetAndRaise(NonValidatedDirectIntProperty, ref _nonValidatedDirect, value); } } public int ValidatedDirectInt @@ -147,20 +247,32 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); } } - public string ValidatedDirectString + public string? ValidatedDirectString { get { return _directString; } set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } - public List<(BindingValueType type, object value)> Notifications { get; } = new(); + public int NonValidatedStyledInt + { + get { return GetValue(NonValidatedStyledIntProperty); } + set { SetValue(NonValidatedStyledIntProperty, value); } + } + + public int ValidatedStyledInt + { + get => GetValue(ValidatedStyledIntProperty); + set => SetValue(ValidatedStyledIntProperty, value); + } + + public List Notifications { get; } = new(); protected override void UpdateDataValidation( AvaloniaProperty property, BindingValueType state, - Exception error) + Exception? error) { - Notifications.Add((state, GetValue(property))); + Notifications.Add(new(state, GetValue(property), error)); } } @@ -168,16 +280,18 @@ namespace Avalonia.Base.UnitTests { static Class2() { - NonValidatedDirectProperty.OverrideMetadata( + NonValidatedDirectIntProperty.OverrideMetadata( new DirectPropertyMetadata(enableDataValidation: true)); + NonValidatedStyledIntProperty.OverrideMetadata( + new StyledPropertyMetadata(enableDataValidation: true)); } } public class ViewModel : NotifyingBase { - private string _stringValue; + private string? _stringValue; - public string StringValue + public string? StringValue { get { return _stringValue; } set { _stringValue = value; RaisePropertyChanged(); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs index 7e932373c2..181596a681 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs @@ -198,6 +198,7 @@ namespace Avalonia.Base.UnitTests defaultBindingMode: BindingMode.OneWay, validate: null, coerce: null, + enableDataValidation: false, notifying: FooNotifying); public int NotifyCount { get; private set; } diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs index 3d7dc66cc4..1bb1b4af73 100644 --- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs @@ -1,13 +1,63 @@ using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Media; +using Avalonia.Platform; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests.Input { public class MouseDeviceTests : PointerTestsBase { + [Fact] + public void Initial_Buttons_Are_Not_Set_Without_Corresponding_Mouse_Down() + { + using var scope = AvaloniaLocator.EnterScope(); + var settingsMock = new Mock(); + var threadingMock = new Mock(); + + threadingMock.Setup(x => x.CurrentThreadIsLoopThread).Returns(true); + + AvaloniaLocator.CurrentMutable.BindToSelf(this) + .Bind().ToConstant(settingsMock.Object); + + using var app = UnitTestApplication.Start( + new TestServices( + inputManager: new InputManager(), + threadingInterface: threadingMock.Object)); + + var renderer = RendererMocks.CreateRenderer(); + var device = new MouseDevice(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var control = new Control(); + var root = CreateInputRoot(impl.Object, control); + + MouseButton button = default; + + root.PointerReleased += (s, e) => button = e.InitialPressMouseButton; + + var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown); + var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp); + + SetHit(renderer, control); + + impl.Object.Input!(up); + + Assert.Equal(MouseButton.None, button); + + impl.Object.Input!(down); + impl.Object.Input!(up); + + Assert.Equal(MouseButton.Left, button); + + impl.Object.Input!(up); + + Assert.Equal(MouseButton.None, button); + } + [Fact] public void Capture_Is_Transferred_To_Parent_When_Control_Removed() { @@ -37,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Input impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); Assert.NotNull(result); - + result.Capture(control); Assert.Same(control, result.Captured); @@ -67,8 +117,8 @@ namespace Avalonia.Base.UnitTests.Input }) } }); - - + + Point? result = null; root.PointerMoved += (_, a) => { diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs index 2d45c699f1..5915343764 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs @@ -55,20 +55,29 @@ public abstract class PointerTestsBase return root; } + protected static RawPointerEventArgs CreateRawPointerArgs( + IPointerDevice pointerDevice, + IInputRoot root, + RawPointerEventType type, + Point? position = default) + { + return new RawPointerEventArgs(pointerDevice, 0, root, type, position ?? default, default); + } + protected static RawPointerEventArgs CreateRawPointerMovedArgs( IPointerDevice pointerDevice, IInputRoot root, - Point? positition = null) + Point? position = null) { return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move, - positition ?? default, default); + position ?? default, default); } protected static PointerEventArgs CreatePointerMovedArgs( - IInputRoot root, IInputElement? source, Point? positition = null) + IInputRoot root, IInputElement? source, Point? position = null) { return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock().Object, (Visual)root, - positition ?? default, default, PointerPointProperties.None, KeyModifiers.None); + position ?? default, default, PointerPointProperties.None, KeyModifiers.None); } protected static Mock CreatePointerDeviceMock( diff --git a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs index d75bf9fe8c..f91b4b613c 100644 --- a/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs +++ b/tests/Avalonia.Base.UnitTests/RenderTests_Culling.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests private DrawingContext CreateDrawingContext() { - return new DrawingContext(Mock.Of()); + return new PlatformDrawingContext(Mock.Of()); } private class TestControl : Control diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index c1468a28e4..9d810fa110 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph new Matrix(), Brushes.Black, null, - geometry, default); + geometry); geometryNode.HitTest(new Point()); } @@ -77,7 +77,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph private class TestRectangleDrawOperation : RectangleNode { public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) - : base(transform, pen.Brush, pen, bounds, new BoxShadows()) + : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows()) { } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs index 565b217180..a2e438e3e0 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void FillOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100)); var point = new Point(x, y); @@ -37,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void StrokeOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100), null); + var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100)); var point = new Point(x, y); diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index b5a9b35134..60603937d9 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -104,7 +104,7 @@ public class StyledElementTests_Theming target.Theme = null; Assert.Equal("style", target.Tag); } - + [Fact] public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() { @@ -539,12 +539,42 @@ public class StyledElementTests_Theming Assert.Same(target.Theme, theme3); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_To_Children() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); } - private static TestRoot CreateRoot(Control child) + private static TestRoot CreateRoot(Control child, ControlTheme? theme = null) { var result = new TestRoot() { @@ -552,7 +582,7 @@ public class StyledElementTests_Theming { new Style(x => x.OfType()) { - Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) } + Setters = { new Setter(StyledElement.ThemeProperty, theme ?? CreateTheme()) } } } }; @@ -580,8 +610,8 @@ public class StyledElementTests_Theming { new Style(x => x.Nesting().Template().OfType()) { - Setters = - { + Setters = + { new Setter(Border.BackgroundProperty, Brushes.Red), new Setter(Control.TagProperty, tag), } diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index e83b2d7598..40d504a0ac 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -65,7 +65,7 @@ namespace Avalonia.Benchmarks { } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { } diff --git a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs index b0db806afa..2905b1e464 100644 --- a/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs +++ b/tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs @@ -21,7 +21,7 @@ namespace Avalonia.Benchmarks.Rendering _lineFill = new Line { Fill = new SolidColorBrush() }; _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() }; - _drawingContext = new DrawingContext(new NullDrawingContextImpl(), true); + _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true); AvaloniaLocator.CurrentMutable.Bind().ToConstant(new NullRenderingPlatform()); } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 15ff6e68e3..e5c96dcab6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,9 +11,6 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { - static PointerEventArgs CreateArgs(RoutedEvent ev, object source) - => new PointerEventArgs(ev, source, new FakePointer(), (Visual)source, default, 0, PointerPointProperties.None, default); - static PointerPressedEventArgs CreatePressed(object source) => new PointerPressedEventArgs(source, new FakePointer(), (Visual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); @@ -171,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, isSubMenuOpen: true, parent: menu.Object); var nextItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -191,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -206,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -365,7 +362,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); @@ -381,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); item.Verify(x => x.Open(), Times.Never); @@ -401,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(hasSubMenu: true, isSubMenuOpen: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); parentItem.SetupGet(x => x.SubItems).Returns(new[] { item.Object, sibling.Object }); @@ -421,7 +418,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -438,7 +435,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(sibling.Object); target.PointerExited(item, e); @@ -454,7 +451,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); item.Setup(x => x.IsPointerOverSubMenu).Returns(true); target.PointerExited(item, e); @@ -488,8 +485,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); var childItem = CreateMockMenuItem(parent: item.Object); - var enter = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); - var leave = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var enter = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var leave = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); // Pointer enters item; item is selected. target.PointerEntered(item, enter); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4804b29fee..bc1225e0e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -18,6 +18,7 @@ using Avalonia.Input; using Avalonia.Rendering; using System.Threading.Tasks; using Avalonia.Threading; +using Avalonia.Interactivity; namespace Avalonia.Controls.UnitTests.Primitives { @@ -1048,6 +1049,30 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Events_Should_Be_Routed_To_Popup_Parent() + { + using (CreateServices()) + { + var popupContent = new Border(); + var popup = new Popup { Child = popupContent }; + var popupParent = new Border { Child = popup }; + var root = PreparedWindow(popupParent); + var raised = 0; + + root.LayoutManager.ExecuteInitialLayoutPass(); + popup.Open(); + root.LayoutManager.ExecuteLayoutPass(); + + var ev = new RoutedEventArgs(Button.ClickEvent); + + popupParent.AddHandler(Button.ClickEvent, (s, e) => ++raised); + popupContent.RaiseEvent(ev); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index c3d35653cc..d3eb42f147 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -237,6 +237,40 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Reducing_Extent_Should_Constrain_Offset() + { + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + }; + var root = new TestRoot(target); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(50, 50); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(-30, -30), e.ExtentDelta); + Assert.Equal(new Vector(-30, -30), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); + + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(1, raised); + Assert.Equal(new Vector(20, 20), target.Offset); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index f1dd874c71..ba8e7242a1 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -4,11 +4,14 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -278,6 +281,134 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [Fact] + public void Removing_Range_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 80); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 95); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + + [Fact] + public void NthChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + + [Fact] + public void NthLastChild_Selector_Works() + { + using var app = App(); + + var style = new Style(x => x.OfType().NthLastChild(5, 0)) + { + Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) }, + }; + + var (target, _, _) = CreateTarget(styles: new[] { style }); + var realized = target.GetRealizedContainers()!.Cast().ToList(); + + Assert.Equal(10, realized.Count); + + for (var i = 0; i < 10; ++i) + { + var container = realized[i]; + var index = target.IndexFromContainer(container); + var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null; + + Assert.Equal(i, index); + Assert.Equal(expectedBackground, container.Background); + } + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -322,7 +453,8 @@ namespace Avalonia.Controls.UnitTests private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget( IEnumerable? items = null, - bool useItemTemplate = true) + bool useItemTemplate = true, + IEnumerable