From 0e9ce1522391ab91a2950848c74ebeb08fff4108 Mon Sep 17 00:00:00 2001 From: jp2masa Date: Sun, 4 Feb 2024 06:10:33 +0000 Subject: [PATCH 1/9] Use soft DataContext synchronization for Reactive UI controls. (#14477) --- .../ReactiveUserControl.cs | 27 +++++++-------- src/Avalonia.ReactiveUI/ReactiveWindow.cs | 34 +++++++------------ 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index b0978dc3f6..94090f2614 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -1,8 +1,4 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.VisualTree; using Avalonia.Controls; using ReactiveUI; @@ -29,7 +25,6 @@ namespace Avalonia.ReactiveUI // This WhenActivated block calls ViewModel's WhenActivated // block if the ViewModel implements IActivatableViewModel. this.WhenActivated(disposables => { }); - this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged); } /// @@ -47,21 +42,23 @@ namespace Avalonia.ReactiveUI set => ViewModel = (TViewModel?)value; } - protected override void OnDataContextChanged(EventArgs e) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnDataContextChanged(e); - ViewModel = DataContext as TViewModel; - } + base.OnPropertyChanged(change); - private void OnViewModelChanged(object? value) - { - if (value == null) + if (change.Property == DataContextProperty) { - ClearValue(DataContextProperty); + if (Object.ReferenceEquals(change.OldValue, ViewModel)) + { + SetCurrentValue(ViewModelProperty, change.NewValue); + } } - else if (DataContext != value) + else if (change.Property == ViewModelProperty) { - DataContext = value; + if (Object.ReferenceEquals(change.OldValue, DataContext)) + { + SetCurrentValue(DataContextProperty, change.NewValue); + } } } } diff --git a/src/Avalonia.ReactiveUI/ReactiveWindow.cs b/src/Avalonia.ReactiveUI/ReactiveWindow.cs index 14e9353096..727c22f016 100644 --- a/src/Avalonia.ReactiveUI/ReactiveWindow.cs +++ b/src/Avalonia.ReactiveUI/ReactiveWindow.cs @@ -1,8 +1,4 @@ using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using Avalonia; -using Avalonia.VisualTree; using Avalonia.Controls; using ReactiveUI; @@ -29,8 +25,6 @@ namespace Avalonia.ReactiveUI // This WhenActivated block calls ViewModel's WhenActivated // block if the ViewModel implements IActivatableViewModel. this.WhenActivated(disposables => { }); - this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged); - this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged); } /// @@ -48,27 +42,23 @@ namespace Avalonia.ReactiveUI set => ViewModel = (TViewModel?)value; } - private void OnDataContextChanged(object? value) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (value is TViewModel viewModel) - { - ViewModel = viewModel; - } - else - { - ViewModel = null; - } - } + base.OnPropertyChanged(change); - private void OnViewModelChanged(object? value) - { - if (value == null) + if (change.Property == DataContextProperty) { - ClearValue(DataContextProperty); + if (Object.ReferenceEquals(change.OldValue, ViewModel)) + { + SetCurrentValue(ViewModelProperty, change.NewValue); + } } - else if (DataContext != value) + else if (change.Property == ViewModelProperty) { - DataContext = value; + if (Object.ReferenceEquals(change.OldValue, DataContext)) + { + SetCurrentValue(DataContextProperty, change.NewValue); + } } } } From b8eec4216712ab177f999dafe64d3d59c4b88064 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 5 Feb 2024 08:23:23 +0000 Subject: [PATCH 2/9] Set default refresh indicator content in control theme (#14443) * Set default refresh indicator content in control theme * Apply composition animation to any RefreshVisualizer content, not just path icons * move refresh indicator initializing to OnPropertyChanged --- .../PullToRefresh/RefreshVisualizer.cs | 112 +++++++++--------- .../Accents/BaseResources.xaml | 1 + .../Controls/RefreshVisualizer.xaml | 19 +-- .../Controls/RefreshVisualizer.xaml | 19 +-- 4 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index ea15aa992d..b7647df444 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -129,70 +129,58 @@ namespace Avalonia.Controls if (_root != null) { - _content = Content as Control; - - if (_content == null) + OnOrientationChanged(); + + if (_root != null && _content != null) { - _content = new PathIcon() - { - Height = DefaultIndicatorSize, - Width = DefaultIndicatorSize, - Name = "PART_Icon" - }; + _root.Children.Insert(0, _content); + _content.VerticalAlignment = Layout.VerticalAlignment.Center; + _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; - _content.Loaded += (s, e) => - { - var composition = ElementComposition.GetElementVisual(_content); + UpdateContent(); + } + } + } - if(composition == null) - return; + private void OnContentLoaded(object? s, RoutedEventArgs e) + { + if (_content == null) + return; + + var composition = ElementComposition.GetElementVisual(_content); - var compositor = composition.Compositor; - composition.Opacity = 0; + if (composition == null) return; - var smoothRotationAnimation - = compositor.CreateScalarKeyFrameAnimation(); - smoothRotationAnimation.Target = "RotationAngle"; - smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); - smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); + var compositor = composition.Compositor; + composition.Opacity = 0; - var opacityAnimation - = compositor.CreateScalarKeyFrameAnimation(); - opacityAnimation.Target = "Opacity"; - opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); - opacityAnimation.Duration = TimeSpan.FromMilliseconds(100); + var smoothRotationAnimation = compositor.CreateScalarKeyFrameAnimation(); + smoothRotationAnimation.Target = "RotationAngle"; + smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); - var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); - offsetAnimation.Target = "Offset"; - offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); - offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + var opacityAnimation = compositor.CreateScalarKeyFrameAnimation(); + opacityAnimation.Target = "Opacity"; + opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + opacityAnimation.Duration = TimeSpan.FromMilliseconds(100); - var scaleAnimation - = compositor.CreateVector3KeyFrameAnimation(); - scaleAnimation.Target = "Scale"; - scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); - scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); - var animation = compositor.CreateImplicitAnimationCollection(); - animation["RotationAngle"] = smoothRotationAnimation; - animation["Offset"] = offsetAnimation; - animation["Scale"] = scaleAnimation; - animation["Opacity"] = opacityAnimation; + var scaleAnimation = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); - composition.ImplicitAnimations = animation; + var animation = compositor.CreateImplicitAnimationCollection(); + animation["RotationAngle"] = smoothRotationAnimation; + animation["Offset"] = offsetAnimation; + animation["Scale"] = scaleAnimation; + animation["Opacity"] = opacityAnimation; - UpdateContent(); - }; - - SetCurrentValue(ContentProperty, _content); - } - else - { - RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false); - } - } - - OnOrientationChanged(); + composition.ImplicitAnimations = animation; UpdateContent(); } @@ -229,6 +217,7 @@ namespace Avalonia.Controls visualizerVisual.Offset = IsPullDirectionVertical ? new Vector3D(visualizerVisual.Offset.X, 0, 0) : new Vector3D(0, visualizerVisual.Offset.Y, 0); + visual.Offset = visualizerVisual.Offset; _content.InvalidateMeasure(); break; case RefreshVisualizerState.Interacting: @@ -353,14 +342,27 @@ namespace Avalonia.Controls } else if (change.Property == ContentProperty) { + if (change.OldValue is Control c) + { + c.Loaded -= OnContentLoaded; + _root?.Children.Remove(c); + } + + _content = change.NewValue as Control; + + if (_content != null) + { + _content.Loaded += OnContentLoaded; + } + if (_root != null && _content != null) { _root.Children.Insert(0, _content); _content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; - } - UpdateContent(); + UpdateContent(); + } } else if (change.Property == OrientationProperty) { diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index 6f12f1d306..5328a71b3c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -13,6 +13,7 @@ 10,6,6,5 20 20 + 24 8,5,8,6 diff --git a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml index a9ca8a0a5c..c5f993bb0e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml @@ -11,19 +11,20 @@ Value="{DynamicResource RefreshVisualizerBackground}"/> + + + - - - - + Background="{TemplateBinding Background}"/> diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml index 09f15e4b2a..fe9f291794 100644 --- a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -13,19 +13,20 @@ Value="{DynamicResource RefreshVisualizerBackground}"/> + + + - - - - + Background="{TemplateBinding Background}"/> From 5fa3ffaeab7e5cd2662ef02d03e34b9d4cb1a489 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Feb 2024 02:00:39 -0800 Subject: [PATCH 3/9] Update BrowserSingleViewLifetime.cs (#14492) --- .../Avalonia.Browser/BrowserSingleViewLifetime.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index 6607bcf539..f534c3f27b 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -18,18 +18,6 @@ internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime, IActi initiallyVisible = null; (visible ? Activated : Deactivated)?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); }); - - // Trigger Activated as an initial state, if web page is visible, and wasn't hidden during initialization. - if (initiallyVisible == true) - { - Dispatcher.UIThread.Invoke(() => - { - if (initiallyVisible == true) - { - Activated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background)); - } - }); - } } public AvaloniaView? View; From 6513f71a7ac248970e26755baa68791e2496ac4c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 5 Feb 2024 21:11:28 +0000 Subject: [PATCH 4/9] fix macos crash at startup by not parsing all app arguments into uris. (#14494) --- native/Avalonia.Native/src/OSX/app.mm | 2 +- .../AvaloniaNativeApplicationPlatform.cs | 11 ++++++++++- src/Avalonia.Native/avn.idl | 3 ++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 7630de1d0b..d3d3d938e9 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -70,7 +70,7 @@ ComPtr _events; { auto array = CreateAvnStringArray(urls); - _events->FilesOpened(array); + _events->UrlsOpened(array); } - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index 3365da7d36..212088d4cd 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -13,12 +13,21 @@ namespace Avalonia.Native void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls) { ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); + } + + void IAvnApplicationEvents.UrlsOpened(IAvnStringArray urls) + { + // Raise the urls opened event to be compatible with legacy behavior. + ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray()); if (Application.Current?.ApplicationLifetime is MacOSClassicDesktopStyleApplicationLifetime lifetime) { foreach (var url in urls.ToStringArray()) { - lifetime.RaiseUrl(new Uri(url)); + if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri)) + { + lifetime.RaiseUrl(uri); + } } } } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 7791426c0b..27da2fc894 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1085,7 +1085,8 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown [uuid(6575b5af-f27a-4609-866c-f1f014c20f79)] interface IAvnApplicationEvents : IUnknown { - void FilesOpened (IAvnStringArray* urls); + void FilesOpened (IAvnStringArray* args); + void UrlsOpened (IAvnStringArray* urls); bool TryShutdown(); void OnReopen (); void OnHide (); From ece2e69fb7631f3f78c35ff5a07f3ba739b74831 Mon Sep 17 00:00:00 2001 From: Lubomir Tetak <50887170+ltetak@users.noreply.github.com> Date: Tue, 6 Feb 2024 01:44:37 +0100 Subject: [PATCH 5/9] Prevent random dispatcher deadlocks (#14229) Co-authored-by: Lubomir Tetak --- src/Avalonia.Base/Threading/Dispatcher.Queue.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs index 151d3d5fd6..a4f48f35f5 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.Queue.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.Queue.cs @@ -194,7 +194,6 @@ public partial class Dispatcher if (Now - backgroundJobExecutionStartedAt.Value > _maximumInputStarvationTime) { - _signaled = true; RequestBackgroundProcessing(); return; } From e0127c610c38701c3af34f580273f6efd78285b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Korczy=C5=84ski?= Date: Tue, 6 Feb 2024 02:18:22 +0000 Subject: [PATCH 6/9] Double BringIntoView in VirtualizingStackPanel (#14419) This fixes a case when elements have different widths and the BringIntoView wants to scroll horizontally due to custom TargetRect Co-authored-by: Max Katz --- .../VirtualizingStackPanel.cs | 6 +++ .../VirtualizingStackPanelTests.cs | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index feb40d7dd9..f702f09e8a 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -430,6 +430,12 @@ namespace Avalonia.Controls root.LayoutManager.ExecuteLayoutPass(); } + // During the previous BringIntoView, the scroll width extent might have been out of date if + // elements have different widths. Because of that, the ScrollViewer might not scroll to the correct offset. + // After the previous BringIntoView, Y offset should be correct and an extra layout pass has been executed, + // hence the width extent should be correct now, and we can try to scroll again. + scrollToElement.BringIntoView(); + _scrollToElement = null; _scrollToIndex = -1; return scrollToElement; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index a34db7d979..b72f2376de 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -6,6 +6,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; @@ -1115,6 +1116,44 @@ namespace Avalonia.Controls.UnitTests Assert.True(container.IsVisible); } + [Fact] + public void ScrollIntoView_With_TargetRect_Outside_Viewport_Should_Scroll_To_Item() + { + using var app = App(); + var items = Enumerable.Range(0, 101).Select(x => new ItemWithHeight(x, x * 100 + 1)); + var itemTemplate = new FuncDataTemplate((x, _) => + new Border + { + Height = 10, + [!Layoutable.WidthProperty] = new Binding("Height"), + }); + var (target, scroll, itemsControl) = CreateTarget( + items: items, + itemTemplate: itemTemplate, + styles: new[] + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Visible), + } + } + }); + itemsControl.ContainerPrepared += (_, ev) => + { + ev.Container.AddHandler(Control.RequestBringIntoViewEvent, (_, e) => + { + var dataContext = e.TargetObject.DataContext as ItemWithHeight; + e.TargetRect = new Rect(dataContext.Height - 50, 0, 50, 10); + }); + }; + + target.ScrollIntoView(100); + + Assert.Equal(9901, scroll.Offset.X); + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() From 7afb5a437e5dc8c44147dbe87e71e19fb7529047 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Feb 2024 21:11:19 -0800 Subject: [PATCH 7/9] MVP of SkiaSharp 3 compat --- .../Compatibility/SkiaCompat.SKCanvas.cs | 33 ++++++++++++ .../Compatibility/SkiaCompat.SKImageFilter.cs | 54 +++++++++++++++++++ .../Compatibility/SkiaCompat.SKMatrix.cs | 40 ++++++++++++++ .../Compatibility/SkiaCompat.SKPath.cs | 17 ++++++ .../Avalonia.Skia/Compatibility/SkiaCompat.cs | 8 +++ .../DrawingContextImpl.Effects.cs | 4 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +-- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- .../Avalonia.Skia/TransformedGeometryImpl.cs | 6 +-- 9 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs create mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKImageFilter.cs create mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs create mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs create mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.cs diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs new file mode 100644 index 0000000000..839877af13 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs @@ -0,0 +1,33 @@ +using System; +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal static unsafe partial class SkiaCompat +{ + public static void CSetMatrix(this SKCanvas canvas, SKMatrix matrix) + { + if (IsSkiaSharp3) + { + NewCall(canvas, matrix); + } + else + { + LegacyCall(canvas, matrix); + } + + static void NewCall(SKCanvas canvas, SKMatrix matrix) + { + var m44 = ToSkMatrix44(matrix); + sk_canvas_set_matrix(canvas.Handle, &m44); + } + + static void LegacyCall(SKCanvas canvas, SKMatrix matrix) => + canvas.SetMatrix(matrix); + } + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + internal static extern unsafe void sk_canvas_set_matrix(IntPtr ccanvas, sk_matrix44_t* cmatrix); + +} diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKImageFilter.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKImageFilter.cs new file mode 100644 index 0000000000..493aa16736 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKImageFilter.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal static partial class SkiaCompat +{ + private static Func? s_sk3FilterBlurFactory; + private static Func? s_sk3FilterDropShadowFactory; + + public static SKImageFilter CreateBlur(float sigmaX, float sigmaY) + { + if (IsSkiaSharp3) return NewCall(sigmaX, sigmaY); + else return LegacyCall(sigmaX, sigmaY); + + static SKImageFilter LegacyCall(float sigmaX, float sigmaY) => SKImageFilter.CreateBlur(sigmaX, sigmaY); + + [DynamicDependency("CreateBlur(System.Single,System.Single)", typeof(SKImageFilter))] + static SKImageFilter NewCall(float sigmaX, float sigmaY) + { + if (s_sk3FilterBlurFactory is null) + { + var method = typeof(SKImageFilter).GetMethod("CreateBlur", new[] { typeof(float), typeof(float) })!; + s_sk3FilterBlurFactory = (Func)Delegate.CreateDelegate(typeof(Func), null, method); + } + + return s_sk3FilterBlurFactory(sigmaX, sigmaY); + } + } + + public static SKImageFilter CreateDropShadow(float dropOffsetX, float dropOffsetY, float sigma, float f, SKColor color) + { + if (IsSkiaSharp3) return NewCall(dropOffsetX, dropOffsetY, sigma, f, color); + else return LegacyCall(dropOffsetX, dropOffsetY, sigma, f, color); + + static SKImageFilter LegacyCall(float dropOffsetX, float dropOffsetY, float sigma, float f, SKColor color) + => SKImageFilter.CreateDropShadow(dropOffsetX, dropOffsetY, sigma, f, color); + + [DynamicDependency("CreateDropShadow(System.Single,System.Single,System.Single,System.Single,SkiaSharp.SKColor)", typeof(SKImageFilter))] + static SKImageFilter NewCall(float dropOffsetX, float dropOffsetY, float sigma, float f, SKColor color) + { + if (s_sk3FilterDropShadowFactory is null) + { + var method = typeof(SKImageFilter).GetMethod("CreateDropShadow", + new[] { typeof(float), typeof(float), typeof(float), typeof(float), typeof(SKColor) })!; + s_sk3FilterDropShadowFactory = (Func) + Delegate.CreateDelegate(typeof(Func), null, method); + } + + return s_sk3FilterDropShadowFactory(dropOffsetX, dropOffsetY, sigma, f, color); + } + } +} diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs new file mode 100644 index 0000000000..16153ed2e5 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal static partial class SkiaCompat +{ + // SkiaSharp 3.0 only: + // https://github.com/mono/skia/blob/83c17a6dee5af2db80af57197627f6fbbe4ad272/include/c/sk_types.h#L177C1-L182C17 + [StructLayout(LayoutKind.Sequential)] + public struct sk_matrix44_t { + public float m00, m01, m02, m03; + public float m10, m11, m12, m13; + public float m20, m21, m22, m23; + public float m30, m31, m32, m33; + } + + public static sk_matrix44_t ToSkMatrix44(SKMatrix matrix) + { + return new sk_matrix44_t + { + m00 = matrix.ScaleX, + m01 = matrix.SkewX, + m02 = 0.0f, + m03 = matrix.TransX, + m10 = matrix.SkewY, + m11 = matrix.ScaleY, + m12 = 0.0f, + m13 = matrix.TransY, + m20 = 0.0f, + m21 = 0.0f, + m22 = 1f, + m23 = 0.0f, + m30 = matrix.Persp0, + m31 = matrix.Persp1, + m32 = 0.0f, + m33 = matrix.Persp2 + }; + } +} diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs new file mode 100644 index 0000000000..7937b8209e --- /dev/null +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.InteropServices; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal static unsafe partial class SkiaCompat +{ + public static void CTransform(this SKPath path, ref SKMatrix matrix) + { + fixed (SKMatrix* m = &matrix) + sk_path_transform(path.Handle, m); + } + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + private static extern void sk_path_transform(IntPtr cpath, SKMatrix* cmatrix); +} diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.cs new file mode 100644 index 0000000000..a09b6452c2 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.cs @@ -0,0 +1,8 @@ +using SkiaSharp; + +namespace Avalonia.Skia; + +internal static partial class SkiaCompat +{ + public static bool IsSkiaSharp3 { get; } = typeof(SKPath).Assembly.GetName().Version?.Major == 3; +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs index babc547209..d9c2c27496 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs @@ -30,7 +30,7 @@ partial class DrawingContextImpl if (blur.Radius <= 0) return null; var sigma = SkBlurRadiusToSigma(blur.Radius); - return SKImageFilter.CreateBlur(sigma, sigma); + return SkiaCompat.CreateBlur(sigma, sigma); } if (effect is IDropShadowEffect drop) @@ -41,7 +41,7 @@ partial class DrawingContextImpl alpha *= _currentOpacity; var color = new SKColor(drop.Color.R, drop.Color.G, drop.Color.B, (byte)Math.Max(0, Math.Min(255, alpha))); - return SKImageFilter.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color); + return SkiaCompat.CreateDropShadow((float)drop.OffsetX, (float)drop.OffsetY, sigma, sigma, color); } return null; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index c3778d8426..2778c664ce 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -116,7 +116,7 @@ namespace Avalonia.Skia { if (!_isDisposed) { - _context.Canvas.SetMatrix(_revertTransform); + _context.Canvas.CSetMatrix(_revertTransform); _context._leased = false; _isDisposed = true; } @@ -273,7 +273,7 @@ namespace Avalonia.Skia { var ac = shadow.Color; - var filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); + var filter = SkiaCompat.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur)); var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity)); paint.Reset(); @@ -752,7 +752,7 @@ namespace Avalonia.Skia transform *= _postTransform.Value; } - Canvas.SetMatrix(transform.ToSKMatrix()); + Canvas.CSetMatrix(transform.ToSKMatrix()); } } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index c695e8ba41..7dce7b764a 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -156,7 +156,7 @@ namespace Avalonia.Skia var oldMatrix = context.Canvas.TotalMatrix; context.Canvas.ResetMatrix(); _surface.Surface.Draw(context.Canvas, 0, 0, null); - context.Canvas.SetMatrix(oldMatrix); + context.Canvas.CSetMatrix(oldMatrix); } } diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index 2dee8c318c..9e74610163 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -20,8 +20,8 @@ namespace Avalonia.Skia var matrix = transform.ToSKMatrix(); var transformedPath = StrokePath = source.StrokePath.Clone(); - transformedPath?.Transform(matrix); - + transformedPath?.CTransform(ref matrix); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; if (ReferenceEquals(source.StrokePath, source.FillPath)) @@ -29,7 +29,7 @@ namespace Avalonia.Skia else if (source.FillPath != null) { FillPath = transformedPath = source.FillPath.Clone(); - transformedPath.Transform(matrix); + transformedPath.CTransform(ref matrix); } } From 82f1f1b11df943aeedb1bb704f8f482d1076e2ec Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Feb 2024 22:34:18 -0800 Subject: [PATCH 8/9] iOS compat --- .../Compatibility/SkiaCompat.SKCanvas.cs | 12 +++++++++--- .../Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs | 9 ++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs index 839877af13..a70f533b2b 100644 --- a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Avalonia.Compatibility; using SkiaSharp; namespace Avalonia.Skia; @@ -20,14 +21,19 @@ internal static unsafe partial class SkiaCompat static void NewCall(SKCanvas canvas, SKMatrix matrix) { var m44 = ToSkMatrix44(matrix); - sk_canvas_set_matrix(canvas.Handle, &m44); + if (OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS()) + sk_canvas_set_matrix_ios(canvas.Handle, &m44); + else + sk_canvas_set_matrix(canvas.Handle, &m44); } - + static void LegacyCall(SKCanvas canvas, SKMatrix matrix) => canvas.SetMatrix(matrix); } [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - internal static extern unsafe void sk_canvas_set_matrix(IntPtr ccanvas, sk_matrix44_t* cmatrix); + internal static extern void sk_canvas_set_matrix(IntPtr ccanvas, sk_matrix44_t* cmatrix); + [DllImport("@rpath/libSkiaSharp.framework/libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + internal static extern void sk_canvas_set_matrix_ios(IntPtr ccanvas, sk_matrix44_t* cmatrix); } diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs index 7937b8209e..afb0d4c63f 100644 --- a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using Avalonia.Compatibility; using SkiaSharp; namespace Avalonia.Skia; @@ -9,9 +10,15 @@ internal static unsafe partial class SkiaCompat public static void CTransform(this SKPath path, ref SKMatrix matrix) { fixed (SKMatrix* m = &matrix) - sk_path_transform(path.Handle, m); + if (OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS()) + sk_path_transform_ios(path.Handle, m); + else + sk_path_transform(path.Handle, m); } [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] private static extern void sk_path_transform(IntPtr cpath, SKMatrix* cmatrix); + + [DllImport("@rpath/libSkiaSharp.framework/libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + private static extern void sk_path_transform_ios(IntPtr cpath, SKMatrix* cmatrix); } From 34c84fafb68de4404dfa01724e6648a709fe8f04 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 5 Feb 2024 22:54:44 -0800 Subject: [PATCH 9/9] Don't use PInvokes, as it breaks Mono AOT --- .../Compatibility/SkiaCompat.SKCanvas.cs | 27 ++++++------- .../Compatibility/SkiaCompat.SKMatrix.cs | 40 ------------------- .../Compatibility/SkiaCompat.SKPath.cs | 40 +++++++++++++------ 3 files changed, 40 insertions(+), 67 deletions(-) delete mode 100644 src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs index a70f533b2b..0483c3ff28 100644 --- a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKCanvas.cs @@ -1,12 +1,14 @@ using System; -using System.Runtime.InteropServices; -using Avalonia.Compatibility; +using System.Diagnostics.CodeAnalysis; using SkiaSharp; namespace Avalonia.Skia; -internal static unsafe partial class SkiaCompat +internal static partial class SkiaCompat { + private delegate void CanvasSetMatrixDelegate(SKCanvas canvas, in SKMatrix matrix); + private static CanvasSetMatrixDelegate? s_canvasSetMatrix; + public static void CSetMatrix(this SKCanvas canvas, SKMatrix matrix) { if (IsSkiaSharp3) @@ -18,22 +20,19 @@ internal static unsafe partial class SkiaCompat LegacyCall(canvas, matrix); } + [DynamicDependency("SetMatrix(SkiaSharp.SKMatrix)", typeof(SKCanvas))] static void NewCall(SKCanvas canvas, SKMatrix matrix) { - var m44 = ToSkMatrix44(matrix); - if (OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS()) - sk_canvas_set_matrix_ios(canvas.Handle, &m44); - else - sk_canvas_set_matrix(canvas.Handle, &m44); + if (s_canvasSetMatrix is null) + { + var method = typeof(SKCanvas).GetMethod("SetMatrix", new[] { typeof(SKMatrix).MakeByRefType() })!; + s_canvasSetMatrix = (CanvasSetMatrixDelegate)Delegate.CreateDelegate(typeof(CanvasSetMatrixDelegate), method); + } + + s_canvasSetMatrix(canvas, matrix); } static void LegacyCall(SKCanvas canvas, SKMatrix matrix) => canvas.SetMatrix(matrix); } - - [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - internal static extern void sk_canvas_set_matrix(IntPtr ccanvas, sk_matrix44_t* cmatrix); - - [DllImport("@rpath/libSkiaSharp.framework/libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - internal static extern void sk_canvas_set_matrix_ios(IntPtr ccanvas, sk_matrix44_t* cmatrix); } diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs deleted file mode 100644 index 16153ed2e5..0000000000 --- a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKMatrix.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Runtime.InteropServices; -using SkiaSharp; - -namespace Avalonia.Skia; - -internal static partial class SkiaCompat -{ - // SkiaSharp 3.0 only: - // https://github.com/mono/skia/blob/83c17a6dee5af2db80af57197627f6fbbe4ad272/include/c/sk_types.h#L177C1-L182C17 - [StructLayout(LayoutKind.Sequential)] - public struct sk_matrix44_t { - public float m00, m01, m02, m03; - public float m10, m11, m12, m13; - public float m20, m21, m22, m23; - public float m30, m31, m32, m33; - } - - public static sk_matrix44_t ToSkMatrix44(SKMatrix matrix) - { - return new sk_matrix44_t - { - m00 = matrix.ScaleX, - m01 = matrix.SkewX, - m02 = 0.0f, - m03 = matrix.TransX, - m10 = matrix.SkewY, - m11 = matrix.ScaleY, - m12 = 0.0f, - m13 = matrix.TransY, - m20 = 0.0f, - m21 = 0.0f, - m22 = 1f, - m23 = 0.0f, - m30 = matrix.Persp0, - m31 = matrix.Persp1, - m32 = 0.0f, - m33 = matrix.Persp2 - }; - } -} diff --git a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs index afb0d4c63f..73561aec34 100644 --- a/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs +++ b/src/Skia/Avalonia.Skia/Compatibility/SkiaCompat.SKPath.cs @@ -1,24 +1,38 @@ using System; -using System.Runtime.InteropServices; -using Avalonia.Compatibility; +using System.Diagnostics.CodeAnalysis; using SkiaSharp; namespace Avalonia.Skia; -internal static unsafe partial class SkiaCompat +internal static partial class SkiaCompat { + private delegate void PathTransformDelegate(SKPath canvas, in SKMatrix matrix); + private static PathTransformDelegate? s_pathTransform; + public static void CTransform(this SKPath path, ref SKMatrix matrix) { - fixed (SKMatrix* m = &matrix) - if (OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS()) - sk_path_transform_ios(path.Handle, m); - else - sk_path_transform(path.Handle, m); - } + if (IsSkiaSharp3) + { + NewCall(path, matrix); + } + else + { + LegacyCall(path, matrix); + } - [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - private static extern void sk_path_transform(IntPtr cpath, SKMatrix* cmatrix); + [DynamicDependency("Transform(SkiaSharp.SKMatrix)", typeof(SKPath))] + static void NewCall(SKPath path, SKMatrix matrix) + { + if (s_pathTransform is null) + { + var method = typeof(SKPath).GetMethod("Transform", new[] { typeof(SKMatrix).MakeByRefType() })!; + s_pathTransform = (PathTransformDelegate)Delegate.CreateDelegate(typeof(PathTransformDelegate), method); + } - [DllImport("@rpath/libSkiaSharp.framework/libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - private static extern void sk_path_transform_ios(IntPtr cpath, SKMatrix* cmatrix); + s_pathTransform(path, matrix); + } + + static void LegacyCall(SKPath path, SKMatrix matrix) => + path.Transform(matrix); + } }