Skip to content

Commit

Permalink
fix(ShadowContainer): prevent unnecessary repaint (#830)
Browse files Browse the repository at this point in the history
  • Loading branch information
roubachof committed Oct 6, 2023
1 parent 7e94607 commit fcf4422
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,51 @@
<StackPanel Spacing="8">
<TextBlock Text="Complex Shadow Shapes" Style="{StaticResource TitleTextBlockStyle}" />

<StackPanel>
<!--
Should be the same as: https://developer.mozilla.org/fr/docs/Web/CSS/CSS_backgrounds_and_borders/Box-shadow_generator
element {
width: 300px; height: 100px; background-color: #7A67F8; position: relative;
box-shadow:
6px -5px 5px 0px #E88BAB,
inset 5px -5px 5px 0px #94DB6D,
inset -20px -10px 10px 10px #000000;
}
-->
<utu:ShadowContainer Margin="0,30" Background="{StaticResource UnoColor}">
<utu:ShadowContainer.Shadows>
<utu:ShadowCollection>
<utu:Shadow IsInner="True"
OffsetX="-20"
OffsetY="-10"
BlurRadius="10"
Spread="10"
Color="Black"
Opacity="1" />
<utu:Shadow IsInner="True"
OffsetX="5"
OffsetY="-5"
BlurRadius="5"
Spread="0"
Color="#94DB6D"
Opacity="1" />
<utu:Shadow OffsetX="6"
OffsetY="-5"
BlurRadius="5"
Spread="0"
Color="#E88BAB"
Opacity="1" />
</utu:ShadowCollection>
</utu:ShadowContainer.Shadows>
<Button Width="300"
Height="100"
Background="Transparent"
BorderThickness="0"
Content="CSS box-shadow"
CornerRadius="0"
Foreground="White" />
</utu:ShadowContainer>

<StackPanel>
<TextBlock Text="Border 200x100 CR=10,50,10,50" />
<StackPanel Orientation="Horizontal">
<Border Width="200" Height="100" CornerRadius="10,50,10,50" Background="Blue" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public partial class ShadowContainer

private ShadowPaintState? _lastPaintState;
private bool _isShadowDirty;
private int _paintCount;

internal event EventHandler<SurfacePaintCompletedEventArgs>? SurfacePaintCompleted;

Expand All @@ -38,10 +39,11 @@ private void OnSurfacePaintCompleted(bool createdNewCanvas)

private bool NeedsPaint(ShadowPaintState state, out bool pixelRatioChanged)
{
var needsPaint = state != _lastPaintState;
pixelRatioChanged = state.PixelRatio != _lastPaintState?.PixelRatio;
var needsPaint = state != _lastPaintState || _isShadowHostDirty;
pixelRatioChanged = _lastPaintState != null && state.PixelRatio != _lastPaintState.PixelRatio;

_lastPaintState = state;
_isShadowHostDirty = false;
return needsPaint;
}

Expand Down Expand Up @@ -76,11 +78,15 @@ private void OnSurfacePainted(object? sender, SKPaintSurfaceEventArgs e)

if (state.Shadows.Length == 0)
{
canvas.Clear(SKColors.Transparent);
shape.DrawContentBackground(state, canvas, background ?? Colors.Transparent);
return;
}

if (_logger.IsEnabled(LogLevel.Trace))
{
_logger.Trace($"[ShadowContainer] Painting shadows (x{++_paintCount}) for content {Content.GetType().Name}, actualSize: {contentAsFE.ActualWidth}x{contentAsFE.ActualHeight}");
}

using var _ = canvas.SnapshotState();

var key =
Expand Down Expand Up @@ -148,7 +154,7 @@ private void OnSurfacePainted(object? sender, SKPaintSurfaceEventArgs e)
};
}

private IShadowShapeContext GetShadowShapeContext(object content)
private ShadowShapeContext GetShadowShapeContext(object content)
{
return content switch
{
Expand All @@ -163,28 +169,29 @@ private IShadowShapeContext GetShadowShapeContext(object content)

/// <summary>
/// Serves both as a record of states relevant to shadow shape (not <see cref="Shape"/>, but in the broad sense), and the implementations for painting the shadows.
/// We can't use an interface here or we will lose the ability to compare instances of this class (thanks to generated Equals).
/// </summary>
private interface IShadowShapeContext
private abstract record ShadowShapeContext
{
void ClipToContent(ShadowPaintState state, SKCanvas canvas);
public abstract void ClipToContent(ShadowPaintState state, SKCanvas canvas);

void DrawContentBackground(ShadowPaintState state, SKCanvas canvas, Color color);
public abstract void DrawContentBackground(ShadowPaintState state, SKCanvas canvas, Color color);

void DrawDropShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow);
public abstract void DrawDropShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow);

void DrawInnerShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow);
public abstract void DrawInnerShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow);
}

