From 9de82b19393ff5ff4910637d99d15fb4d3f75294 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 19 Jan 2023 23:26:03 +0100 Subject: [PATCH 01/36] Make nth-last-child style change background. Will hopefully make it a bit more obvious if it breaks in future: previously it was easier to miss as both `nth-child` and `nth-last-child` changed the foreground. --- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ From 649e4fc57e45f4f8c357e61b7b66fd818eef9557 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jan 2023 10:57:40 +0100 Subject: [PATCH 02/36] Add failing test for #9997. And a passing test for `nth-child` selector. --- .../VirtualizingStackPanelTests.cs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index f1dd874c71..469f832b3a 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -9,6 +9,8 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -278,6 +280,58 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [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 +376,8 @@ namespace Avalonia.Controls.UnitTests private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget( IEnumerable? items = null, - bool useItemTemplate = true) + bool useItemTemplate = true, + IEnumerable + From 0c20abec120cd9a0039342c88737c4b0085d6e13 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 1 Feb 2023 02:27:27 -0500 Subject: [PATCH 12/36] Update src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 2197b0fe59..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -22,7 +22,7 @@ - + From 04df472194e194559d33619e089f3aa918e3f5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 9 Feb 2023 03:10:19 +0000 Subject: [PATCH 13/36] Added unit test for #10226. --- .../Styling/StyledElementTests_Theming.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) 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), } From b8a987e84748b3c26d37dbfa787e720c0a188aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 8 Feb 2023 20:29:25 +0000 Subject: [PATCH 14/36] Ensure templated parent control theme is applied. --- src/Avalonia.Base/Layout/Layoutable.cs | 6 ++++++ src/Avalonia.Base/StyledElement.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..c45fc16929 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -795,6 +795,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/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5bf022cd51..b910012444 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -390,7 +390,7 @@ namespace Avalonia /// public bool ApplyStyling() { - if (_initCount == 0 && (!_stylesApplied || !_themeApplied)) + if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); From 3ace482c198a56f558cb1369b5885508d2c839da Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:32:12 -0500 Subject: [PATCH 15/36] Fix wrong property names and documentation --- src/Avalonia.Base/Styling/ThemeVariant.cs | 2 +- src/Avalonia.Controls/Application.cs | 2 +- src/Avalonia.Controls/Control.cs | 1 + src/Avalonia.Controls/ThemeVariantScope.cs | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 8218533f4f..5a6a8785a5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -6,7 +6,7 @@ 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 diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 8456a9a3a9..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -95,7 +95,7 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// + /// public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 775fc8e243..c9e8f0d045 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -162,6 +162,7 @@ namespace Avalonia.Controls set => SetValue(TagProperty, value); } + /// public ThemeVariant ActualThemeVariant => GetValue(ThemeVariantScope.ActualThemeVariantProperty); public event EventHandler? ActualThemeVariantChanged; diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index 2aac1d3e2f..ff950d3985 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -8,11 +8,11 @@ namespace Avalonia.Controls public class ThemeVariantScope : Decorator { /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty ActualThemeVariantProperty = AvaloniaProperty.Register( - nameof(ThemeVariant), + nameof(ActualThemeVariant), inherits: true, defaultValue: ThemeVariant.Light); @@ -21,9 +21,9 @@ namespace Avalonia.Controls /// public static readonly StyledProperty RequestedThemeVariantProperty = AvaloniaProperty.Register( - nameof(ThemeVariant), + nameof(RequestedThemeVariant), defaultValue: ThemeVariant.Default); - + /// /// 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. From af70f74c88163b9e0d5446163685ec9f0b748e7d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 23:03:11 -0500 Subject: [PATCH 16/36] Workaround for the inherited default value bug --- src/Avalonia.Controls/ThemeVariantScope.cs | 3 +- .../Xaml/ThemeDictionariesTests.cs | 73 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index ff950d3985..b29f1ee56e 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -13,8 +13,7 @@ namespace Avalonia.Controls public static readonly StyledProperty ActualThemeVariantProperty = AvaloniaProperty.Register( nameof(ActualThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); + inherits: true); /// /// Defines the property. diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 20d76a3cc7..3ac4677694 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,8 +1,12 @@ -using Avalonia.Controls; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using Avalonia.Styling; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -444,4 +448,71 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } + + [Fact] + public void Theme_Switch_Works_In_Nested_Scope() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + window.ApplyTemplate(); + + var scope = window.FindControl("Scope")!; + var text = window.FindControl("Text")!; + + Assert.Equal(ThemeVariant.Dark, text.ActualThemeVariant); + Assert.Equal(Color.Parse("#dedede"), ((ISolidColorBrush)text.Foreground!).Color); + + scope.RequestedThemeVariant = ThemeVariant.Light; + Assert.Equal(ThemeVariant.Light, text.ActualThemeVariant); + Assert.Equal(Colors.Black, ((ISolidColorBrush)text.Foreground!).Color); + } + } + + [Fact] + public void Theme_Switch_Works_In_With_Popup() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + + +"); + window.Show(); + + var scope = window.FindControl("Scope")!; + var popup = window.FindControl("Popup")!; + + popup.IsOpen = true; + + var border = (Border)popup.Child!; + + Assert.Equal(ThemeVariant.Dark, popup.ActualThemeVariant); + Assert.Equal(ThemeVariant.Dark, border.ActualThemeVariant); + Assert.Equal(Color.Parse("#282828"), ((ISolidColorBrush)border.Background!).Color); + + scope.RequestedThemeVariant = ThemeVariant.Light; + + Assert.Equal(ThemeVariant.Light, popup.ActualThemeVariant); + Assert.Equal(ThemeVariant.Light, border.ActualThemeVariant); + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background!).Color); + } + } + } } From 9933fd04ce136e0376b78b57355556267b28493e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 23:47:55 -0500 Subject: [PATCH 17/36] Bind target theme variant to the popup root --- src/Avalonia.Controls/Primitives/Popup.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d6cd71aedc..142db71761 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, + placementTarget.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + .DisposeWith(handlerCleanup); + } + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, From 5d0d3ba5bbe791590215a88d8e05b83ab68f9ae4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 15 Feb 2023 00:05:03 -0500 Subject: [PATCH 18/36] Simplify simple themes of ported control --- .../Controls/SplitButton.xaml | 104 +++++++----------- .../Controls/SplitView.xaml | 6 +- .../Controls/ToggleSwitch.xaml | 91 +++++---------- 3 files changed, 73 insertions(+), 128 deletions(-) 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 @@ From 104023bfc88a9e7ee6e7033c4181494f302b6e61 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 00:27:21 -0500 Subject: [PATCH 19/36] Remove specific data type methods from the IDataObject, add new Files format --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 41 ++++++++------- .../ControlCatalog/Pages/DragAndDropPage.xaml | 3 +- .../Pages/DragAndDropPage.xaml.cs | 48 +++++++++++++++--- src/Avalonia.Base/Input/DataFormats.cs | 10 +++- src/Avalonia.Base/Input/DataObject.cs | 25 ++++------ .../Input/DataObjectExtensions.cs | 50 +++++++++++++++++++ src/Avalonia.Base/Input/IDataObject.cs | 17 ++----- .../Platform/Storage/FileIO/BclStorageFile.cs | 5 -- .../Storage/FileIO/BclStorageFolder.cs | 9 ---- .../Storage/FileIO/StorageProviderHelpers.cs | 17 +++++++ .../Platform/Storage/PickerOptions.cs | 2 + .../Storage/StorageProviderExtensions.cs | 12 +++++ src/Avalonia.Native/ClipboardImpl.cs | 41 +++++++++------ .../Avalonia.Win32/ClipboardFormats.cs | 3 ++ src/Windows/Avalonia.Win32/DataObject.cs | 15 ++---- src/Windows/Avalonia.Win32/OleDataObject.cs | 18 +++---- 16 files changed, 213 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Base/Input/DataObjectExtensions.cs 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/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..807c242914 --- /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 avaialble, 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 avaialble, 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 avaialble, 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..b6fcd8c7db 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 avaialble, returns null. + /// object? Get(string dataFormat); } } 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.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/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); From dcb73b9fefee2fa5a6509fe1764590a175a6aa36 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 00:27:40 -0500 Subject: [PATCH 20/36] Browser drag n drop target support --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 55 +++++++++++ .../Avalonia.Browser/BrowserDataObject.cs | 91 +++++++++++++++++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 9 ++ src/Browser/Avalonia.Browser/ClipboardImpl.cs | 2 +- .../Interop/AvaloniaModule.cs | 21 +++-- .../Interop/GeneralHelpers.cs | 22 +++++ .../Avalonia.Browser/Interop/InputHelper.cs | 5 +- .../Avalonia.Browser/Interop/StorageHelper.cs | 3 + .../Storage/BrowserStorageProvider.cs | 16 ++-- .../webapp/modules/avalonia.ts | 4 +- .../webapp/modules/avalonia/generalHelpers.ts | 19 ++++ .../webapp/modules/avalonia/input.ts | 22 +++++ .../webapp/modules/storage/storageItem.ts | 42 ++++++++- .../webapp/modules/storage/storageProvider.ts | 8 +- 14 files changed, 289 insertions(+), 30 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserDataObject.cs create mode 100644 src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs create mode 100644 src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts 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/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(); } From b360902d3fd461f048c973525c9abc015ae85432 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 01:33:57 -0500 Subject: [PATCH 21/36] Typo --- src/Avalonia.Base/Input/DataObjectExtensions.cs | 6 +++--- src/Avalonia.Base/Input/IDataObject.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs index 807c242914..6af531b0d8 100644 --- a/src/Avalonia.Base/Input/DataObjectExtensions.cs +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input /// . /// /// - /// Collection of storage items - files or folders. If format isn't avaialble, returns null. + /// Collection of storage items - files or folders. If format isn't available, returns null. /// public static IEnumerable? GetFiles(this IDataObject dataObject) { @@ -23,7 +23,7 @@ namespace Avalonia.Input /// /// /// - /// Collection of file names. If format isn't avaialble, returns null. + /// 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) @@ -40,7 +40,7 @@ namespace Avalonia.Input /// /// /// - /// A text string. If format isn't avaialble, returns null. + /// A text string. If format isn't available, returns null. /// public static string? GetText(this IDataObject dataObject) { diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index b6fcd8c7db..6ccd0a8499 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Input /// Tries to get the data of the given DataFormat. /// /// - /// Object data. If format isn't avaialble, returns null. + /// Object data. If format isn't available, returns null. /// object? Get(string dataFormat); } From cc1cf2003a62fc6653a392f094b978a162e9e79b Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Feb 2023 20:29:22 -0500 Subject: [PATCH 22/36] Separate out SplitView types and move to SplitView directory --- .../{ => SplitView}/SplitView.cs | 62 ------------------- .../SplitView/SplitViewDisplayMode.cs | 29 +++++++++ .../SplitViewPaneClosingEventArgs.cs | 0 .../SplitView/SplitViewPanePlacement.cs | 18 ++++++ .../SplitView/SplitViewTemplateSettings.cs | 32 ++++++++++ 5 files changed, 79 insertions(+), 62 deletions(-) rename src/Avalonia.Controls/{ => SplitView}/SplitView.cs (89%) create mode 100644 src/Avalonia.Controls/SplitView/SplitViewDisplayMode.cs rename src/Avalonia.Controls/{ => SplitView}/SplitViewPaneClosingEventArgs.cs (100%) create mode 100644 src/Avalonia.Controls/SplitView/SplitViewPanePlacement.cs create mode 100644 src/Avalonia.Controls/SplitView/SplitViewTemplateSettings.cs diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs similarity index 89% rename from src/Avalonia.Controls/SplitView.cs rename to src/Avalonia.Controls/SplitView/SplitView.cs index b45e0f2adb..35faf8605d 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -11,68 +11,6 @@ using Avalonia.Metadata; 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 /// 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/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitView/SplitViewPaneClosingEventArgs.cs similarity index 100% rename from src/Avalonia.Controls/SplitViewPaneClosingEventArgs.cs rename to src/Avalonia.Controls/SplitView/SplitViewPaneClosingEventArgs.cs 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); + } + } +} From 11f09232d3bf7b376f0ff7091365800f6266df7e Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Feb 2023 20:59:36 -0500 Subject: [PATCH 23/36] Use OnPropertyChanged override method for all SplitView change handling --- src/Avalonia.Controls/SplitView/SplitView.cs | 135 +++++++++---------- 1 file changed, 65 insertions(+), 70 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index 35faf8605d..da8190d764 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -110,12 +110,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.OnPaneChanged(e)); } /// @@ -273,7 +267,40 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if (change.Property == IsPaneOpenProperty) + 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(); @@ -290,6 +317,30 @@ namespace Avalonia.Controls OnPaneClosed(EventArgs.Empty); } } + 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) @@ -325,7 +376,7 @@ namespace Avalonia.Controls } if (closePane) { - IsPaneOpen = false; + SetCurrentValue(IsPaneOpenProperty, false); e.Handled = true; } } @@ -355,21 +406,9 @@ namespace Avalonia.Controls PaneClosed?.Invoke(this, 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); - } - } - + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch @@ -382,6 +421,9 @@ namespace Avalonia.Controls }; } + /// + /// Gets the appropriate PseudoClass for the given . + /// private static string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch @@ -392,53 +434,6 @@ 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) - { - var oldState = GetPseudoClass(e.GetOldValue()); - var newState = GetPseudoClass(e.GetNewValue()); - - PseudoClasses.Remove($":{oldState}"); - PseudoClasses.Add($":{newState}"); - - var (closedPaneWidth, paneColumnGridLength) = e.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; - } - - private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e) - { - var mode = (bool)e.NewValue!; - PseudoClasses.Set(":lightdismiss", mode); - } - - private void OnPaneChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.OldValue is ILogical oldChild) - { - LogicalChildren.Remove(oldChild); - } - - if (e.NewValue is ILogical newChild) - { - LogicalChildren.Add(newChild); - } - } - /// /// Called when the property has to be coerced. /// From 1018a7779565d185aa2f1c12775ede2f9ba0c8c3 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Feb 2023 21:25:19 -0500 Subject: [PATCH 24/36] Switch to routed events in SplitView and support cancellation in opening/closing --- src/Avalonia.Controls/SplitView/SplitView.cs | 109 +++++++++++++++---- 1 file changed, 85 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/SplitView/SplitView.cs b/src/Avalonia.Controls/SplitView/SplitView.cs index da8190d764..1099a40f08 100644 --- a/src/Avalonia.Controls/SplitView/SplitView.cs +++ b/src/Avalonia.Controls/SplitView/SplitView.cs @@ -97,6 +97,38 @@ namespace Avalonia.Controls public static readonly StyledProperty TemplateSettingsProperty = AvaloniaProperty.Register(nameof(TemplateSettings)); + /// + /// 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; @@ -208,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) { @@ -308,13 +364,15 @@ namespace Avalonia.Controls { PseudoClasses.Add(":open"); PseudoClasses.Remove(":closed"); - OnPaneOpened(EventArgs.Empty); + + OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this)); } else { PseudoClasses.Add(":closed"); PseudoClasses.Remove(":open"); - OnPaneClosed(EventArgs.Empty); + + OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this)); } } else if (change.Property == PaneProperty) @@ -386,24 +444,24 @@ namespace Avalonia.Controls return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay); } - protected virtual void OnPaneOpening(EventArgs args) + protected virtual void OnPaneOpening(CancelRoutedEventArgs args) { - PaneOpening?.Invoke(this, args); + RaiseEvent(args); } - protected virtual void OnPaneOpened(EventArgs args) + protected virtual void OnPaneOpened(RoutedEventArgs args) { - PaneOpened?.Invoke(this, args); + RaiseEvent(args); } - protected virtual void OnPaneClosing(SplitViewPaneClosingEventArgs args) + protected virtual void OnPaneClosing(CancelRoutedEventArgs args) { - PaneClosing?.Invoke(this, args); + RaiseEvent(args); } - protected virtual void OnPaneClosed(EventArgs args) + protected virtual void OnPaneClosed(RoutedEventArgs args) { - PaneClosed?.Invoke(this, args); + RaiseEvent(args); } /// @@ -440,19 +498,22 @@ namespace Avalonia.Controls /// The value to coerce. protected virtual bool OnCoerceIsPaneOpen(bool value) { + CancelRoutedEventArgs eventArgs; + if (value) { - OnPaneOpening(EventArgs.Empty); + eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this); + OnPaneOpening(eventArgs); } else { - var eventArgs = new SplitViewPaneClosingEventArgs(false); + eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this); OnPaneClosing(eventArgs); + } - if (eventArgs.Cancel) - { - return !value; - } + if (eventArgs.Cancel) + { + return !value; } return value; From 937b07f339fb1d801e071ac8d3f7bd1a464a1bd4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Feb 2023 21:26:34 -0500 Subject: [PATCH 25/36] Remove SplitViewPaneClosingEventArgs --- .../SplitView/SplitViewPaneClosingEventArgs.cs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/Avalonia.Controls/SplitView/SplitViewPaneClosingEventArgs.cs diff --git a/src/Avalonia.Controls/SplitView/SplitViewPaneClosingEventArgs.cs b/src/Avalonia.Controls/SplitView/SplitViewPaneClosingEventArgs.cs deleted file mode 100644 index 46fb2d161b..0000000000 --- a/src/Avalonia.Controls/SplitView/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; - } - } -} From afbb04b5ffe4fe133de16f63dfcd14323656833a Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 18 Feb 2023 21:26:49 -0500 Subject: [PATCH 26/36] Fix eventArgs type in Expander --- src/Avalonia.Controls/Expander.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } From 1ecdf3252064fafddf6bb82da477cb7cb1e0ea73 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 20 Feb 2023 10:34:21 +0100 Subject: [PATCH 27/36] Remove trailing whitespace for right aligned text Fix justification for non wrapped text Fix text trimming for RTL flow direction --- src/Avalonia.Base/Media/FormattedText.cs | 2 +- .../Media/TextCollapsingCreateInfo.cs | 4 +- .../TextFormatting/InterWordJustification.cs | 13 +- .../Media/TextFormatting/ShapedTextRun.cs | 21 +- .../TextCollapsingProperties.cs | 9 +- .../TextFormatting/TextEllipsisHelper.cs | 191 +++++++++++++----- .../Media/TextFormatting/TextLayout.cs | 25 +-- .../TextLeadingPrefixCharacterEllipsis.cs | 7 +- .../Media/TextFormatting/TextLineImpl.cs | 47 +++-- .../TextTrailingCharacterEllipsis.cs | 7 +- .../TextTrailingWordEllipsis.cs | 7 +- .../Media/TextLeadingPrefixTrimming.cs | 2 +- .../Media/TextTrailingTrimming.cs | 4 +- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 2 +- 15 files changed, 231 insertions(+), 112 deletions(-) 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/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/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 8a2d4ecc6b..dc8744b292 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties, FlowDirection.LeftToRight)); } currentHeight += textLine.Height; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 70e74cdf83..bd64d72a4d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -407,7 +407,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.False(textLine.HasCollapsed); - TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties)); + TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties, FlowDirection.LeftToRight)); var collapsedLine = textLine.Collapse(collapsingProperties); From cbcff7bf1d9d8bf8c3311029077b84c11fe2536a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 21 Feb 2023 03:19:25 -0500 Subject: [PATCH 28/36] Use popup theme instead of target, as it is correct --- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 142db71761..9d443d9289 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -392,7 +392,7 @@ namespace Avalonia.Controls.Primitives topLevelPopup .Bind( ThemeVariantScope.ActualThemeVariantProperty, - placementTarget.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) .DisposeWith(handlerCleanup); } From 74ef92a4a8f5575472d02c8ffed01774d118cd2e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 21 Feb 2023 03:19:41 -0500 Subject: [PATCH 29/36] Make StyledElement use theme variants again --- src/Avalonia.Base/StyledElement.cs | 21 ++++++++++++++++++++- src/Avalonia.Base/Styling/ThemeVariant.cs | 15 +++++++++++++++ src/Avalonia.Controls/Control.cs | 18 +----------------- src/Avalonia.Controls/ThemeVariantScope.cs | 16 ++++------------ 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 405b874fed..94af8385a8 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, @@ -143,6 +143,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -299,6 +302,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -618,7 +624,20 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); + else + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } } private protected virtual void OnControlThemeChanged() diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 5a6a8785a5..389136b0f5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -11,6 +11,21 @@ namespace Avalonia.Styling; [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.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index c9e8f0d045..dbece12575 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls /// - A property to allow user-defined data to be attached to the control. /// - and other context menu related members. /// - public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue, IThemeVariantHost + public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue { /// /// Defines the property. @@ -162,11 +162,6 @@ namespace Avalonia.Controls set => SetValue(TagProperty, value); } - /// - public ThemeVariant ActualThemeVariant => GetValue(ThemeVariantScope.ActualThemeVariantProperty); - - public event EventHandler? ActualThemeVariantChanged; - /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -531,17 +526,6 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } - else if (change.Property == ThemeVariantScope.RequestedThemeVariantProperty) - { - if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ThemeVariantScope.ActualThemeVariantProperty, themeVariant); - else - ClearValue(ThemeVariantScope.ActualThemeVariantProperty); - } - else if (change.Property == ThemeVariantScope.ActualThemeVariantProperty) - { - ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); - } } } } diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index b29f1ee56e..f5ad4b2f94 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -7,21 +7,13 @@ namespace Avalonia.Controls /// public class ThemeVariantScope : Decorator { - /// - /// Defines the property. - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ActualThemeVariant), - inherits: true); + ThemeVariant.ActualThemeVariantProperty.AddOwner(); - /// - /// Defines the property. - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(RequestedThemeVariant), - defaultValue: ThemeVariant.Default); + ThemeVariant.RequestedThemeVariantProperty.AddOwner(); /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. From 3096a2d1590ec2a3172b0b79f0faa737d225f59c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 21 Feb 2023 23:08:29 +0100 Subject: [PATCH 30/36] Add failing integration test. For the following scenario: - Open a child window of main window - Open a modal window as a child of main window - Close main window - First child window should remain in front of main window --- .../WindowTests_MacOS.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 0839cbf183..55812d8df7 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -150,6 +150,18 @@ namespace Avalonia.IntegrationTests.Appium windowState = mainWindow.FindElementByAccessibilityId("MainWindowState"); Assert.Equal("Normal", windowState.Text); } + + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Dialog_Stays_InFront_Of_Parent_After_Modal_Closed() + { + using (OpenWindow(new PixelSize(200, 300), ShowWindowMode.Owned, WindowStartupLocation.Manual)) + { + OpenWindow(null, ShowWindowMode.Modal, WindowStartupLocation.Manual).Dispose(); + + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); + } + } [PlatformFact(TestPlatforms.MacOS)] public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked() From e77c79b2aa4885ba3e8d4f96efd1e4ed6e48ad03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 21 Feb 2023 23:11:10 +0100 Subject: [PATCH 31/36] Enforce window order after a delay. Sometimes, after `windowDidBecomeKey` is called and the window order is enforced via `BringToFront`, the window that became key is then moved to the front, breaking window order. Given that we're already invalidating the shadow by scheduling it on the dispatcher, use this opportunity to also enforce the window order again. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 23abf1d53f..3a03abf211 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -292,12 +292,13 @@ { if (_parent == nullptr) return; - + _parent->BringToFront(); dispatch_async(dispatch_get_main_queue(), ^{ @try { - [self invalidateShadow]; + [self invalidateShadow]; + self->_parent->BringToFront(); } @finally{ } From 3de50b3baa52d6c40edca1ad6037c256c98f563c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 22 Feb 2023 13:09:17 +0000 Subject: [PATCH 32/36] if the window is closed, we cannot become the key window. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 23abf1d53f..505ff63d4b 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()); From d98467c3d87992b9d541679e3488bda282842c5e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Feb 2023 17:48:49 +0100 Subject: [PATCH 33/36] Remove TotalCountChanged. Merge it into existing `IChildIndexProvider.ChildIndexChanged` event. --- .../LogicalTree/ChildIndexChangedEventArgs.cs | 63 ++++++++++++++++--- .../LogicalTree/IChildIndexProvider.cs | 5 -- .../Styling/Activators/NthChildActivator.cs | 9 +-- .../Primitives/DataGridCellsPresenter.cs | 2 +- .../DataGridColumnHeadersPresenter.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 11 +--- src/Avalonia.Controls/Panel.cs | 11 +--- 7 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs index 8f2be75e0f..6b41c1c66c 100644 --- a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -1,26 +1,59 @@ -#nullable enable -using System; +using System; + +#nullable enable namespace Avalonia.LogicalTree { /// - /// Event args for event. + /// Describes the action that caused a event. /// - public class ChildIndexChangedEventArgs : EventArgs + public enum ChildIndexChangedAction { - public static new ChildIndexChangedEventArgs Empty { get; } = new ChildIndexChangedEventArgs(); + /// + /// The index of a single child changed. + /// + ChildIndexChanged, - private ChildIndexChangedEventArgs() - { - Index = -1; - } + /// + /// 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 + { + /// + /// 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; } + private ChildIndexChangedEventArgs(ChildIndexChangedAction action) + { + Action = action; + Index = -1; + } + + /// + /// 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. /// @@ -30,5 +63,17 @@ namespace Avalonia.LogicalTree /// 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 a5e191d63e..186c9527f2 100644 --- a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs +++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs @@ -28,10 +28,5 @@ namespace Avalonia.LogicalTree /// Notifies subscriber when a child's index was changed. /// event EventHandler? ChildIndexChanged; - - /// - /// Notifies subscriber when the total child count changes. - /// - event EventHandler? TotalCountChanged; } } diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs index e0fa056765..8bdcec2e53 100644 --- a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -37,7 +37,6 @@ namespace Avalonia.Styling.Activators protected override void Initialize() { _provider.ChildIndexChanged += ChildIndexChanged; - _provider.TotalCountChanged += TotalCountChanged; } protected override void Deinitialize() @@ -48,9 +47,11 @@ namespace Avalonia.Styling.Activators private void ChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { // Run matching again if: - // 1. e.Child is null, when all children indices were changed. - // 2. Subscribed child index was changed. - if (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 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/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 8c3c65a6a7..9483f98881 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -101,7 +101,6 @@ namespace Avalonia.Controls private int _itemCount; private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; - private EventHandler? _totalCountChanged; private IDataTemplate? _displayMemberItemTemplate; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; @@ -218,12 +217,6 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } - event EventHandler? IChildIndexProvider.TotalCountChanged - { - add => _totalCountChanged += value; - remove => _totalCountChanged -= value; - } - /// public event EventHandler HorizontalSnapPointsChanged { @@ -500,7 +493,7 @@ namespace Avalonia.Controls else if (change.Property == ItemCountProperty) { UpdatePseudoClasses(change.GetNewValue()); - _totalCountChanged?.Invoke(this, EventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); } else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { @@ -585,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) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 3006efb7de..10391afca2 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -35,7 +35,6 @@ namespace Avalonia.Controls } private EventHandler? _childIndexChanged; - private EventHandler? _totalCountChanged; /// /// Initializes a new instance of the class. @@ -67,12 +66,6 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } - event EventHandler? IChildIndexProvider.TotalCountChanged - { - add => _totalCountChanged += value; - remove => _totalCountChanged -= value; - } - /// /// Renders the visual to a . /// @@ -161,7 +154,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset); InvalidateMeasureOnChildrenChanged(); } @@ -173,7 +166,7 @@ namespace Avalonia.Controls private void ChildrenPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Children.Count) || e.PropertyName is null) - _totalCountChanged?.Invoke(this, EventArgs.Empty); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); } private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) From 87dc9bb1b4d37c9c8435c2b0e5bc48606c502d85 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Feb 2023 18:23:16 +0100 Subject: [PATCH 34/36] Only subscribe to event when necessary. --- src/Avalonia.Controls/Panel.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 10391afca2..fa18ee468c 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -42,7 +42,6 @@ namespace Avalonia.Controls public Panel() { Children.CollectionChanged += ChildrenChanged; - Children.PropertyChanged += ChildrenPropertyChanged; } /// @@ -62,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; + } } /// From c837c1005165413ae49f0fde2f71d5f4ed52d223 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Feb 2023 22:23:17 +0100 Subject: [PATCH 35/36] Don't call BringToFront on closed window. --- native/Avalonia.Native/src/OSX/AvnWindow.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/AvnWindow.mm b/native/Avalonia.Native/src/OSX/AvnWindow.mm index 80a0d3e66f..b1fb915e04 100644 --- a/native/Avalonia.Native/src/OSX/AvnWindow.mm +++ b/native/Avalonia.Native/src/OSX/AvnWindow.mm @@ -298,7 +298,8 @@ dispatch_async(dispatch_get_main_queue(), ^{ @try { [self invalidateShadow]; - self->_parent->BringToFront(); + if (self->_parent != nullptr) + self->_parent->BringToFront(); } @finally{ } From ae1fcfed51546b0bd38d67ee9619d3f4b739131e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 23 Feb 2023 11:07:51 +0300 Subject: [PATCH 36/36] Refactored DrawingContext and VisualBrush, added DrawingBrush (#10419) Refactored DrawingContext and VisualBrush, added DrawingBrush --- samples/RenderDemo/Pages/CustomSkiaPage.cs | 20 +- .../RenderDemo/Pages/PathMeasurementPage.cs | 5 +- .../Pages/RenderTargetBitmapPage.cs | 4 +- src/Avalonia.Base/Media/DrawingBrush.cs | 66 +++ src/Avalonia.Base/Media/DrawingContext.cs | 403 +++++++++--------- src/Avalonia.Base/Media/DrawingGroup.cs | 139 ++---- src/Avalonia.Base/Media/DrawingImage.cs | 2 +- src/Avalonia.Base/Media/ISceneBrush.cs | 31 ++ src/Avalonia.Base/Media/IVisualBrush.cs | 16 - src/Avalonia.Base/Media/Imaging/Bitmap.cs | 2 +- .../Media/Imaging/RenderTargetBitmap.cs | 18 +- .../Media/ImmediateDrawingContext.cs | 6 +- .../Media/Immutable/ImmutableVisualBrush.cs | 66 --- .../Media/PlatformDrawingContext.cs | 112 +++++ src/Avalonia.Base/Media/VisualBrush.cs | 24 +- src/Avalonia.Base/Platform/IRenderTarget.cs | 6 +- .../Composition/CompositingRenderer.cs | 2 +- .../Drawing/CompositionDrawList.cs | 37 +- .../CompositionDrawListSceneBrushContent.cs | 37 ++ .../Drawing/CompositionDrawingContext.cs | 183 ++++---- .../Composition/Server/DrawingContextProxy.cs | 30 +- .../Server/ServerCompositionTarget.cs | 7 +- .../Rendering/IVisualBrushRenderer.cs | 29 -- .../Rendering/ImmediateRenderer.cs | 37 +- .../SceneGraph/BrushDrawOperation.cs | 15 +- .../Rendering/SceneGraph/ClipNode.cs | 4 +- .../SceneGraph/CustomDrawOperation.cs | 39 +- .../Rendering/SceneGraph/DrawOperation.cs | 10 + .../Rendering/SceneGraph/EllipseNode.cs | 37 +- .../SceneGraph/ExperimentalAcrylicNode.cs | 24 +- .../Rendering/SceneGraph/GeometryClipNode.cs | 4 +- .../Rendering/SceneGraph/GeometryNode.cs | 30 +- .../Rendering/SceneGraph/GlyphRunNode.cs | 33 +- .../Rendering/SceneGraph/IDrawOperation.cs | 10 +- .../Rendering/SceneGraph/ImageNode.cs | 21 +- .../Rendering/SceneGraph/LineNode.cs | 19 +- .../Rendering/SceneGraph/OpacityMaskNode.cs | 41 +- .../Rendering/SceneGraph/RectangleNode.cs | 52 +-- .../Threading/ThreadSafeObjectPool.cs | 7 +- .../ExperimentalAcrylicBorder.cs | 2 +- .../Utils/BorderRenderHelper.cs | 2 +- .../HeadlessPlatformRenderInterface.cs | 4 +- src/Avalonia.X11/X11CursorFactory.cs | 2 +- src/Avalonia.X11/X11IconLoader.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 136 ++++-- .../Avalonia.Skia/FramebufferRenderTarget.cs | 3 +- .../Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 3 +- .../Helpers/DrawingContextHelper.cs | 3 +- .../Helpers/ImageSavingHelper.cs | 10 + src/Skia/Avalonia.Skia/PictureRenderTarget.cs | 55 +++ src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 3 +- .../ExternalRenderTarget.cs | 4 +- .../FramebufferShimRenderTarget.cs | 8 +- .../Media/DrawingContextImpl.cs | 32 +- .../Media/ImageBrushImpl.cs | 2 +- .../Imaging/D2DRenderTargetBitmapImpl.cs | 4 +- .../Imaging/WicRenderTargetBitmapImpl.cs | 8 +- .../Avalonia.Direct2D1/RenderTarget.cs | 4 +- .../SwapChainRenderTarget.cs | 4 +- .../RenderTests_Culling.cs | 2 +- .../SceneGraph/DrawOperationTests.cs | 4 +- .../Rendering/SceneGraph/EllipseNodeTests.cs | 4 +- .../Rendering/ShapeRendering.cs | 2 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 4 +- .../Media/TileBrushTests.cs | 94 ++++ .../MockPlatformRenderInterface.cs | 6 +- tests/Avalonia.UnitTests/TestRoot.cs | 4 +- .../DrawingBrushIsProperlyTiled.expected.png | Bin 0 -> 4340 bytes .../DrawingBrushIsProperlyTiled.expected.png | Bin 0 -> 3083 bytes ...rawingBrushIsProperlyUpscaled.expected.png | Bin 0 -> 5008 bytes 70 files changed, 1094 insertions(+), 945 deletions(-) create mode 100644 src/Avalonia.Base/Media/DrawingBrush.cs create mode 100644 src/Avalonia.Base/Media/ISceneBrush.cs delete mode 100644 src/Avalonia.Base/Media/IVisualBrush.cs delete mode 100644 src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs create mode 100644 src/Avalonia.Base/Media/PlatformDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs delete mode 100644 src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs create mode 100644 src/Skia/Avalonia.Skia/PictureRenderTarget.cs create mode 100644 tests/Avalonia.RenderTests/Media/TileBrushTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png create mode 100644 tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png create mode 100644 tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyUpscaled.expected.png 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/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs new file mode 100644 index 0000000000..2825628948 --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -0,0 +1,66 @@ +using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + /// + /// Paints an area with an . + /// + public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + { + /// + /// Defines the property. + /// + public static readonly StyledProperty DrawingProperty = + AvaloniaProperty.Register(nameof(Drawing)); + + static DrawingBrush() + { + AffectsRender(DrawingProperty); + } + + /// + /// Initializes a new instance of the class. + /// + public DrawingBrush() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The visual to draw. + public DrawingBrush(Drawing visual) + { + Drawing = visual; + } + + /// + /// Gets or sets the visual to draw. + /// + public Drawing? Drawing + { + get { return GetValue(DrawingProperty); } + set { SetValue(DrawingProperty, value); } + } + + ISceneBrushContent? ISceneBrush.CreateContent() + { + if (Drawing == null) + return null; + + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + Drawing?.Draw(recorder); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + drawList.CalculateBounds(), true); + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index a37fa6fd32..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,17 +355,13 @@ namespace Avalonia.Media /// A disposable used to undo the clip geometry. public PushedState PushGeometryClip(Geometry clip) { - _ = clip ?? throw new ArgumentNullException(nameof(clip)); - - // HACK: This check was added when nullable annotations pointed out that we're potentially - // pushing a null value for the clip here. Ideally we'd return an empty PushedState here but - // I don't want to make that change as part of adding nullable annotations. - if (clip.PlatformImpl is null) - throw new InvalidOperationException("Cannot push empty geometry clip."); - - PlatformImpl.PushGeometryClip(clip.PlatformImpl); - return new PushedState(this, PushedState.PushedStateType.GeometryClip); + PushGeometryClipCore(clip); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.GeometryClip)); + return new PushedState(this); } + + protected abstract void PushGeometryClipCore(Geometry clip); /// /// Pushes an opacity value. @@ -364,11 +370,13 @@ namespace Avalonia.Media /// The bounds. /// A disposable used to undo the opacity. public PushedState PushOpacity(double opacity, Rect bounds) - //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity, bounds); - return new PushedState(this, PushedState.PushedStateType.Opacity); + PushOpacityCore(opacity, bounds); + _states ??= StateStackPool.Get(); + _states.Push(new RestoreState(this, RestoreState.PushedStateType.Opacity)); + return new PushedState(this); } + protected abstract void PushOpacityCore(double opacity, Rect bounds); /// /// Pushes an opacity mask. @@ -380,70 +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 7b02649b6c..812d315912 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -67,10 +67,7 @@ namespace Avalonia.Media } } - public DrawingContext Open() - { - return new DrawingContext(new DrawingGroupDrawingContext(this)); - } + public DrawingContext Open() => new DrawingGroupDrawingContext(this); public override void Draw(DrawingContext context) { @@ -105,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(); @@ -135,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)) { @@ -159,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)) { @@ -169,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) { @@ -179,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) + protected override void PushClipCore(RoundedRect rect) { throw new NotImplementedException(); } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushClipCore(Rect rect) { throw new NotImplementedException(); } - public void Custom(ICustomDrawOperation custom) + protected override void PushGeometryClipCore(Geometry clip) { 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 PushOpacityCore(double opacity, Rect bounds) { throw new NotImplementedException(); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { throw new NotImplementedException(); } - public void PopBitmapBlendMode() + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { throw new NotImplementedException(); } - public void PopClip() + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); } - public void PopGeometryClip() + protected override void DrawLineCore(IPen pen, Point p1, Point p2) { - throw new NotImplementedException(); - } - - public void PopOpacity() - { - throw new NotImplementedException(); - } - - public void PopOpacityMask() - { - throw new NotImplementedException(); - } - - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) - { - throw new NotImplementedException(); - } - - public void PushClip(Rect clip) - { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); - public void PushClip(RoundedRect clip) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); } - public void PushGeometryClip(IGeometryImpl clip) + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { - throw new NotImplementedException(); - } + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rrect.Rect); - public void PushOpacity(double opacity, Rect bounds) - { - throw new NotImplementedException(); + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void PushOpacityMask(IBrush mask, Rect bounds) - { - throw new NotImplementedException(); - } + public override void Custom(ICustomDrawOperation custom) => throw new NotSupportedException(); - public void Dispose() + protected override void DisposeCore() { // Dispose may be called multiple times without throwing // an exception. @@ -366,22 +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/ISceneBrush.cs b/src/Avalonia.Base/Media/ISceneBrush.cs new file mode 100644 index 0000000000..df72dd1ace --- /dev/null +++ b/src/Avalonia.Base/Media/ISceneBrush.cs @@ -0,0 +1,31 @@ +using System; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; + +namespace Avalonia.Media +{ + [NotClientImplementable] + public interface ISceneBrush : ITileBrush + { + ISceneBrushContent? CreateContent(); + } + + [NotClientImplementable] + public interface ISceneBrushContent : IImmutableBrush, IDisposable + { + ITileBrush Brush { get; } + Rect Rect { get; } + void Render(IDrawingContextImpl context, Matrix? transform); + internal bool UseScalableRasterization { get; } + } + + internal class ImmutableSceneBrush : ImmutableTileBrush + { + public ImmutableSceneBrush(ITileBrush source) : base(source) + { + } + } +} diff --git a/src/Avalonia.Base/Media/IVisualBrush.cs b/src/Avalonia.Base/Media/IVisualBrush.cs deleted file mode 100644 index a7d3e4da10..0000000000 --- a/src/Avalonia.Base/Media/IVisualBrush.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media -{ - /// - /// Paints an area with an . - /// - [NotClientImplementable] - public interface IVisualBrush : ITileBrush - { - /// - /// Gets the visual to draw. - /// - Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 6577532891..c4720d772e 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -227,7 +227,7 @@ namespace Avalonia.Media.Imaging Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { - context.PlatformImpl.DrawBitmap( + context.DrawBitmap( PlatformImpl, 1, sourceRect, diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 88e5e627ee..e77dd9d1ab 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.Imaging /// /// A bitmap that holds the rendering of a . /// - public class RenderTargetBitmap : Bitmap, IDisposable, IRenderTarget + public class RenderTargetBitmap : Bitmap, IDisposable { /// /// Initializes a new instance of the class. @@ -44,7 +44,11 @@ namespace Avalonia.Media.Imaging /// Renders a visual to the . /// /// The visual to render. - public void Render(Visual visual) => ImmediateRenderer.Render(visual, this); + public void Render(Visual visual) + { + using (var ctx = CreateDrawingContext()) + ImmediateRenderer.Render(visual, ctx); + } /// /// Creates a platform-specific implementation for a . @@ -58,9 +62,11 @@ namespace Avalonia.Media.Imaging return factory.CreateRenderTargetBitmap(size, dpi); } - /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); - - bool IRenderTarget.IsCorrupted => false; + public DrawingContext CreateDrawingContext() + { + var platform = PlatformImpl.Item.CreateDrawingContext(); + platform.Clear(Colors.Transparent); + return new PlatformDrawingContext(platform); + } } } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 2564d89bac..58b153482d 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -354,12 +354,10 @@ namespace Avalonia.Media throw new ObjectDisposedException(nameof(DrawingContext)); while (_states.Count != 0) _states.Peek().Dispose(); - StateStackPool.Return(_states); - _states = null; + StateStackPool.ReturnAndSetNull(ref _states); if (_transformContainers.Count != 0) throw new InvalidOperationException("Transform container stack is non-empty"); - TransformStackPool.Return(_transformContainers); - _transformContainers = null; + TransformStackPool.ReturnAndSetNull(ref _transformContainers); if (_ownsImpl) PlatformImpl.Dispose(); } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs deleted file mode 100644 index e9086eee37..0000000000 --- a/src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media.Imaging; - -namespace Avalonia.Media.Immutable -{ - /// - /// Paints an area with an . - /// - internal class ImmutableVisualBrush : ImmutableTileBrush, IVisualBrush - { - /// - /// Initializes a new instance of the class. - /// - /// The visual to draw. - /// The horizontal alignment of a tile in the destination. - /// The vertical alignment of a tile in the destination. - /// The rectangle on the destination in which to paint a tile. - /// The opacity of the brush. - /// The transform of the brush. - /// The transform origin of the brush - /// The rectangle of the source image that will be displayed. - /// - /// How the source rectangle will be stretched to fill the destination rect. - /// - /// The tile mode. - /// Controls the quality of interpolation. - public ImmutableVisualBrush( - Visual? visual, - AlignmentX alignmentX = AlignmentX.Center, - AlignmentY alignmentY = AlignmentY.Center, - RelativeRect? destinationRect = null, - double opacity = 1, - ImmutableTransform? transform = null, - RelativePoint transformOrigin = default, - RelativeRect? sourceRect = null, - Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None, - BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) - : base( - alignmentX, - alignmentY, - destinationRect ?? RelativeRect.Fill, - opacity, - transform, - transformOrigin, - sourceRect ?? RelativeRect.Fill, - stretch, - tileMode, - bitmapInterpolationMode) - { - Visual = visual; - } - - /// - /// Initializes a new instance of the class. - /// - /// The brush from which this brush's properties should be copied. - public ImmutableVisualBrush(IVisualBrush source) - : base(source) - { - Visual = source.Visual; - } - - /// - public Visual? Visual { get; } - } -} diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs new file mode 100644 index 0000000000..eb8a93722c --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport +{ + private readonly IDrawingContextImpl _impl; + private readonly bool _ownsImpl; + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; + + private Stack? _transforms; + + + public PlatformDrawingContext(IDrawingContextImpl impl, bool ownsImpl = true) + { + _impl = impl; + _ownsImpl = ownsImpl; + } + + protected override void DrawLineCore(IPen pen, Point p1, Point p2) => + _impl.DrawLine(pen, p1, p2); + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) => + _impl.DrawGeometry(brush, pen, geometry); + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, + BoxShadows boxShadows = default) => + _impl.DrawRectangle(brush, pen, rrect, boxShadows); + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); + + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) => + _impl.DrawBitmap(source, opacity, sourceRect, destRect, bitmapInterpolationMode); + + public override void Custom(ICustomDrawOperation custom) => + custom.Render(_impl); + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) + { + _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); + + if (foreground != null) + _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + } + + protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); + + protected override void PushClipCore(Rect rect) => _impl.PushClip(rect); + + protected override void PushGeometryClipCore(Geometry clip) => + _impl.PushGeometryClip(clip.PlatformImpl ?? throw new ArgumentException()); + + protected override void PushOpacityCore(double opacity, Rect bounds) => + _impl.PushOpacity(opacity, bounds); + + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) => + _impl.PushOpacityMask(mask, bounds); + + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) => + _impl.PushBitmapBlendMode(blendingMode); + + protected override void PushTransformCore(Matrix matrix) + { + _transforms ??= TransformStackPool.Get(); + var current = _impl.Transform; + _transforms.Push(current); + _impl.Transform = matrix * current; + } + + protected override void PopClipCore() => _impl.PopClip(); + + protected override void PopGeometryClipCore() => _impl.PopGeometryClip(); + + protected override void PopOpacityCore() => _impl.PopOpacity(); + + protected override void PopOpacityMaskCore() => _impl.PopOpacityMask(); + + protected override void PopBitmapBlendModeCore() => _impl.PopBitmapBlendMode(); + + protected override void PopTransformCore() => + _impl.Transform = + (_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop(); + + protected override void DisposeCore() + { + if (_ownsImpl) + _impl.Dispose(); + if (_transforms != null) + { + if (_transforms.Count != 0) + throw new InvalidOperationException("Not all states are disposed"); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } + } + + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (_impl is IDrawingContextWithAcrylicLikeSupport idc) + idc.DrawRectangle(material, rect); + else + DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect); + } +} diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 2be3e9a94e..6bfe20271f 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,11 +1,14 @@ using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, IVisualBrush, IMutableBrush + public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender { /// /// Defines the property. @@ -43,10 +46,23 @@ namespace Avalonia.Media set { SetValue(VisualProperty, value); } } - /// - IImmutableBrush IMutableBrush.ToImmutable() + ISceneBrushContent? ISceneBrush.CreateContent() { - return new ImmutableVisualBrush(this); + if (Visual == null) + return null; + + if (Visual is IVisualBrushInitialize initialize) + initialize.EnsureInitialized(); + + var recorder = new CompositionDrawingContext(); + recorder.BeginUpdate(null); + ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); + var drawList = recorder.EndUpdate(); + if (drawList == null) + return null; + + return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, + new(Visual.Bounds.Size), false); } } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 73e9e58da4..31ad84341d 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -14,11 +14,7 @@ namespace Avalonia.Platform /// /// Creates an for a rendering session. /// - /// - /// A render to be used to render visual brushes. May be null if no visual brushes are - /// to be drawn. - /// - IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); + IDrawingContextImpl CreateDrawingContext(); /// /// Indicates if the render target is no longer usable and needs to be recreated diff --git a/src/Avalonia.Base/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 6b380608fe..f81cc5a1a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -7,7 +8,7 @@ using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; -using Avalonia.VisualTree; +using Avalonia.Threading; // Special license applies License.md @@ -16,46 +17,60 @@ namespace Avalonia.Rendering.Composition; /// /// An IDrawingContextImpl implementation that builds /// -internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport +internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport { private CompositionDrawListBuilder _builder = new(); private int _drawOperationIndex; + + private static ThreadSafeObjectPool> TransformStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public Matrix Transform { get; set; } = Matrix.Identity; + private Stack? _transforms; - /// - public void Clear(Color color) - { - // Cannot clear a deferred scene. - } + private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } = + ThreadSafeObjectPool>.Default; - /// - public void Dispose() - { - // Nothing to do here since we allocate no unmanaged resources. - } + private Stack? _needsToPopOpacityMask; + public Matrix Transform { get; set; } = Matrix.Identity; + public void BeginUpdate(CompositionDrawList? list) { _builder.Reset(list); _drawOperationIndex = 0; } - public CompositionDrawList EndUpdate() + public CompositionDrawList? EndUpdate() { + // Make sure that any pending pop operations are completed + Dispose(); + _builder.TrimTo(_drawOperationIndex); - return _builder.DrawOperations!; + return _builder.DrawOperations; } + + protected override void DisposeCore() + { + if (_transforms != null) + { + _transforms.Clear(); + TransformStackPool.ReturnAndSetNull(ref _transforms); + } - /// - public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + if (_needsToPopOpacityMask != null) + { + _needsToPopOpacityMask.Clear(); + _needsToPopOpacityMask = null; + } + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) { - Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); + Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry)); } else { @@ -63,9 +78,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, - BitmapInterpolationMode bitmapInterpolationMode) + internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { var next = NextDrawAs(); @@ -81,14 +95,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) - { - // This method is currently only used to composite layers so shouldn't be called here. - throw new NotSupportedException(); - } - - /// - public void DrawLine(IPen? pen, Point p1, Point p2) + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) { if (pen is null) { @@ -99,7 +106,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) { - Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); + Add(new LineNode(Transform, pen, p1, p2)); } else { @@ -108,14 +115,14 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) { - Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); + Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows)); } else { @@ -138,21 +145,21 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) { - Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); + Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect)); } else { ++_drawOperationIndex; } } - - public void Custom(ICustomDrawOperation custom) + + public override void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); if (next == null || !next.Item.Equals(Transform, custom)) @@ -161,10 +168,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } - public object? GetFeature(Type t) => null; - - /// - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) { if (foreground is null) { @@ -173,9 +177,9 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) + if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl)) { - Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); + Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl)); } else @@ -184,13 +188,17 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - public IDrawingContextLayerImpl CreateLayer(Size size) + protected override void PushTransformCore(Matrix matrix) { - throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + _transforms ??= TransformStackPool.Get(); + _transforms.Push(Transform); + Transform = matrix * Transform; } + + protected override void PopTransformCore() => + Transform = (_transforms ?? throw new InvalidOperationException()).Pop(); - /// - public void PopClip() + protected override void PopClipCore() { var next = NextDrawAs(); @@ -205,7 +213,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PopGeometryClip() + protected override void PopGeometryClipCore() { var next = NextDrawAs(); @@ -219,8 +227,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopBitmapBlendMode() + protected override void PopBitmapBlendModeCore() { var next = NextDrawAs(); @@ -234,8 +241,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacity() + protected override void PopOpacityCore() { var next = NextDrawAs(); @@ -249,14 +255,16 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PopOpacityMask() + protected override void PopOpacityMaskCore() { + if (!_needsToPopOpacityMask!.Pop()) + return; + var next = NextDrawAs(); if (next == null || !next.Item.Equals(null, null)) { - Add(new OpacityMaskNode()); + Add(new OpacityMaskPopNode()); } else { @@ -264,8 +272,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(Rect clip) + + protected override void PushClipCore(Rect clip) { var next = NextDrawAs(); @@ -279,8 +287,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushClip(RoundedRect clip) + protected override void PushClipCore(RoundedRect clip) { var next = NextDrawAs(); @@ -294,26 +301,24 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushGeometryClip(IGeometryImpl? clip) + protected override void PushGeometryClipCore(Geometry clip) { - if (clip is null) + if (clip.PlatformImpl is null) return; var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, clip)) + if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl)) { - Add(new GeometryClipNode(Transform, clip)); + Add(new GeometryClipNode(Transform, clip.PlatformImpl)); } else { ++_drawOperationIndex; } } - - /// - public void PushOpacity(double opacity, Rect bounds) + + protected override void PushOpacityCore(double opacity, Rect bounds) { var next = NextDrawAs(); @@ -327,23 +332,30 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } } - /// - public void PushOpacityMask(IBrush mask, Rect bounds) + protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) { var next = NextDrawAs(); + bool needsToPop = true; if (next == null || !next.Item.Equals(mask, bounds)) { - Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); + var immutableMask = ConvertBrush(mask); + if (immutableMask != null) + Add(new OpacityMaskNode(immutableMask, bounds)); + else + needsToPop = false; } else { ++_drawOperationIndex; } + + _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get(); + _needsToPopOpacityMask.Push(needsToPop); } /// - public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + protected override void PushBitmapBlendModeCore(BitmapBlendingMode blendingMode) { var next = NextDrawAs(); @@ -378,29 +390,12 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW : null; } - private static IDisposable? CreateChildScene(IBrush? brush) + private IImmutableBrush? ConvertBrush(IBrush? brush) { - if (brush is VisualBrush visualBrush) - { - var visual = visualBrush.Visual; - - if (visual != null) - { - // TODO: This is a temporary solution to make visual brush to work like it does with DeferredRenderer - // We should directly reference the corresponding CompositionVisual (which should - // be attached to the same composition target) like UWP does. - // Render-able visuals shouldn't be dangling unattached - (visual as IVisualBrushInitialize)?.EnsureInitialized(); - - var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(null); - ImmediateRenderer.Render(visual, new DrawingContext(recorder)); - var drawList = recorder.EndUpdate(); - drawList.Size = visual.Bounds.Size; - - return drawList; - } - } - return null; + if (brush is IMutableBrush mutable) + return mutable.ToImmutable(); + if (brush is ISceneBrush sceneBrush) + return sceneBrush.CreateContent(); + return (IImmutableBrush?)brush; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 08e506536f..eaa9a70ca0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -21,19 +21,10 @@ namespace Avalonia.Rendering.Composition.Server; internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDrawingContextImpl _impl; - private readonly VisualBrushRenderer _visualBrushRenderer; - public CompositorDrawingContextProxy(IDrawingContextImpl impl, VisualBrushRenderer visualBrushRenderer) + public CompositorDrawingContextProxy(IDrawingContextImpl impl) { _impl = impl; - _visualBrushRenderer = visualBrushRenderer; - } - - // This is a hack to make it work with the current way of handling visual brushes - public CompositionDrawList? VisualBrushDrawList - { - get => _visualBrushRenderer.VisualBrushDrawList; - set => _visualBrushRenderer.VisualBrushDrawList = value; } public Matrix PostTransform { get; set; } = Matrix.Identity; @@ -157,24 +148,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont } public object? GetFeature(Type t) => _impl.GetFeature(t); - - public class VisualBrushRenderer : IVisualBrushRenderer - { - public CompositionDrawList? VisualBrushDrawList { get; set; } - public Size GetRenderTargetSize(IVisualBrush brush) - { - return VisualBrushDrawList?.Size ?? default; - } - - public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (VisualBrushDrawList != null) - { - foreach (var cmd in VisualBrushDrawList) - cmd.Item.Render(context); - } - } - } + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 63ec8d756b..977acd8470 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -151,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server Readback.CompleteWrite(Revision); _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext(null)) + using (var targetContext = _renderTarget.CreateDrawingContext()) { var layerSize = Size * Scaling; if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) @@ -165,12 +165,11 @@ namespace Avalonia.Rendering.Composition.Server if (!_dirtyRect.IsDefault) { - var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); - using (var context = _layer.CreateDrawingContext(visualBrushHelper)) + using (var context = _layer.CreateDrawingContext()) { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); + Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); context.PopClip(); } } diff --git a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs b/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs deleted file mode 100644 index f5312ad39b..0000000000 --- a/src/Avalonia.Base/Rendering/IVisualBrushRenderer.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Avalonia.Media; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.Rendering -{ - /// - /// Defines a renderer used to render a visual brush to a bitmap. - /// - [Unstable] - public interface IVisualBrushRenderer - { - /// - /// Gets the size of the intermediate render target to which the visual brush should be - /// drawn. - /// - /// The visual brush. - /// The size of the intermediate render target to create. - Size GetRenderTargetSize(IVisualBrush brush); - - /// - /// Renders a visual brush to a bitmap. - /// - /// The drawing context to render to. - /// The visual brush. - /// A bitmap containing the rendered brush. - void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush); - } -} diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 09d2d55ce3..4a12e78817 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -14,19 +14,8 @@ namespace Avalonia.Rendering /// a simple tree traversal. /// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush /// - internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer + internal class ImmediateRenderer { - /// - /// Renders a visual to a render target. - /// - /// The visual. - /// The render target. - public static void Render(Visual visual, IRenderTarget target) - { - using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer())); - Render(context, visual, visual.Bounds); - } - /// /// Renders a visual to a drawing context. /// @@ -36,28 +25,6 @@ namespace Avalonia.Rendering { Render(context, visual, visual.Bounds); } - - - /// - Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) - { - (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? default; - } - - /// - void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) - { - if (brush.Visual is { } visual) - { - Render(new DrawingContext(context), visual, visual.Bounds); - } - } - - internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) - { - Render(context, visual, visual.Bounds); - } private static Rect GetTransformedBounds(Visual visual) { @@ -75,7 +42,7 @@ namespace Avalonia.Rendering } - private static void Render(DrawingContext context, Visual visual, Rect clipRect) + public static void Render(DrawingContext context, Visual visual, Rect clipRect) { var opacity = visual.Opacity; var clipToBounds = visual.ClipToBounds; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs index e81966ce81..62fc73db44 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs @@ -8,22 +8,19 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : DrawOperation + internal abstract class BrushDrawOperation : DrawOperationWithTransform { - public BrushDrawOperation(Rect bounds, Matrix transform, IDisposable? aux) + public IImmutableBrush? Brush { get; } + + public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush) : base(bounds, transform) { - Aux = aux; + Brush = brush; } - /// - /// Auxiliary data required to draw the brush - /// - public IDisposable? Aux { get; } - public override void Dispose() { - Aux?.Dispose(); + (Brush as ISceneBrushContent)?.Dispose(); base.Dispose(); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index e1bfaa4aa3..782e287989 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a clip push or pop. /// - internal class ClipNode : IDrawOperation + internal class ClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -70,8 +70,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index b7311936d3..ff2616bfe4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -4,30 +4,19 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperation + internal sealed class CustomDrawOperation : DrawOperationWithTransform { - public Matrix Transform { get; } public ICustomDrawOperation Custom { get; } public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) : base(custom.Bounds, transform) { - Transform = transform; Custom = custom; } - public override bool HitTest(Point p) - { - if (Transform.HasInverse) - { - return Custom.HitTest(p * Transform.Invert()); - } - - return false; - } + public override bool HitTest(Point p) => Custom.HitTest(p); public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; Custom.Render(context); } @@ -37,8 +26,28 @@ namespace Avalonia.Rendering.SceneGraph Transform == transform && Custom?.Equals(custom) == true; } - public interface ICustomDrawOperation : IDrawOperation, IEquatable + public interface ICustomDrawOperation : IEquatable, IDisposable { - + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs index c49e7705e0..5b93cd8cfc 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs @@ -28,4 +28,14 @@ namespace Avalonia.Rendering.SceneGraph { } } + + internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform + { + protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform) + { + Transform = transform; + } + + public Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs index 4600653b9d..d5f0270cb2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs @@ -14,33 +14,20 @@ namespace Avalonia.Rendering.SceneGraph { public EllipseNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - Rect rect, - IDisposable? aux = null) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, aux) + Rect rect) + : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; } - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// public ImmutablePen? Pen { get; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the rect of the ellipse to draw. /// @@ -54,21 +41,10 @@ namespace Avalonia.Rendering.SceneGraph rect.Equals(Rect); } - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawEllipse(Brush, Pen, Rect); - } + public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); public override bool HitTest(Point p) { - if (!Transform.TryInvert(out Matrix inverted)) - { - return false; - } - - p *= inverted; - var center = Rect.Center; var strokeThickness = Pen?.Thickness ?? 0; @@ -112,5 +88,10 @@ namespace Avalonia.Rendering.SceneGraph return false; } + + public override void Dispose() + { + (Brush as ISceneBrushContent)?.Dispose(); + } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 82f8fc2d56..e1f79e0e10 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a rectangle draw. /// - internal class ExperimentalAcrylicNode : DrawOperation + internal class ExperimentalAcrylicNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,16 +22,10 @@ namespace Avalonia.Rendering.SceneGraph RoundedRect rect) : base(rect.Rect, transform) { - Transform = transform; Material = material.ToImmutable(); Rect = rect; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - public IExperimentalAcrylicMaterial Material { get; } /// @@ -60,8 +54,6 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if(context is IDrawingContextWithAcrylicLikeSupport idc) { idc.DrawRectangle(Material, Rect); @@ -73,18 +65,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) - { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) - { - p *= Transform.Invert(); - - var rect = Rect.Rect; - return rect.ContainsExclusive(p); - } - - return false; - } + public override bool HitTest(Point p) => Rect.Rect.ContainsExclusive(p); } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 842edf2bcb..8575e61de4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a geometry clip push or pop. /// - internal class GeometryClipNode : IDrawOperation + internal class GeometryClipNode : IDrawOperationWithTransform { /// /// Initializes a new instance of the class that represents a @@ -58,8 +58,6 @@ namespace Avalonia.Rendering.SceneGraph /// public void Render(IDrawingContextImpl context) { - context.Transform = Transform; - if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs index cf53b86fa7..3ab535897a 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs @@ -19,28 +19,15 @@ namespace Avalonia.Rendering.SceneGraph /// The geometry. /// Auxiliary data required to draw the brush. public GeometryNode(Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, - IGeometryImpl geometry, - IDisposable? aux) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, aux) + IGeometryImpl geometry) + : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Geometry = geometry; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -74,21 +61,14 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } /// public override bool HitTest(Point p) { - if (Transform.HasInverse) - { - p *= Transform.Invert(); - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - - return false; + return (Brush != null && Geometry.FillContains(p)) || + (Pen != null && Geometry.StrokeContains(Pen, p)); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index a2d914bdd7..4d8759f545 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -19,37 +19,21 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, - IBrush foreground, - IRef glyphRun, - IDisposable? aux = null) - : base(new Rect(glyphRun.Item.Size), transform, aux) + IImmutableBrush foreground, + IRef glyphRun) + : base(new Rect(glyphRun.Item.Size), transform, foreground) { - Transform = transform; - Foreground = foreground.ToImmutable(); GlyphRun = glyphRun.Clone(); } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the foreground brush. - /// - public IBrush Foreground { get; } - + + /// /// Gets the glyph run to draw. /// public IRef GlyphRun { get; } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - context.DrawGlyphRun(Foreground, GlyphRun); - } + public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); /// /// Determines if this draw operation equals another. @@ -65,16 +49,17 @@ namespace Avalonia.Rendering.SceneGraph internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && - Equals(foreground, Foreground) && + Equals(foreground, Brush) && Equals(glyphRun.Item, GlyphRun.Item); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); public override void Dispose() { GlyphRun?.Dispose(); + base.Dispose(); } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 2bfd2080c3..6a1aefe6b2 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IDrawOperation : IDisposable + internal interface IDrawOperation : IDisposable { /// /// Gets the bounds of the visible content in the node in global coordinates. @@ -30,4 +30,12 @@ namespace Avalonia.Rendering.SceneGraph /// The drawing context. void Render(IDrawingContextImpl context); } + + internal interface IDrawOperationWithTransform : IDrawOperation + { + /// + /// Gets the transform with which the node will be drawn. + /// + Matrix Transform { get; } + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs index 339881e675..dd9787e8d1 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : DrawOperation + internal class ImageNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -21,19 +21,13 @@ namespace Avalonia.Rendering.SceneGraph public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform) { - Transform = transform; Source = source.Clone(); Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; BitmapInterpolationMode = bitmapInterpolationMode; SourceVersion = Source.Item.Version; - } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } + } /// /// Gets the image to draw. @@ -68,14 +62,6 @@ namespace Avalonia.Rendering.SceneGraph /// public BitmapInterpolationMode BitmapInterpolationMode { get; } - /// - /// The bitmap blending mode. - /// - /// - /// The blending mode. - /// - public BitmapBlendingMode BitmapBlendingMode { get; } - /// /// Determines if this draw operation equals another. /// @@ -104,12 +90,11 @@ namespace Avalonia.Rendering.SceneGraph /// public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawBitmap(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// - public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + public override bool HitTest(Point p) => DestRect.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 0af8ba2752..f21791d038 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -8,7 +8,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents a line draw. /// - internal class LineNode : BrushDrawOperation + internal class LineNode : DrawOperationWithTransform { /// /// Initializes a new instance of the class. @@ -22,21 +22,14 @@ namespace Avalonia.Rendering.SceneGraph Matrix transform, IPen pen, Point p1, - Point p2, - IDisposable? aux = null) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform, aux) + Point p2) + : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) { - Transform = transform; Pen = pen.ToImmutable(); P1 = p1; P2 = p2; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - /// /// Gets the stroke pen. /// @@ -71,17 +64,11 @@ namespace Avalonia.Rendering.SceneGraph public override void Render(IDrawingContextImpl context) { - context.Transform = Transform; context.DrawLine(Pen, P1, P2); } public override bool HitTest(Point p) { - if (!Transform.HasInverse) - return false; - - p *= Transform.Invert(); - var halfThickness = Pen.Thickness / 2; var minX = Math.Min(P1.X, P2.X) - halfThickness; var maxX = Math.Max(P1.X, P2.X) + halfThickness; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 3ecc07fa54..e10d712c2d 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -18,27 +18,12 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity mask to push. /// The bounds of the mask. /// Auxiliary data required to draw the brush. - public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(default, Matrix.Identity, aux) + public OpacityMaskNode(IImmutableBrush mask, Rect bounds) + : base(default, Matrix.Identity, mask) { - Mask = mask.ToImmutable(); MaskBounds = bounds; } - /// - /// Initializes a new instance of the class that represents an - /// opacity mask pop. - /// - public OpacityMaskNode() - : base(default, Matrix.Identity, null) - { - } - - /// - /// Gets the mask to be pushed or null if the operation represents a pop. - /// - public IBrush? Mask { get; } - /// /// Gets the bounds of the opacity mask or null if the operation represents a pop. /// @@ -58,19 +43,23 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IBrush? mask, Rect? bounds) => Mask == mask && MaskBounds == bounds; + public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; /// public override void Render(IDrawingContextImpl context) { - if (Mask != null) - { - context.PushOpacityMask(Mask, MaskBounds!.Value); - } - else - { - context.PopOpacityMask(); - } + context.PushOpacityMask(Brush!, MaskBounds!.Value); } } + + internal class OpacityMaskPopNode : DrawOperation + { + public OpacityMaskPopNode() : base(default, Matrix.Identity) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) => context.PopOpacityMask(); + } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs index f2ffd7411c..cee9ce9df7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs @@ -23,30 +23,17 @@ namespace Avalonia.Rendering.SceneGraph /// Auxiliary data required to draw the brush. public RectangleNode( Matrix transform, - IBrush? brush, + IImmutableBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows, - IDisposable? aux = null) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, aux) + BoxShadows boxShadows) + : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) { - Transform = transform; - Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); Rect = rect; BoxShadows = boxShadows; } - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - /// Gets the fill brush. - /// - public IBrush? Brush { get; } - /// /// Gets the stroke pen. /// @@ -85,35 +72,22 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override void Render(IDrawingContextImpl context) - { - context.Transform = Transform; - - context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - } + public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); /// public override bool HitTest(Point p) { - // TODO: This doesn't respect CornerRadius yet. - if (Transform.HasInverse) + if (Brush != null) { - p *= Transform.Invert(); - - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); - return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); - } + var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); + return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); } - - return false; } } } diff --git a/src/Avalonia.Base/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/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/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.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5b84ceef7f..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(); } @@ -491,7 +491,7 @@ namespace Avalonia.Headless } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { return new HeadlessDrawingContextStub(); } 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/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 969f0b5e2a..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 { @@ -25,7 +27,6 @@ namespace Avalonia.Skia private readonly Stack _opacityStack = new(); private readonly Stack _blendingModeStack = new(); private readonly Matrix? _postTransform; - private readonly IVisualBrushRenderer? _visualBrushRenderer; private double _currentOpacity = 1.0f; private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; @@ -61,12 +62,7 @@ namespace Avalonia.Skia /// Dpi of drawings. /// public Vector Dpi; - - /// - /// Visual brush renderer. - /// - public IVisualBrushRenderer? VisualBrushRenderer; - + /// /// Render text without Lcd rendering. /// @@ -141,7 +137,6 @@ namespace Avalonia.Skia ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); _dpi = createInfo.Dpi; - _visualBrushRenderer = createInfo.VisualBrushRenderer; _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; @@ -908,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)); @@ -970,36 +965,98 @@ namespace Avalonia.Skia } } - /// - /// Configure paint wrapper to use visual brush. - /// - /// Paint wrapper. - /// Visual brush. - /// Visual brush renderer. - /// Tile brush image. - private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush visualBrush, - IVisualBrushRenderer? visualBrushRenderer, ref IDrawableBitmapImpl? tileBrushImage) + private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) { - if (visualBrushRenderer == null) - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } - - var intermediateSize = visualBrushRenderer.GetRenderTargetSize(visualBrush); + if(content.UseScalableRasterization) + ConfigureSceneBrushContentWithPicture(ref paintWrapper, content, targetSize); + else + ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetSize); + } + + private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { - var intermediate = CreateRenderTarget(intermediateSize, false); + using var intermediate = CreateRenderTarget(intermediateSize, false); - using (var ctx = intermediate.CreateDrawingContext(visualBrushRenderer)) + using (var ctx = intermediate.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); - - visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + content.Render(ctx, rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } - tileBrushImage = intermediate; - paintWrapper.AddDisposable(tileBrushImage); + ConfigureTileBrush(ref paintWrapper, targetSize, content.Brush, intermediate); + } + } + + private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, + Size targetSize) + { + var rect = content.Rect; + var contentSize = rect.Size; + if (contentSize.Width <= 0 || contentSize.Height <= 0) + { + paintWrapper.Paint.Color = SKColor.Empty; + return; + } + + var tileBrush = content.Brush; + var transform = rect.TopLeft == default ? Matrix.Identity : Matrix.CreateTranslation(-rect.X, -rect.Y); + + var calc = new TileBrushCalculator(tileBrush, contentSize, targetSize); + transform *= calc.IntermediateTransform; + + using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); + using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) + { + ctx.PushClip(calc.IntermediateClip); + content.Render(ctx, transform); + ctx.PopClip(); + } + + using var picture = pictureTarget.GetPicture(); + + var paintTransform = + tileBrush.TileMode != TileMode.None + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); + + SKShaderTileMode tileX = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipX || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + SKShaderTileMode tileY = + tileBrush.TileMode == TileMode.None + ? SKShaderTileMode.Clamp + : tileBrush.TileMode == TileMode.FlipY || tileBrush.TileMode == TileMode.FlipXY + ? SKShaderTileMode.Mirror + : SKShaderTileMode.Repeat; + + paintTransform = SKMatrix.Concat(paintTransform, + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + + if (tileBrush.Transform is { }) + { + var origin = tileBrush.TransformOrigin.ToPixels(targetSize); + var offset = Matrix.CreateTranslation(origin); + var brushTransform = (-offset) * tileBrush.Transform.Value * (offset); + + paintTransform = paintTransform.PreConcat(brushTransform.ToSKMatrix()); + } + + using (var shader = picture.ToShader(tileX, tileY, paintTransform, + new SKRect(0, 0, picture.CullRect.Width, picture.CullRect.Height))) + { + paintWrapper.Paint.FilterQuality = SKFilterQuality.None; + paintWrapper.Paint.Shader = shader; } } @@ -1113,12 +1170,25 @@ namespace Avalonia.Skia } var tileBrush = brush as ITileBrush; - var visualBrush = brush as IVisualBrush; var tileBrushImage = default(IDrawableBitmapImpl); - if (visualBrush != null) + if (brush is ISceneBrush sceneBrush) { - ConfigureVisualBrush(ref paintWrapper, visualBrush, _visualBrushRenderer, ref tileBrushImage); + using (var content = sceneBrush.CreateContent()) + { + if (content != null) + { + ConfigureSceneBrushContent(ref paintWrapper, content, targetSize); + return paintWrapper; + } + else + paint.Color = default; + } + } + else if (brush is ISceneBrushContent sceneBrushContent) + { + ConfigureSceneBrushContent(ref paintWrapper, sceneBrushContent, targetSize); + return paintWrapper; } else { diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index f1216100bc..a22b67e09e 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -36,7 +36,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var framebuffer = _platformSurface.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, @@ -55,7 +55,6 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true }; diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 7f9108481d..797c565ca1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Skia _renderTarget.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var session = _renderTarget.BeginRenderingSession(); @@ -31,7 +31,6 @@ namespace Avalonia.Skia GrContext = session.GrContext, Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = true, Gpu = _skiaGpu, CurrentSession = session diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index 4d8afe9830..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/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/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index f88e74d738..92210c30e2 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -97,7 +97,7 @@ namespace Avalonia.Skia } /// - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { _canvas.RestoreToCount(-1); _canvas.ResetMatrix(); @@ -106,7 +106,6 @@ namespace Avalonia.Skia { Surface = _surface.Surface, Dpi = Dpi, - VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, GrContext = _grContext, Gpu = _gpu, diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 02932c52da..10f9239b1a 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1 _externalRenderTargetProvider.DestroyRenderTarget(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, null, target, null, () => + return new DrawingContextImpl( null, target, null, () => { try { diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 984a24fb30..0af326d6a8 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -22,7 +22,7 @@ namespace Avalonia.Direct2D1 { } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var locked = _surface.Lock(); if (locked.Format == PixelFormat.Rgb565) @@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1 } return new FramebufferShim(locked) - .CreateDrawingContext(visualBrushRenderer); + .CreateDrawingContext(); } public bool IsCorrupted => false; @@ -47,9 +47,9 @@ namespace Avalonia.Direct2D1 _target = target; } - public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public override IDrawingContextImpl CreateDrawingContext() { - return base.CreateDrawingContext(visualBrushRenderer, () => + return base.CreateDrawingContext(() => { using (var l = WicImpl.Lock(BitmapLockFlags.Read)) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 0dd9c155bb..f9b5953e3f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -19,7 +19,6 @@ namespace Avalonia.Direct2D1.Media /// internal class DrawingContextImpl : IDrawingContextImpl { - private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly DeviceContext _deviceContext; @@ -39,13 +38,11 @@ namespace Avalonia.Direct2D1.Media /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( - IVisualBrushRenderer visualBrushRenderer, ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { - _visualBrushRenderer = visualBrushRenderer; _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; @@ -491,7 +488,8 @@ namespace Avalonia.Direct2D1.Media var radialGradientBrush = brush as IRadialGradientBrush; var conicGradientBrush = brush as IConicGradientBrush; var imageBrush = brush as IImageBrush; - var visualBrush = brush as IVisualBrush; + var sceneBrush = brush as ISceneBrush; + var sceneBrushContent = brush as ISceneBrushContent; if (solidColorBrush != null) { @@ -518,11 +516,13 @@ namespace Avalonia.Direct2D1.Media (BitmapImpl)imageBrush.Source.PlatformImpl.Item, destinationSize); } - else if (visualBrush != null) + else if (sceneBrush != null || sceneBrushContent != null) { - if (_visualBrushRenderer != null) + sceneBrushContent ??= sceneBrush.CreateContent(); + if (sceneBrushContent != null) { - var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + var rect = sceneBrushContent.Rect; + var intermediateSize = rect.Size; if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) { @@ -533,28 +533,26 @@ namespace Avalonia.Direct2D1.Media var pixelSize = PixelSize.FromSizeWithDpi(intermediateSize, dpi); using (var intermediate = new BitmapRenderTarget( - _deviceContext, - CompatibleRenderTargetOptions.None, - pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) + _deviceContext, + CompatibleRenderTargetOptions.None, + pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) { - using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + sceneBrushContent.Render(ctx, + rect.TopLeft == default ? null : Matrix.CreateTranslation(-rect.X, -rect.Y)); } return new ImageBrushImpl( - visualBrush, + sceneBrushContent.Brush, _deviceContext, new D2DBitmapImpl(intermediate.Bitmap), destinationSize); } + } } - else - { - throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); - } } return new SolidColorBrushImpl(null, _deviceContext); diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index 829b887d9d..a08c96c40c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -95,7 +95,7 @@ namespace Avalonia.Direct2D1.Media CompatibleRenderTargetOptions.None, calc.IntermediateSize.ToSharpDX()); - using (var context = new RenderTarget(result).CreateDrawingContext(null)) + using (var context = new RenderTarget(result).CreateDrawingContext()) { var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2dbc1d67d1..6b1ca911fb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -30,9 +30,9 @@ namespace Avalonia.Direct2D1.Media.Imaging return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); + return new DrawingContextImpl( this, _renderTarget, null, () => Version++); } public bool IsCorrupted => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index d6b1e618e5..fa40e75fa7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - => CreateDrawingContext(visualBrushRenderer, null); + public virtual IDrawingContextImpl CreateDrawingContext() + => CreateDrawingContext(null); public bool IsCorrupted => false; - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) + public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) { - return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => + return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => { Version++; finishedCallback?.Invoke(); diff --git a/src/Windows/Avalonia.Direct2D1/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/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.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.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index 05e160dca8..4ba0c82b87 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -76,7 +76,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media var r = Avalonia.AvaloniaLocator.Current.GetRequiredService(); using(var cpuContext = r.CreateBackendContext(null)) using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) - using (var ctx = target.CreateDrawingContext(null)) + using (var ctx = target.CreateDrawingContext()) { ctx.Clear(Colors.Transparent); ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); @@ -90,7 +90,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media fb.Deallocate(); using (var rtb = new RenderTargetBitmap(new PixelSize(100, 100), new Vector(96, 96))) { - using (var ctx = rtb.CreateDrawingContext(null)) + using (var ctx = rtb.CreateDrawingContext()) { ctx.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 100, 100)); ctx.DrawRectangle(Brushes.Pink, null, new Rect(0, 20, 100, 10)); diff --git a/tests/Avalonia.RenderTests/Media/TileBrushTests.cs b/tests/Avalonia.RenderTests/Media/TileBrushTests.cs new file mode 100644 index 0000000000..c171573be7 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/TileBrushTests.cs @@ -0,0 +1,94 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests; +#else +namespace Avalonia.Direct2D1.RenderTests.Media; +#endif +public class DrawingBrushTests: TestBase +{ + public DrawingBrushTests() + : base(@"Media\DrawingBrush") + { + } + + [Fact] + public async Task DrawingBrushIsProperlyTiled() + { + Decorator target = new Decorator + { + Padding = new Thickness(10), + Width = 220, + Height = 220, + Child = new Rectangle + { + Fill = new DrawingBrush + { + Stretch = Stretch.None, + TileMode = TileMode.Tile, + Drawing = CreateDrawing(), + DestinationRect = new RelativeRect(0,0,0.25,0.25, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + +#if AVALONIA_SKIA + [Fact] +#endif + public async Task DrawingBrushIsProperlyUpscaled() + { + Decorator target = new Decorator + { + Padding = new Thickness(10), + Width = 420, + Height = 420, + Child = new Rectangle + { + Fill = new DrawingBrush + { + Stretch = Stretch.Fill, + TileMode = TileMode.None, + Drawing = CreateDrawing() + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + GeometryDrawing CreateDrawing() + { + return new GeometryDrawing + { + Geometry = new GeometryGroup + { + Children = + { + new RectangleGeometry(new Rect(50, 25, 25, 25)), + new RectangleGeometry(new Rect(25, 50, 25, 25)), + } + }, + Pen = new Pen(new LinearGradientBrush() + { + GradientStops = + { + new GradientStop(Colors.Blue, 0), + new GradientStop(Colors.Black, 1), + } + }, 5), + Brush = Brushes.Yellow, + }; + } + +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index fe84659038..df128b8ae3 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -34,15 +34,15 @@ namespace Avalonia.UnitTests } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public IDrawingContextImpl CreateDrawingContext() { var m = new Mock(); m.Setup(c => c.CreateLayer(It.IsAny())) .Returns(() => { var r = new Mock(); - r.Setup(r => r.CreateDrawingContext(It.IsAny())) - .Returns(CreateDrawingContext(null)); + r.Setup(r => r.CreateDrawingContext()) + .Returns(CreateDrawingContext()); return r.Object; } ); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 875c5eb944..c17eeda3e1 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -70,12 +70,12 @@ namespace Avalonia.UnitTests { var layerDc = new Mock(); var layer = new Mock(); - layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); + layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object); return layer.Object; }); var result = new Mock(); - result.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(dc.Object); + result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object); return result.Object; } diff --git a/tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png b/tests/TestFiles/Direct2D1/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..78b6764360bf14d3e787e83cf37f130ad0304f83 GIT binary patch literal 4340 zcmeAS@N?(olHy`uVBq!ia0vp^cR-kf4M<8yoo@tEjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WMyCwnCI!@7*Y}UcJ9S2xs4aw?wYXtn(m~jC{Xn6en)YThN2~h+V#ml&P}?_ zsk*#1==#;y6@N~CI+wM?f3w`%ef&Jer#L*{>+D{=wX?>NL$O7`361FJw7>H^{mJR= z-QEA|e5XB{o$?{6GP?V-*`8^We=MJz_NwAm+w~$LdGF)h)&F~RI5loZ&Ds8NYm8j= zMw9fL*BciL=k@XJ-~9*mBj;WI{rAVmZn_+_QjhwQQvO0$o*~xbx4xN*}B++9!S` z@%aubruxIZEUgni?wwE?>^}GLD~_VWPhA~(6qON5b}W@)E4^X4u5UrKSO1|CKjH$~ zfeI~7Oba^JR;idhy>f-KJCv35f}`N@U-ig1kslXZ4_4&1X0}_skqJNExNXX}MPD3c z%lcL;KbRJjqq3NLFALaAhkOe|k<4yxBdJ#%u0FB{^DHWr&ek=qeEhR>rSb#wIX`YL z=!-aVMevO@pPboi;i*=S1cKcgr2Xn@v|Sx-9HD@r+Rb#YfbUW`TTYJK0@$(x|?Y({R+zux^4B{e1&L0XmZgu6M=LOxTFD#27 z+LFCr2F}={Vc-wusvMYRq<=!pbzxuWio=r~qED^ua*}-=(5~*dZA!FQtJSrIeEwE> zJ1#1@iSJ@7UE?e}kL5?(ZD5Kz8dq|I;{)$@Q(%nmeX-%<$F}DAk`UpHn!^XbeQ;UY zXYt0hoi|T&d8<|0l0xsD?S}%~b-pb+%$C0+b77z4%O49*%IO)D9!b=<%K0Fj7queU zP3oLU<#eq_bu*R~%sumR|KZ@{Qdb1qwtK#uow9zit);fo&DAOTllOXjTVZAUEJEbf z3xih@F2|<@>Eut}c>-d(+u>=Kx_9r7EGQ3|wf6@oIjDd1S3i5@;}&4~pkg@U?c=r(j0v%5nKRp>{|Loqo?ORD>xrF=V+$p%n;vHHnZKT zBn84!Z|h?)kNJ1};}VC(+|3_vab4yEt7XpBeCztM8B{X3AJAQT!^2Iyiu2Wm;uzV+ zdj&}vm&C+VOu$*R>2AR%msx+Af{%X|4t76~Z}D-nqwL0xmC85FWo&Y;scV%UQCtSJ zW6qx+VO>oX$GCmKvKOTNx{O{!i-sI;$>j!bfuRm88MZ7Hah{k4&Hqh%9)4PM;Cw~n zN>E8*GUsqXIako94WC|4oNfADAby$C9(&QFzXL8HXM0V)*`{~BdCl*HMzM;O3VqlD zi3VU6y?!`!(^60_gJw**6^{#n5d@NHeHoW^BS=rzCmNQ6ub;jYam$8qA@K83!Qn{p zwa_8~DTBY0SpzRNo+r$8f6c!B>8TylJ}>{stG-M(N*Kbmo?fGqE0=2vEhpCgcdtG3P(a7*FPc@e?L0sCCw`GX*uML)<4Wc8b9=0I{VaR$zVT?{ zEdF}a)X_{on(2Y1!)T@-&Ge%+{b)@;bF`)(t?5Ub_weR@Gsdt1a1>!eVNA>){`>{D WEZvK~rU6GT7(8A5T-G@yGywoasCd-? literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png b/tests/TestFiles/Skia/Media/DrawingBrush/DrawingBrushIsProperlyTiled.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..1187581a925aaf8972c82989fa757e5284c0d6b0 GIT binary patch literal 3083 zcmeH}c{H2p9>-N}!zI!ML;JdDjf|ye7#+ckEmGT{w=GqwmWCM3T-#exCMu$qNfDzJ z%OF$J&D3_Kqee7CN~IJ-Q&I_SwU&w^q(ttUICJLCzxR)O&ON8kIq&ae{P|$dQasTod zSg}wU5NE*I-`eZ*;l|q^=U2 z61CK#KDMZ|wF{D9W^Hytq5m?3!s>hB-*Oi!xsOKU&ymNynUbv;moFcK{BNor87CM{ zar=vZNAzjIP%*38SVrZ7MxwTOrdrBfT^*Jh8a_|qQe04*V}RiyOOauLDlh|&W3U3I z-@osDyO^n)p0gn8gPz5QZ<+~(O);>d9D}ObAi<1-^NM?dqHw|A{kzHs~`LUg- zmcbS6I9bGP;~{)nY@oB;?{3bpPszJ3nK3aZ-Q2gl(^@Qxg7#$b64ua#5$}g5pKtK) zv4<%UN#n_7DXFQ^WXj`}Hl6{cvVKm;GWp z&bM{;?4&dS-Qv>zjO(0N=|_9>c=ohgQ6kH07ky*Gi@7sVqBqc&efCah6G30`xSmn` zyHoeHjTTT8g$$KunW!(1#_v}Krup#RY^v=|YnJl)Jz&8l^nn6uH#1V-dr4SS8TkF7 z{>NIh*ikcex#4gA<Bm=$%lK#usTkToemj0{d8_oe}Cp)S230(_qB=8 zY)hOtp#rn~eo9~xd287$Ve$Q)p-octKRcyj|0{<9uma7WJ;E;An`d?OMZhi?>aocn z-qGN#3%ng+27vuDMk9D~UC-)j32#zrCHmX=LaCFpNaNjq$lJRqQOvx0C+$DR+(woj#h3hpNPIfrhKG1mKGpnMpnff zrX^iujOJ&l0?SBR@NsNYW<(<`cFWTJoM;mtazD_6dp@&J6^M4qTQ~)DOnwvps298pR3EoNA%WO zBYkWI)-faZp^PKw7An9nBsR)CwTTdsl9Cq8eO=3!wSdni9fQfK-@*!IQGWC6)0c0n z6;LI#(pI@Q9D~4Ru^m8nW9z`tZBe=L3m2d9Yq|qb1T{LfwaYF4M{xZNJzfVDl3mmk zEX2Zz)Fyin+qeg2xGi4mKzj|q@_}5{UOnh`5h>uQ&vGQ+Mg?e$vY*IL=KYw=BXw*w zJXtznM5P?x_T4>-cVu6T-tHdTe|=7w5;aVI$_ZabtZh_Fm5xjk*%y==suBsoHLq*k zfrCrTX_TU7TeDzmcOxxpipVC&IO`A>8t%fCNEXiy+~tLFrz1Q6PEmv%!iltF83M#F zN&3kIdcLBZqxX8&@c#v+9SLWR)KQza$J~Apgt6ORO5v@ZdME<4rVCbv=mdF}H0I>{ zlpK|EjKK9^s3)+2Ccb$p@~p9p*MG1UDzVoeyw*Hn;(K!k5Q2sN4u66<_)lKf$RcjT z-B`1}%=9J14v6zO2M!WpD5p4<(KbXEYm~rbQKFC&3n`f+de>bf3mub{=_rYwXPdlE z=xj2jz_Kr4=*GzvE3c}JAcmN^EFiMm8ELUod%vPtsso_Xh4rh0_&d7FFx0D9`)14- z8P@^P+Qz^6S~SUYLszH$+N(*|q?iF%eYg2Ps1nf_$PXB;bLcGQ4zzFy{)phl@tDNY z{$CF^|J*z8ZM#v?@5~br(Wf9TS{%9Jw*gto2SZtMM#>F-3<=P4dX z6uU**%T-NG{(5xb?Tr?FP6-{4101*DTRZvXgxH@Cn( zsYP(t*fI4j8zhZhn#VpEy?9+vBo7pQ^9dRh4I)h1jAL9Trurt6YCbg|Wk_dPlk6d0 zW>SJ_m##XGaeImnbQ#L!Rpud_d_+u^D`wOqyue5%|sAK=j5lCrBc1pJKqgVKtP~KiJ zUfpn!*tE{@YQa%s-HJf_melh-Q@8>WVc(dKNk9uX1HMoh{qx5$9jyzOCYYv-yzwaP zwwnR}#ILk=0YZ7E&U7GkAO%QeUYul_`ok9<8)vVNs~-=jCO7l27QhONFIF|5hYv!j z`*Yb@z{-Wag_*@4da5dxydkFPURXw=@H)$eSpbBF_7R7GzZC9*l7+o~@{p(?fJ7jM zw5dHzHp?B@+Zam8YZ&lVjM@^1bTRKz>?uflI+okJ74z({s!Cop*=rMQZ6M`CrI;M& zVXW)c1pC;Lmf}PhwZ6CK-X1X>(v%8mTC1{5W!bgizP*hcUx2jA`kqyPuA)@~|M8Ij zrnd&>2ZmY8SS7HML1*YrMssA{;6>O&ADM@D$l;G5CARi?R0u>VvlROAY`Jgzy8u&kr~mu)um^6BdMOo3ZW?tdY=sJ<*1V{k$Dy4@IlS zH{K#sdIz~c-)Umgov!P8M*g#kJq=cK!P1ck-1ZLjPA<w2CaRWBe|#0Azna_X?~;6w755tnk1fL! zQN?+HW?aqo)(TdEj@Et$6n4$cw=zm%F(s>VS#1(32tzKbDlr3w;;@ilddBl@PZA0Q>YGXU@V1)&M*>Ss#!%~B#y0{sA(Nf)XXA@!Ag1}zTMjz9;KNf$cs#9) z17y)*0+R4vwm|*^clVXKEFEpnTrP*R{MQ`TU{MUp1z?wsBN2anUI}S?=8X5+V~;|0 z)Zv9^3I{#l0O!jdwcbx)W#+THF~Tz?u#vPZRuvO=qT9~qi=Qg%vtzlMG9^XaeY=Eb zEGA$}9l4rrY!ZGhWqA>ogcrtdf`zyO>#XOY>>AyT3ROJT1m$HPm*eMmj*G*UpGXF; zp21k|bOJZK;5^($&EdTOSOO$+2WpIeXH}hzhQREO|KhX|i+ND*3X3zffY366&;mAG zo`A9t2?gLT^oHkS58Lhy6LlS?})Q1qCm*y|9CSo3Icm|uDuBPBU(HRDa zdu&qdj%xLoxA=0S1-{XI4taUJGZ5SWw7N2H=}Z_sj=NGAEH;y|>s03_;!5k?sEH~v zyJ)@>)B~g6V`scf2D2n!v(E)Z{bTrz^_9*~cHoxW1V56UHy^?7+pu1C;fSllS~~|l>&QIU8Kx;(=BoJyH>L(9-MBW*P zGzD?zU9#~zEL0|e`n;aH_<2+oi){G6T?L?j)dUCQs|%L<@FwR$g%z%oS+!e-jgtqi z+-1;;7Ap`cuUcogCKWJQ1>|`$j`?0ks~+NQ2pki_?{Gv}`D}wckUwG01fm1VTJ`_h z$JF3zvU2{XtAFkFg-;_L*KkbE7aP`L&SbH9UJ$9jg+8BU5C?oC;Y~7Zm^_g8%|ER) z-9|-XzdT=XLB;Y4i>&iC*i@NkDyvipsN*02DvCL*EST@e>}uaiaq7I!#8jaK9W&y& zW!TKBW?S0H)O^Xhcyj4#>`xa{#X$cJ$5Izl;1Lq zO^bY#Ilpsz-C9?vLdnLQ(=o*}Zvwk&Bz(Gb>c+2==`{h5S_jrr>6Hf_r43{z4`tji zHmeTkoyXCOYUU=qrE`<>|1fEI7d5#YU7Z>9M)WE_>X#lt$aufi@YdC~bhvXqHCx^9 z|J~nJhlH1W^WgrKfi*=}y!bQkRN-&YX(g$kJJ9p$qnZsVl*)nNM#D^EyP)A$c|47A zH--42F+3rvIcQW6GCZ>(#^4#;SV0Q{67;r~zq&mqL<`2@hMW^<-`vprU4r;#NzDf( zoCVz0hSIdgO!>)i;VlQB<{w<7?a+)M0Np7^ntN( z_OqPow2SBvF*;d`^(dit9H2xHh|G?#u{Y;2i>z>VkoP9aU!;~`Dt>ey0~nwN_Kt=qf|)sfpv`08Yt zND)uNZhQJQI^yhUfze_LoREG$r8-u4Y%yTs!XZIOFC#)WVL&7(t2u??b0i|!Py$7- z&h(;)8u9Vd5!u`VT{@JG$M7qx92Ks^hXW+WV^k`V#4FYEqJmWG-}ec0ftL z=2$17f6YW+YK-su+n%^h-ZA~tIMJn{!qU$6t~pwpPxSEAdt;Z0(@EOVBZ+M@%`Hh= z(Mvx`%+I+leag>w90jvz!OXjZ^M%px&mFY9wA6B7t<`kuOb@!N9r}ts>-@ZOo@Qp^ zLAIF|UMyM`yI_M6C*%E{n55|&CaHmj_Q==jZBqR{LLitzXPvULWiVw-~~ zl}D;I%os9BORAbe9_gXUiC0(#4;mpp>u3jp+WHdaHdhm@*<|WUi=8`M;gRJdF5RS@ zKe>}yPXI>kh=gc;Hl7291fPHqrA}(Z@HXmhRZyzb^*Tg!2NZ{ZstZzLi9;;%c?2*} ze8h#lI!c-Ra1FQWGU!DxEnSJ>pp0F=jHel+H(<8gVY3Hf_MrPK?zcPc$6{t6Ls87T z_g*XUIKY9WAAab!Y-Y)1sZFVdp~)oReWO4fZfnbQJL+5k1iogK{TP1xiu+ms0P&$$ zmAE&m;B{L#W%8xJ`-WVW_~-t%LzKz3u(FfDkVhlsjNxtWdx5nHc-r2_CsuPc4TEsy zo`+X#f{Yjq98WxFT1VR{IT z(RK#t057vRqP7Xkaj_k^Bp1<9?9uaM3rf6Kcc_;mx@c}Wo?dtNWUGTd^o4vaO1y_~ z=&3htGGjo#wgYTJ?+Hda$Gi}_jXoz^)mMPhlyOcD(-Fh&uH{2PVG$0@Tc16Fr&VS+ z6AjA;$k!F%H6E-D>V!7*NYGCQn=?)|y>r8y$}dO3SV#_|mq#7#8-lefaIKsYQ4 zPn@23(-=icS%qwlXvMJH;6^xu?&DhkwwnP5xdFE}SNEqqi?h${Ew{D7izK%WdIAK#WvA6O8ovgKM}AYD0fM0OMHg zW@`vfL;#cpXMs!@UW7@xf$M4hFIyKEst!K5Qk z=EKM(Yko$K_ur74@dT{R2d*7~DGShSmhly!DLM|CDySxMqt|3q#UrBL0+q!BI@*ZR zg@Pi#(B)k5h}Q@6IijY9vnsJv+SG#=U;?dmg@EJ9fD63P$M(zEIo(|)WL(>R)5It2Sm>l71WF?)7=tzi?IDo^SX{n+PNxM0Emldcm zfi=GEQ3#8LJ~Spbe&asc?Q)-AW`;&qC5Qn^5*inUkSZpLA4eu_=vAmX^@J}@q>Bps z6si(yIU=W0#*HXMyVmCN9dD16rf782neYUJlOJ3I4@Ou_Y@%!^0!P1$8b1ty+3N>t z(=eS{-#2wS@E3rkXZr?>(Gc_t(9>AHxUI+uk8q3l0EQhsD$?YHHmGh8)jiU~Q^BYL zWO`hdm4Bp3#R=PmfJJnBgYvO=tAkLk)*8e2Y+0?pwhxi@7GzcNfL7v#!-!E0m=~dM z9{8Z-!Yg_Jt$VKb1+J_?nyD&SIrFjJdW4FiVo!BF9bN9?cm zy){x;&@ewW-q3YyCi{0Qw6wEKuq9jJn_soy12rky^X5JIWggm9(B5v3!|oCrGV|X; CPGgk- literal 0 HcmV?d00001