From 9d4be7f45eb615cb3eefe1313ac9ee0821c14c5a Mon Sep 17 00:00:00 2001 From: Xiaotian Gu Date: Wed, 23 Aug 2023 14:06:50 -0400 Subject: [PATCH] fix(shadows): revert double shadow impl reverting: "6d09cf17 fix(shadows): background theming issue" --- .../Controls/Shadows/Shadow.cs | 6 + .../Controls/Shadows/ShadowCollection.cs | 13 +- .../Controls/Shadows/ShadowContainer.Paint.cs | 314 +++++++++++------- .../Shadows/ShadowContainer.Properties.cs | 45 ++- .../Controls/Shadows/ShadowContainer.cs | 189 ++++++----- .../Extensions/DependencyObjectExtensions.cs | 7 +- .../Helpers/VisualTreeHelperEx.cs | 11 +- 7 files changed, 352 insertions(+), 233 deletions(-) diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs index c501ef996..f7f28444e 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/Shadow.cs @@ -1,5 +1,11 @@ using System.ComponentModel; + +#if IS_WINUI using Microsoft.UI.Xaml; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif namespace Uno.Toolkit.UI; diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs index 2a00ebc80..ad1d163e8 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowCollection.cs @@ -1,8 +1,15 @@ -using System; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; namespace Uno.Toolkit.UI; -public class ShadowCollection : ObservableCollection { } +public class ShadowCollection : ObservableCollection +{ + public bool HasInnerShadow() => this.Any(s => s.IsInner); + + public string ToKey(double width, double height, Windows.UI.Color? contentBackground) + => string.Create(CultureInfo.InvariantCulture, $"w{width},h{height}") + + (contentBackground.HasValue ? $",cb{contentBackground.Value}:" : ":") + + string.Join("/", this.Select(x => x.ToKey())); +} diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs index 3dcdb1158..14cdad866 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Paint.cs @@ -1,25 +1,22 @@ -#if false -// We keep that as a reference cause it would be better to use the hardware-accelerated version -#define ANDROID_REFERENTIAL_IMPL -#endif - -using System; +using System; using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.UI.Xaml.Media; -using Microsoft.UI.Xaml.Controls; -using Windows.UI; + using SkiaSharp; -using SkiaSharp.Views.Windows; + +using Windows.UI; + +using Microsoft.Extensions.Logging; using Uno.Extensions; using Uno.Logging; -#if __ANDROID__ && ANDROID_REFERENTIAL_IMPL -using _SKXamlCanvas = SkiaSharp.Views.Windows.SKSwapChainPanel; -using _SKPaintSurfaceEventArgs = SkiaSharp.Views.Windows.SKPaintGLSurfaceEventArgs; +#if IS_WINUI +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Controls; +using SkiaSharp.Views.Windows; #else -using _SKXamlCanvas = SkiaSharp.Views.Windows.SKXamlCanvas; -using _SKPaintSurfaceEventArgs = SkiaSharp.Views.Windows.SKPaintSurfaceEventArgs; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Controls; +using SkiaSharp.Views.UWP; #endif namespace Uno.Toolkit.UI; @@ -28,7 +25,23 @@ public partial class ShadowContainer { private static readonly ILogger _logger = typeof(ShadowContainer).Log(); - private static readonly ShadowsCache Cache = new ShadowsCache(); + private record ShadowInfos(double Width, double Height, bool IsInner, double BlurRadius, double Spread, double X, double Y, Color color) + { + public static readonly ShadowInfos Empty = new ShadowInfos(0, 0, false, 0, 0, 0, 0, new Color()); + + public static ShadowInfos From(Shadow shadow, double width, double height) + { + return new ShadowInfos( + width, + height, + shadow.IsInner, + shadow.BlurRadius, + shadow.Spread, + shadow.OffsetX, + shadow.OffsetY, + Color.FromArgb((byte)(shadow.Color.A * shadow.Opacity), shadow.Color.R, shadow.Color.G, shadow.Color.B)); + } + } private readonly record struct SKShadow( bool IsInner, @@ -43,13 +56,14 @@ public partial class ShadowContainer { public static SKShadow From(Shadow shadow, float width, float height, float cornerRadius, float pixelRatio) { - var blurRadius = (float)shadow.BlurRadius * pixelRatio; + float blurRadius = (float)shadow.BlurRadius * pixelRatio; // Blur sigma conversion taken from flutter source code - var blurSigma = blurRadius > 0 ? blurRadius * 0.57735f + 0.5f : 0f; + float blurSigma = blurRadius > 0 ? blurRadius * 0.57735f + 0.5f : 0f; // Can't use ToSKColor() or we end up with a weird compilation error asking us to reference System.Drawing - var compoundedAlpha = shadow.Color.A * shadow.Opacity; - var color = new SKColor(shadow.Color.R, shadow.Color.G, shadow.Color.B, (byte)compoundedAlpha); + Color windowsUiColor = shadow.Color; + var color = ToSkiaColor(windowsUiColor); + color = color.WithAlpha((byte)(color.Alpha * shadow.Opacity)); return new SKShadow( shadow.IsInner, @@ -60,41 +74,46 @@ public static SKShadow From(Shadow shadow, float width, float height, float corn (float)shadow.Spread * pixelRatio, width, height, - cornerRadius - ); + cornerRadius); } } - private static bool NeedsPaint(ShadowPaintContext context, Shadow[] shadows, double width, double height, float pixelRatio, out bool pixelRatioChanged) + private ShadowInfos[] _shadowInfoArray = Array.Empty(); + private float _currentPixelRatio; + private Color? _currentContentBackgroundColor; + + private bool NeedsPaint(double width, double height, float pixelRatio, out bool pixelRatioChanged) { - var states = new ShadowPaintState(width, height, pixelRatio, ShadowInfo.Snapshot(shadows)); - var needsPaint = states != context.LastPaintState; - pixelRatioChanged = states.PixelRatio != context.LastPaintState?.PixelRatio; + var shadows = Shadows ?? new ShadowCollection(); + var newShadowInfos = shadows.Select(s => ShadowInfos.From(s, width, height)).ToArray(); - context.LastPaintState = states; - return needsPaint; - } + pixelRatioChanged = false; - private void OnPaintSurface(object? sender, _SKPaintSurfaceEventArgs e) - { - var context = sender switch + bool needsPaint = !newShadowInfos.SequenceEqual(_shadowInfoArray); + _shadowInfoArray = newShadowInfos; + + if (pixelRatio != _currentPixelRatio) { - _SKXamlCanvas x when x == _backgroundPaintContext.ShadowHost => _backgroundPaintContext, - _SKXamlCanvas x when x == _foregroundPaintContext.ShadowHost => _foregroundPaintContext, + _currentPixelRatio = pixelRatio; + pixelRatioChanged = needsPaint = true; + } - _ => throw new InvalidOperationException(), - }; + return needsPaint; + } -#if __ANDROID__ && ANDROID_REFERENTIAL_IMPL - if (!context.IsOpacitySet && (context.ShadowHost as Android.Views.ViewGroup)?.GetChildAt(0) is TextureView openGlTexture) +#if false // ANDROID (see comment in ShadowContainer.cs) + private void OnSurfacePainted(object? sender, SKPaintGLSurfaceEventArgs e) + { + if (!_notOpaqueSet && ((ViewGroup)_shadowHost).GetChildAt(0) is TextureView openGlTexture) { openGlTexture.SetOpaque(false); - context.IsOpacitySet = true; + _notOpaqueSet = true; } +#else + private void OnSurfacePainted(object? sender, SKPaintSurfaceEventArgs e) + { #endif - - if (context.ShadowHost == null || - _currentContent is not { ActualHeight: > 0, ActualWidth: > 0 }) + if (_shadowHost == null || _currentContent is not { ActualHeight: > 0, ActualWidth: > 0 }) { return; } @@ -103,12 +122,11 @@ private void OnPaintSurface(object? sender, _SKPaintSurfaceEventArgs e) var surfaceWidth = e.Info.Width; var surfaceHeight = e.Info.Height; - var pixelRatio = (float)(surfaceWidth / context.ShadowHost.Width); - var width = _currentContent.ActualWidth; - var height = _currentContent.ActualHeight; + float pixelRatio = surfaceWidth / (float)_shadowHost.Width; + double width = _currentContent.ActualWidth; + double height = _currentContent.ActualHeight; - var shadows = Shadows?.Where(x => x.IsInner == context.IsInner).ToArray() ?? Array.Empty(); - if (!NeedsPaint(context, shadows, width, height, pixelRatio, out bool pixelRatioChanged)) + if (!NeedsPaint(width, height, pixelRatio, out bool pixelRatioChanged)) { return; } @@ -117,51 +135,81 @@ private void OnPaintSurface(object? sender, _SKPaintSurfaceEventArgs e) canvas.Clear(SKColors.Transparent); canvas.Save(); - if (shadows.Length == 0) + if (Shadows is not { Count: > 0 } shadows) { return; } - var key = - FormattableString.Invariant($"w{width},h{height}") + - string.Join('/', shadows.Select(x => x.ToKey())); - if (pixelRatioChanged) + // If there is any inner shadow, we need to: + // 1. Get the background color from the content + // 2. Set the content background to transparent + // 3. Draw the content background with skia underneath inner shadows + bool hasInnerShadow = shadows.HasInnerShadow(); + if (hasInnerShadow) { - // Pixel density changed, invalidate cached image - Cache.Remove(key); + // Will set the content background to transparent if needed + if (_currentContentBackgroundColor == null && ProcessContentBackgroundIfNeeded(out var contentBackgroundWinUIColor)) + { + _currentContentBackgroundColor = contentBackgroundWinUIColor; + } } - else if (Cache.TryGetValue(key, out var shadowsImage)) + else if (_currentContentBackgroundColor.HasValue) { - canvas.DrawImage(shadowsImage, SKPoint.Empty); - canvas.Restore(); - return; + // Means that there were inner shadows, and they have been removed: restore content background + TrySetContentBackground(new SolidColorBrush(_currentContentBackgroundColor.Value)); + _currentContentBackgroundColor = null; + } + + string shadowsKey = shadows.ToKey(width, height, _currentContentBackgroundColor); + if (Cache.TryGetValue(shadowsKey, out var shadowsImage)) + { + if (pixelRatioChanged) + { + // Monitor pixel density changed, need to remove cached image + Cache.Remove(shadowsKey); + } + else + { + canvas.DrawImage(shadowsImage, SKPoint.Empty); + canvas.Restore(); + return; + } } - var childWidth = (float)width * pixelRatio; - var childHeight = (float)height * pixelRatio; - var diffWidthSurfaceChild = surfaceWidth - childWidth; - var diffHeightSurfaceChild = surfaceHeight - childHeight; + float childWidth = (float)width * pixelRatio; + float childHeight = (float)height * pixelRatio; + float diffWidthSurfaceChild = surfaceWidth - childWidth; + float diffHeightSurfaceChild = surfaceHeight - childHeight; canvas.Translate(diffWidthSurfaceChild / 2, diffHeightSurfaceChild / 2); - using var paint = new SKPaint() { IsAntialias = true }; - var cornerRadius = (float)_cornerRadius.BottomRight * pixelRatio; + using var paint = new SKPaint(); + paint.IsAntialias = true; - if (context.IsBackground) + float cornerRadius = (float)_cornerRadius.BottomRight * pixelRatio; + + foreach (var shadow in shadows.Where(s => !s.IsInner)) { - foreach (var shadow in shadows) - { - var skShadow = SKShadow.From(shadow, childWidth, childHeight, cornerRadius, pixelRatio); + var skShadow = SKShadow.From(shadow, childWidth, childHeight, cornerRadius, pixelRatio); - DrawDropShadow(canvas, paint, skShadow); - } + DrawDropShadow(canvas, paint, skShadow); } - else + + // Always draw inner shadows on top of the drop shadows + if (hasInnerShadow) { var contentShape = new SKRoundRect(new SKRect(0, 0, childWidth, childHeight), cornerRadius); canvas.ClipRoundRect(contentShape, antialias: true); - foreach (var shadow in shadows) + // Draw the content background first + if (_currentContentBackgroundColor.HasValue) + { + var contentBackgroundColor = ToSkiaColor(_currentContentBackgroundColor.Value); + DrawContentBackground(canvas, contentBackgroundColor, contentShape); + } + + // Then we draw the inner shadows + foreach (var shadow in shadows.Where(s => s.IsInner)) { var skShadow = SKShadow.From(shadow, childWidth, childHeight, cornerRadius, pixelRatio); @@ -171,13 +219,51 @@ private void OnPaintSurface(object? sender, _SKPaintSurfaceEventArgs e) canvas.Restore(); - // If a property has changed dynamically, we don't want to cache the updated shadows - if (!context.IsDirty) + if (!_shadowPropertyChanged) { - Cache.AddOrUpdate(key, surface.Snapshot()); + // If a property has changed dynamically, we don't want to cache the updated shadows + Cache.AddOrUpdate(shadowsKey, surface.Snapshot()); + } + + _shadowPropertyChanged = false; + } + + private bool ProcessContentBackgroundIfNeeded(out Color? contentBackgroundColor) + { + contentBackgroundColor = null; + if (TryGetContentBackground(out var background)) + { + if (background is not SolidColorBrush backgroundColorBrush) + { + throw new NotSupportedException("[ShadowContainer] Unsupported Background brush: when using inner shadows the only supported brush type for the Background property is SolidBrushColor"); + } + + if (backgroundColorBrush.Color != Color.FromArgb(0, 0, 0, 0)) + { + contentBackgroundColor = backgroundColorBrush.Color; + } + + TrySetContentBackground(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0))); + return true; } - context.IsDirty = false; + return false; + } + + private static void DrawContentBackground(SKCanvas canvas, SKColor contentBackgroundColor, SKRoundRect childShape) + { + using var backgroundPaint = new SKPaint + { + Color = contentBackgroundColor, + Style = SKPaintStyle.Fill, + }; + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.Debug( + $"[ShadowContainer] DrawContentBackground => color: {backgroundPaint.Color}"); + } + canvas.DrawRoundRect(childShape, backgroundPaint); } private static void DrawDropShadow(SKCanvas canvas, SKPaint paint, SKShadow shadow) @@ -213,7 +299,8 @@ private static void DrawDropShadow(SKCanvas canvas, SKPaint paint, SKShadow shad if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.Debug($"[ShadowContainer] DrawDropShadow => x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}"); + _logger.Debug( + $"[ShadowContainer] DrawDropShadow => x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}"); } } @@ -231,7 +318,8 @@ private static void DrawInnerShadow(SKCanvas canvas, SKPaint paint, SKShadow sha if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.Debug($"[ShadowContainer] DrawInnerShadow => strokeWidth: {paint.StrokeWidth}, cornerRadius: {shadow.CornerRadius}, x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}"); + _logger.Debug( + $"[ShadowContainer] DrawInnerShadow => strokeWidth: {paint.StrokeWidth}, cornerRadius: {shadow.CornerRadius}, x: {shadow.OffsetX}, y: {shadow.OffsetY}, width: {shadow.ContentWidth}, height: {shadow.ContentHeight}"); } var shadowShape = new SKRoundRect( @@ -248,43 +336,47 @@ private static void DrawInnerShadow(SKCanvas canvas, SKPaint paint, SKShadow sha canvas.DrawRoundRect(shadowShape, paint); } - /// - /// Record of properties at one point in time. - /// - private record ShadowInfo(double OffsetX, double OffsetY, double BlurRadius, double Spread, Color Color, double Opacity) + private bool TryGetContentBackground(out Brush? background) { - public static ShadowInfo Snapshot(Shadow x) => new( - x.OffsetX, x.OffsetY, - x.BlurRadius, x.Spread, - x.Color, - x.Opacity - ); - - public static ShadowInfo[] Snapshot(Shadow[] shadows) => shadows.Select(Snapshot).ToArray(); - } + if (_currentContent == null) + { + background = null; + return false; + } + + background = _currentContent switch + { + Control control => control.Background, + Panel panel => panel.Background, + Border border => border.Background, + _ => null, + }; - /// - /// Used in comparison to determine if the shadow needs to be repainted. - /// - private record ShadowPaintState(double Width, double Height, double PixelRatio, ShadowInfo[] ShadowInfos); + return background != null; + } - /// - /// Used to organize references and context used for shadow painting. 2 copies are required for fore- and background. - /// - private class ShadowPaintContext + private bool TrySetContentBackground(SolidColorBrush background) { - public _SKXamlCanvas? ShadowHost { get; set; } - - public ShadowPaintState? LastPaintState { get; set; } + switch (_currentContent) + { + case Control control: + control.Background = background; + break; + case Panel panel: + panel.Background = background; + break; + case Border border: + border.Background = background; + break; + default: + return false; + } - public bool IsBackground { get; init; } -#if __ANDROID__ && ANDROID_REFERENTIAL_IMPL - public bool IsOpacitySet { get; set; } -#endif - public bool IsDirty { get; set; } + return true; + } - // Inner shadows are drawn on top of content. - // In the code here, they are also refered as "foreground" shadows. - public bool IsInner => !IsBackground; + private static SKColor ToSkiaColor(Color windowsUiColor) + { + return new SKColor(windowsUiColor.R, windowsUiColor.G, windowsUiColor.B, windowsUiColor.A); } } diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs index e60bab907..f68273212 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.Properties.cs @@ -1,9 +1,15 @@ -using System; -using System.Collections.Specialized; +using System.Collections.Specialized; using System.ComponentModel; +using System.Linq; +using Uno.Disposables; + +#if IS_WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Uno.Disposables; +#else +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif namespace Uno.Toolkit.UI; @@ -36,6 +42,9 @@ public ShadowCollection Shadows #endregion + // True if a shadow property has changed dynamically or if we add or removed a shadow + private bool _shadowPropertyChanged; + private static void OnShadowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is ShadowContainer shadowContainer) @@ -49,27 +58,30 @@ private void OnShadowCollectionChanged(object? sender, NotifyCollectionChangedEv switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (var item in e.NewItems ?? Array.Empty()) + for (int i = 0; i < e.NewItems!.Count; i++) { - OnShadowInserted((Shadow)item); + OnShadowInserted((Shadow)e.NewItems[i]!); } - break; + OnShadowSizeChanged(); + InvalidateFromShadowPropertyChange(); + + break; case NotifyCollectionChangedAction.Remove: - foreach (var item in e.OldItems ?? Array.Empty()) + for (int i = 0; i < e.OldItems!.Count; i++) { - OnShadowRemoved((Shadow)item); + OnShadowRemoved((Shadow)e.OldItems[i]!); } + + OnShadowSizeChanged(); + InvalidateFromShadowPropertyChange(); break; case NotifyCollectionChangedAction.Reset: + OnShadowSizeChanged(); + InvalidateFromShadowPropertyChange(); break; - - default: return; } - - OnShadowSizeChanged(); - InvalidateFromShadowPropertyChange(); } private void UpdateShadows() @@ -94,7 +106,7 @@ private void UpdateShadows() _shadowPropertiesChanged.Disposable = _activeShadowRegistrations; OnShadowSizeChanged(); - InvalidateShadowHosts(); + _shadowHost?.Invalidate(); } private void OnShadowInserted(Shadow shadow) @@ -133,8 +145,7 @@ private void OnShadowSizeChanged() private void InvalidateFromShadowPropertyChange() { - _backgroundPaintContext.IsDirty = true; - _foregroundPaintContext.IsDirty = true; - InvalidateShadowHosts(); + _shadowPropertyChanged = true; + _shadowHost?.Invalidate(); } } diff --git a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs index f6bd8ab47..6e3258378 100644 --- a/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs +++ b/src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs @@ -1,21 +1,18 @@ -#if false -// We keep that as a reference cause it would be better to use the hardware-accelerated version -#define ANDROID_REFERENTIAL_IMPL -#endif - -using System; +using System; using System.Linq; + +#if IS_WINUI using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using SkiaSharp; using SkiaSharp.Views.Windows; - -#if __ANDROID__ && ANDROID_REFERENTIAL_IMPL -using _SKXamlCanvas = SkiaSharp.Views.Windows.SKSwapChainPanel; -using _SKPaintSurfaceEventArgs = SkiaSharp.Views.Windows.SKPaintGLSurfaceEventArgs; #else -using _SKXamlCanvas = SkiaSharp.Views.Windows.SKXamlCanvas; -using _SKPaintSurfaceEventArgs = SkiaSharp.Views.Windows.SKPaintSurfaceEventArgs; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using SkiaSharp.Views.UWP; +#endif + +#if __ANDROID__ +using Android.Views; #endif namespace Uno.Toolkit.UI; @@ -33,25 +30,41 @@ public partial class ShadowContainer : ContentControl private const string PART_Canvas = "PART_Canvas"; private Canvas? _canvas; + +#if false // ANDROID (see comment below) + private readonly SKSwapChainPanel _shadowHost; + private bool _notOpaqueSet = false; +#else + private SKXamlCanvas? _shadowHost; +#endif + + private static readonly ShadowsCache Cache = new ShadowsCache(); + private FrameworkElement? _currentContent; - private readonly ShadowPaintContext _backgroundPaintContext = new() { IsBackground = true }; - private readonly ShadowPaintContext _foregroundPaintContext = new(); private CornerRadius _cornerRadius; public ShadowContainer() { + Shadows = new(); + DefaultStyleKey = typeof(ShadowContainer); - Shadows = new(); + _cornerRadius = new CornerRadius(0); Loaded += ShadowContainerLoaded; Unloaded += ShadowContainerUnloaded; } - private void ShadowContainerLoaded(object sender, RoutedEventArgs e) => UpdateShadows(); + private void ShadowContainerUnloaded(object sender, RoutedEventArgs e) + { + RevokeListeners(); + } - private void ShadowContainerUnloaded(object sender, RoutedEventArgs e) => RevokeListeners(); + private void ShadowContainerLoaded(object sender, RoutedEventArgs e) + { + UpdateShadows(); + } private void RevokeListeners() { @@ -62,24 +75,25 @@ private void RevokeListeners() protected override void OnApplyTemplate() { - base.OnApplyTemplate(); + _canvas = GetTemplateChild(nameof(PART_Canvas)) as Canvas; - _canvas = - GetTemplateChild(nameof(PART_Canvas)) as Canvas ?? - throw new InvalidOperationException($"Canvas '{PART_Canvas}' was not found in the control-template."); - _backgroundPaintContext.ShadowHost = new(); - _foregroundPaintContext.ShadowHost = new() { IsHitTestVisible = false }; - _backgroundPaintContext.ShadowHost.PaintSurface += OnPaintSurface; - _foregroundPaintContext.ShadowHost.PaintSurface += OnPaintSurface; +#if false // ANDROID: We keep that as a reference cause it would be better to use the hardware-accelerated version + var skiaCanvas = new SKSwapChainPanel(); + skiaCanvas.PaintSurface += OnSurfacePainted; +#else + var skiaCanvas = new SKXamlCanvas(); + skiaCanvas.PaintSurface += OnSurfacePainted; +#endif #if __IOS__ || __MACCATALYST__ - _backgroundPaintContext.ShadowHost.Opaque = false; - _foregroundPaintContext.ShadowHost.Opaque = false; + skiaCanvas.Opaque = false; #endif - _canvas.Children.Insert(0, _backgroundPaintContext.ShadowHost); - _canvas.Children.Add(_foregroundPaintContext.ShadowHost); + _shadowHost = skiaCanvas; + _canvas?.Children.Insert(0, _shadowHost!); + + base.OnApplyTemplate(); } /// @@ -92,56 +106,68 @@ protected override void OnContentChanged(object oldContent, object newContent) _canvas?.Children.Remove(oldElement); oldElement.SizeChanged -= OnContentSizeChanged; } + if (newContent is FrameworkElement newElement) { _currentContent = newElement; _currentContent.SizeChanged += OnContentSizeChanged; - if (FindCornerRadiusProperty(newElement) is { } dp) + if (TryGetCornerRadius(newElement, out var cornerRadius)) { - _cornerRadius = (CornerRadius)newElement.GetValue(dp); - _cornerRadiusChanged.Disposable = newElement.RegisterDisposablePropertyChangedCallback(dp, OnCornerRadiusChanged); + var cornerRadiusProperty = newElement switch + { + Grid _ => Grid.CornerRadiusProperty, + StackPanel _ => StackPanel.CornerRadiusProperty, + ContentPresenter _ => ContentPresenter.CornerRadiusProperty, + Border _ => Border.CornerRadiusProperty, + Control _ => Control.CornerRadiusProperty, + RelativePanel _ => RelativePanel.CornerRadiusProperty, + _ => default, + + }; + + if (cornerRadiusProperty != null) + { + _cornerRadiusChanged.Disposable = newElement.RegisterDisposablePropertyChangedCallback( + cornerRadiusProperty, + (s, dp) => OnCornerRadiusChanged(s, dp) + ); + } } - else - { - _cornerRadius = default; - _cornerRadiusChanged.Disposable = null; - } - } - InvalidateShadowHosts(); + _cornerRadius = cornerRadius; + } + _shadowHost?.Invalidate(); base.OnContentChanged(oldContent, newContent); } private void OnCornerRadiusChanged(DependencyObject sender, DependencyProperty dp) { - if (_currentContent?.GetValue(dp) is CornerRadius value) + if (_currentContent is { }) { - _cornerRadius = value; - InvalidateShadowHosts(); - } - else - { - _cornerRadius = default; + if (TryGetCornerRadius(_currentContent, out var cornerRadius)) + { + _cornerRadius = cornerRadius; + _shadowHost?.Invalidate(); + } } } - private static DependencyProperty? FindCornerRadiusProperty(FrameworkElement element) + private static bool TryGetCornerRadius(FrameworkElement element, out CornerRadius cornerRadius) { - return element switch + CornerRadius? localCornerRadius = element switch { - // fast path to avoid reflection - Grid _ => Grid.CornerRadiusProperty, - StackPanel _ => StackPanel.CornerRadiusProperty, - ContentPresenter _ => ContentPresenter.CornerRadiusProperty, - Border _ => Border.CornerRadiusProperty, - Control _ => Control.CornerRadiusProperty, - RelativePanel _ => RelativePanel.CornerRadiusProperty, - - DependencyObject @do => @do.FindDependencyPropertyUsingReflection("CornerRadiusProperty"), - _ => null, + Control control => control.CornerRadius, + StackPanel stackPanel => stackPanel.CornerRadius, + RelativePanel relativePanel => relativePanel.CornerRadius, + Grid grid => grid.CornerRadius, + Border border => border.CornerRadius, + _ => VisualTreeHelperEx.TryGetDpValue(element, "CornerRadius", out var value) ? value : default(CornerRadius?), }; + + cornerRadius = localCornerRadius ?? new CornerRadius(0); + return localCornerRadius != null; } private void OnContentSizeChanged(object sender, SizeChangedEventArgs args) @@ -149,23 +175,21 @@ private void OnContentSizeChanged(object sender, SizeChangedEventArgs args) if (args.NewSize.Width > 0 && args.NewSize.Height > 0) { UpdateCanvasSize(args.NewSize.Width, args.NewSize.Height, Shadows); - InvalidateShadowHosts(); + _shadowHost?.Invalidate(); } } private void UpdateCanvasSize(double childWidth, double childHeight, ShadowCollection? shadows) { - if (_currentContent == null || - _canvas == null || - _backgroundPaintContext.ShadowHost == null || _foregroundPaintContext.ShadowHost == null) + if (_currentContent == null || _canvas == null || _shadowHost == null) { return; } - var absoluteMaxOffsetX = 0d; - var absoluteMaxOffsetY = 0d; - var maxBlurRadius = 0d; - var maxSpread = 0d; + double absoluteMaxOffsetX = 0; + double absoluteMaxOffsetY = 0; + double maxBlurRadius = 0; + double maxSpread = 0; if (shadows?.Any() == true) { @@ -177,32 +201,21 @@ private void UpdateCanvasSize(double childWidth, double childHeight, ShadowColle _canvas.Height = childHeight; _canvas.Width = childWidth; - #if __ANDROID__ || __IOS__ _canvas.GetDispatcherCompat().Schedule(() => _canvas.InvalidateMeasure()); #endif + double newHostHeight = childHeight + maxBlurRadius * 2 + absoluteMaxOffsetY * 2 + maxSpread * 2; + double newHostWidth = childWidth + maxBlurRadius * 2 + absoluteMaxOffsetX * 2 + maxSpread * 2; + _shadowHost.Height = newHostHeight; + _shadowHost.Width = newHostWidth; - var newHostHeight = childHeight + maxBlurRadius * 2 + absoluteMaxOffsetY * 2 + maxSpread * 2; - var newHostWidth = childWidth + maxBlurRadius * 2 + absoluteMaxOffsetX * 2 + maxSpread * 2; - var diffWidthShadowHostChild = newHostWidth - childWidth; - var diffHeightShadowHostChild = newHostHeight - childHeight; - var left = -diffWidthShadowHostChild / 2 + _currentContent.Margin.Left; - var top = -diffHeightShadowHostChild / 2 + _currentContent.Margin.Top; + double diffWidthShadowHostChild = newHostWidth - childWidth; + double diffHeightShadowHostChild = newHostHeight - childHeight; - FixOntoCanvas(_backgroundPaintContext.ShadowHost, left, top, newHostWidth, newHostHeight); - FixOntoCanvas(_foregroundPaintContext.ShadowHost, left, top, newHostWidth, newHostHeight); - void FixOntoCanvas(FrameworkElement fe, double left, double top, double width, double height) - { - fe.Width = width; - fe.Height = height; - Canvas.SetLeft(fe, left); - Canvas.SetTop(fe, top); - } - } + float left = (float)(-diffWidthShadowHostChild / 2 + _currentContent.Margin.Left); + float top = (float)(-diffHeightShadowHostChild / 2 + _currentContent.Margin.Top); - private void InvalidateShadowHosts() - { - _backgroundPaintContext.ShadowHost?.Invalidate(); - _foregroundPaintContext.ShadowHost?.Invalidate(); + Canvas.SetLeft(_shadowHost, left); + Canvas.SetTop(_shadowHost, top); } } diff --git a/src/Uno.Toolkit.UI/Extensions/DependencyObjectExtensions.cs b/src/Uno.Toolkit.UI/Extensions/DependencyObjectExtensions.cs index 7f105fbb8..884fa2216 100644 --- a/src/Uno.Toolkit.UI/Extensions/DependencyObjectExtensions.cs +++ b/src/Uno.Toolkit.UI/Extensions/DependencyObjectExtensions.cs @@ -7,8 +7,6 @@ using Microsoft.Extensions.Logging; using Uno.Logging; -using static System.Reflection.BindingFlags; - #if IS_WINUI using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Controls; @@ -193,9 +191,8 @@ internal static void SetParent(this DependencyObject dependencyObject, object? p return property; } - property = - type.GetProperty(propertyName, Public | Static | FlattenHierarchy)?.GetValue(null) as DependencyProperty ?? - type.GetField(propertyName, Public | Static | FlattenHierarchy)?.GetValue(null) as DependencyProperty; + property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as DependencyProperty + ?? type.GetField(propertyName, BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as DependencyProperty; if (property == null) { diff --git a/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs b/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs index ebc539d7f..b12796c13 100644 --- a/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs +++ b/src/Uno.Toolkit.UI/Helpers/VisualTreeHelperEx.cs @@ -1,6 +1,4 @@ -#define USE_CACHED_DP_REFLECTION - -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -249,13 +247,8 @@ IEnumerable GetDetails() internal static bool TryGetDpValue(object owner, string property, out T? value) { if (owner is DependencyObject @do && -#if USE_CACHED_DP_REFLECTION - @do.FindDependencyPropertyUsingReflection($"{property}Property") is { } dp -#else owner.GetType().GetProperty($"{property}Property", Public | Static | FlattenHierarchy) - ?.GetValue(null, null) is DependencyProperty dp -#endif - ) + ?.GetValue(null, null) is DependencyProperty dp) { value = (T)@do.GetValue(dp); return true;