private abstract record RoundRectShadowShapeContext(double Width, double Height) : IShadowShapeContext
private abstract record RoundRectShadowShapeContext(double Width, double Height) : ShadowShapeContext
{
protected abstract SKRoundRect GetContentShape(ShadowPaintState state);

public void ClipToContent(ShadowPaintState state, SKCanvas canvas)
public override void ClipToContent(ShadowPaintState state, SKCanvas canvas)
{
canvas.ClipRoundRect(GetContentShape(state), antialias: true);
}

public void DrawContentBackground(ShadowPaintState state, SKCanvas canvas, Color color)
public override void DrawContentBackground(ShadowPaintState state, SKCanvas canvas, Color color)
{
var shape = GetContentShape(state);
using var backgroundPaint = new SKPaint
Expand All @@ -200,7 +207,7 @@ public void DrawContentBackground(ShadowPaintState state, SKCanvas canvas, Color
canvas.DrawRoundRect(shape, backgroundPaint);
}

public void DrawDropShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow)
public override void DrawDropShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow)
{
var spread = (float)shadow.Spread * state.PixelRatio;
var offsetX = (float)shadow.OffsetX * state.PixelRatio;
Expand Down Expand Up @@ -238,7 +245,7 @@ public void DrawDropShadow(ShadowPaintState state, SKCanvas canvas, SKPaint pain
canvas.DrawRoundRect(shape, paint);
}

public void DrawInnerShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow)
public override void DrawInnerShadow(ShadowPaintState state, SKCanvas canvas, SKPaint paint, ShadowInfo shadow)
{
var spread = (float)shadow.Spread * state.PixelRatio;
var offsetX = (float)shadow.OffsetX * state.PixelRatio;
Expand Down Expand Up @@ -329,7 +336,7 @@ public float GetBlurSigma(float pixelRatio)
/// Used in comparison to determine if the shadow needs to be repainted.
/// </summary>
private sealed record ShadowPaintState(
IShadowShapeContext Shape,
ShadowShapeContext Shape,
Color? Background,
float PixelRatio,
ShadowInfo[] Shadows)
Expand All @@ -355,11 +362,11 @@ public override int GetHashCode()
}

public bool Equals(ShadowPaintState? x) =>
x is { } y &&
Shape == y.Shape &&
Background == y.Background &&
PixelRatio == y.PixelRatio &&
Shadows.SequenceEqual(y.Shadows);
x is not null &&
Shape == x.Shape &&
Background == x.Background &&
PixelRatio == x.PixelRatio &&
Shadows.SequenceEqual(x.Shadows);
}

public class SurfacePaintCompletedEventArgs : EventArgs
Expand Down
39 changes: 32 additions & 7 deletions src/Uno.Toolkit.Skia.WinUI/Controls/Shadows/ShadowContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
Expand Down Expand Up @@ -36,8 +35,11 @@ public partial class ShadowContainer : ContentControl

private Grid? _panel;
private Canvas? _canvas;

private SKXamlCanvas? _shadowHost;

private bool _isShadowHostDirty = true;

public ShadowContainer()
{
DefaultStyleKey = typeof(ShadowContainer);
Expand Down Expand Up @@ -73,6 +75,8 @@ private void BindToPaintingProperties()
this.RegisterDisposablePropertyChangedCallback(ShadowsProperty, OnShadowsChanged),
this.RegisterDisposablePropertyChangedCallback(ContentProperty, OnContentChanged),

RegisterDisposableShadowHostSizeChangedCallback(OnShadowHostSizeChanged),

backgroundNestedDisposable,
shadowsNestedDisposable,
contentNestedDisposable,
Expand All @@ -86,7 +90,7 @@ private void BindToPaintingProperties()
// This method should not fire any of InvalidateXyz-methods directly,
// in order to avoid duplicated invalidate calls.
// Which is why the BindToXyz has been separated from the OnXyzChanged.

void OnBackgroundChanged(DependencyObject sender, DependencyProperty dp)
{
BindToBackgroundMemberProperties(Background);
Expand All @@ -106,6 +110,11 @@ void OnContentChanged(DependencyObject sender, DependencyProperty dp)

InvalidateShadows();
}
// When the skia canvas size changes, the whole canvas is cleared: we'll need to redraw the shadows.
void OnShadowHostSizeChanged(object sender, SizeChangedEventArgs args)
{
_isShadowHostDirty = true;
}

void OnShadowPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Expand All @@ -117,6 +126,7 @@ void OnShadowPropertyChanged(object? sender, PropertyChangedEventArgs e)
}
InvalidateShadows();
}

void OnInnerPropertyChanged(DependencyObject sender, DependencyProperty dp)
{
InvalidateShadows();
Expand All @@ -137,6 +147,21 @@ void OnContentSizeChanged(object sender, SizeChangedEventArgs e)
InvalidateShadows();
}

SerialDisposable RegisterDisposableShadowHostSizeChangedCallback(SizeChangedEventHandler sizeChanged)
{
if (_shadowHost == null)
{
return new SerialDisposable();
}

_shadowHost.SizeChanged += OnShadowHostSizeChanged;
return new SerialDisposable
{
Disposable = Disposable.Create(() => _shadowHost.SizeChanged -= sizeChanged),
};

}

void BindToBackgroundMemberProperties(Brush? background)
{
backgroundNestedDisposable.Disposable = background switch
Expand Down Expand Up @@ -244,6 +269,7 @@ protected override void OnApplyTemplate()
_panel = GetTemplateChild(nameof(PART_ShadowOwner)) as Grid;

var skiaCanvas = new SKXamlCanvas();

skiaCanvas.PaintSurface += OnSurfacePainted;

#if __IOS__ || __MACCATALYST__
Expand All @@ -254,6 +280,8 @@ protected override void OnApplyTemplate()
_canvas?.Children.Insert(0, _shadowHost!);
}



private void InvalidateCanvasLayout()
{
if (Content is not FrameworkElement contentAsFE ||
Expand Down Expand Up @@ -288,7 +316,6 @@ private void InvalidateCanvasLayoutSize()
return;
}


#if __ANDROID__ || __IOS__
_canvas.GetDispatcherCompat().Schedule(() => _canvas.InvalidateMeasure());
#endif
Expand Down Expand Up @@ -318,15 +345,13 @@ private void InvalidateCanvasLayoutSize()
_canvas.HorizontalAlignment = contentAsFE.HorizontalAlignment;
_canvas.VerticalAlignment = contentAsFE.VerticalAlignment;



double left =
+(contentAsFE.HorizontalAlignment == HorizontalAlignment.Left ? -newHostSpreedWidth : 0)
+ (contentAsFE.HorizontalAlignment == HorizontalAlignment.Left ? -newHostSpreedWidth : 0)
+ (contentAsFE.HorizontalAlignment == HorizontalAlignment.Right ? -newHostSpreedWidth - childWidth / 2 : 0)
+ (contentAsFE.HorizontalAlignment == HorizontalAlignment.Stretch ? -newHostSpreedWidth - childWidth / 4 : 0)
+ (contentAsFE.HorizontalAlignment == HorizontalAlignment.Center ? -newHostSpreedWidth - childWidth / 4 : 0);
double top =
+(contentAsFE.VerticalAlignment == VerticalAlignment.Top ? -newHostSpreedHeight : 0)
+ (contentAsFE.VerticalAlignment == VerticalAlignment.Top ? -newHostSpreedHeight : 0)
+ (contentAsFE.VerticalAlignment == VerticalAlignment.Bottom ? -newHostSpreedHeight - childHeight / 2 : 0)
+ (contentAsFE.VerticalAlignment == VerticalAlignment.Stretch ? -newHostSpreedHeight - childHeight / 4 : 0)
+ (contentAsFE.VerticalAlignment == VerticalAlignment.Center ? -newHostSpreedHeight - childHeight / 4 : 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Uno.Logging;

namespace Uno.Toolkit.UI;

public class ShadowsCache
{
private static readonly ILogger _logger = typeof(ShadowsCache).Log();
Expand Down

0 comments on commit fcf4422

Please sign in to comment.