Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(skia): (partially) implement ShapeVisual.ViewBox and fix ShapeVisual clipping #18561

Merged
merged 13 commits into from
Nov 8, 2024
4 changes: 2 additions & 2 deletions src/AddIns/Uno.WinUI.Graphics2DSK/SKCanvasElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Uno.WinUI.Graphics2DSK;
/// <remarks>This is only available on skia-based targets.</remarks>
public abstract class SKCanvasElement : FrameworkElement
{
private protected override ShapeVisual CreateElementVisual() => new SKCanvasVisual(this, Compositor.GetSharedCompositor());
private protected override ContainerVisual CreateElementVisual() => new SKCanvasVisual(this, Compositor.GetSharedCompositor());

/// <summary>
/// Queue a rendering cycle that will call <see cref="RenderOverride"/>.
Expand All @@ -30,7 +30,7 @@ public abstract class SKCanvasElement : FrameworkElement
/// </remarks>
protected abstract void RenderOverride(SKCanvas canvas, Size area);

private class SKCanvasVisual(SKCanvasElement owner, Compositor compositor) : ShapeVisual(compositor)
private class SKCanvasVisual(SKCanvasElement owner, Compositor compositor) : ContainerVisual(compositor)
{
internal override void Paint(in PaintingSession session)
{
Expand Down
2 changes: 2 additions & 0 deletions src/SamplesApp/SamplesApp.Windows/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
global using Microsoft.UI;
global using Microsoft.UI.Text;
global using Colors = Microsoft.UI.Colors;
global using FontWeights = Microsoft.UI.Text.FontWeights;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml\Clipping\ShapeVisualClipping.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_ViewManagement\UISettingsTests.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -6204,6 +6208,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml\Clipping\Clipping4273.xaml.cs">
<DependentUpon>Clipping4273.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml\Clipping\ShapeVisualClipping.xaml.cs">
<DependentUpon>ShapeVisualClipping.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml\Clipping\Clipping652.xaml.cs">
<DependentUpon>Clipping652.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -9707,6 +9714,7 @@
<Content Include="$(MSBuildThisFileDirectory)Assets\RemoteFonts\antikytheraoutlineital.woff" />
<Content Include="$(MSBuildThisFileDirectory)Assets\RemoteFonts\GALACTIC VANGUARDIAN NCV.woff" />
<Content Include="$(MSBuildThisFileDirectory)Assets\ResizedLargeWisteria.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\When_ShapeVisual_ViewBox_Shape_Combinations\*.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\square100.png" />
<Content Include="$(MSBuildThisFileDirectory)Assets\testimage_exif_rotated.jpg" />
<Content Include="$(MSBuildThisFileDirectory)Assets\testimage_exif_rotated_different_dimensions.jpg" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<UserControl x:Class="UITests.Windows_UI_Xaml.Clipping.ShapeVisualClipping"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UITests.Windows_UI_Xaml.Clipping"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">

<StackPanel>
<Button Click="Prev">Prev</Button>
<Button Click="Next">Next</Button>
<Border x:Name="border" Width="500" Height="500" />
</StackPanel>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Uno.UI.Samples.Controls;

namespace UITests.Windows_UI_Xaml.Clipping;

[Sample]
public sealed partial class ShapeVisualClipping : UserControl
{
private readonly List<StackPanel> _samples = new();
private int _sampleIndex;

public ShapeVisualClipping()
{
InitializeComponent();

foreach (var shapeVisualSize in (ReadOnlySpan<int>)[20, 40, 80])
{
foreach (var spriteOffset in (ReadOnlySpan<int>)[-40, -20, 20, 40])
{
foreach (var viewboxSize in (ReadOnlySpan<int>)[20, 40, 80, 160])
{
foreach (var viewboxOffset in (ReadOnlySpan<int>)[20, 40, 80])
{
var element = new Border
{
Height = 40,
Width = 40,
Background = new SolidColorBrush(Microsoft.UI.Colors.Red)
};

var elementVisual = ElementCompositionPreview.GetElementVisual(element);
var compositor = elementVisual.Compositor;
var shapeVisual = compositor.CreateShapeVisual();

var spriteShape = compositor.CreateSpriteShape();
spriteShape.Geometry = compositor.CreateRectangleGeometry();
((CompositionRectangleGeometry)spriteShape.Geometry).Size = new Vector2(40, 40);
spriteShape.Offset = new Vector2(spriteOffset, spriteOffset);
spriteShape.FillBrush = compositor.CreateColorBrush(Microsoft.UI.Colors.Blue);
shapeVisual.Shapes.Add(spriteShape);
shapeVisual.Size = new Vector2(shapeVisualSize, shapeVisualSize);
shapeVisual.ViewBox = compositor.CreateViewBox();
shapeVisual.ViewBox.Size = new Vector2(viewboxSize, viewboxSize);
shapeVisual.ViewBox.Offset = new Vector2(viewboxOffset, viewboxOffset);
((ContainerVisual)elementVisual).Children.InsertAtTop(shapeVisual);

_samples.Add(new StackPanel
{
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Children =
{
new TextBlock
{
Text =
$"""
Sample [{_samples.Count + 1}]
shapeVisualSize = {shapeVisualSize}
spriteOffset = {spriteOffset}
viewboxSize = {viewboxSize}
viewboxOffset = {viewboxOffset}
"""
},
element
}
});
}
}
}
}

border.Child = _samples[_sampleIndex];
}

private void Prev(object sender, RoutedEventArgs e)
{
_sampleIndex = Math.Max(0, _sampleIndex - 1);
border.Child = _samples[_sampleIndex];
}

private void Next(object sender, RoutedEventArgs e)
{
_sampleIndex = Math.Min(_samples.Count - 1, _sampleIndex + 1);
border.Child = _samples[_sampleIndex];
}
}
6 changes: 3 additions & 3 deletions src/Uno.UI.Composition/Composition/BorderVisual.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
namespace Microsoft.UI.Composition;

/// <summary>
/// A ShapeVisual that has a border and a background.
/// A Visual that has a border and a background.
/// </summary>
internal class BorderVisual(Compositor compositor) : ShapeVisual(compositor)
internal class BorderVisual(Compositor compositor) : ContainerVisual(compositor)
{
// state set from outside and used inside the class
private CornerRadius _cornerRadius;
Expand Down Expand Up @@ -309,6 +309,6 @@ private static unsafe SKPath CreateBorderPath(SKRect innerArea, SKRect outerArea
internal override bool HitTest(Point point)
{
UpdatePathsAndCornerClip();
return (_borderShape?.HitTest(point) ?? false) || (_backgroundShape?.HitTest(point) ?? false) || base.HitTest(point);
return (_borderShape?.HitTest(point) ?? false) || (_backgroundShape?.HitTest(point) ?? false);
}
}
24 changes: 0 additions & 24 deletions src/Uno.UI.Composition/Composition/CompositionViewBox.skia.cs

This file was deleted.

55 changes: 55 additions & 0 deletions src/Uno.UI.Composition/Composition/ContainerVisual.skia.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Windows.Foundation;
using SkiaSharp;

namespace Microsoft.UI.Composition;

Expand All @@ -9,6 +12,19 @@ public partial class ContainerVisual : Visual
private List<Visual>? _childrenInRenderOrder;
private bool _hasCustomRenderOrder;

private (Rect rect, bool isAncestorClip)? _layoutClip;

/// <summary>
/// Layout clipping is usually applied in the element's coordinate space.
/// However, for Panels and ScrollViewer headers specifically, WinUI applies clipping in the parent's coordinate space.
/// So, isAncestorClip will be set to true for Panels and ScrollViewer headers, indicating that clipping is in parent's coordinate space.
/// </summary>
internal (Rect rect, bool isAncestorClip)? LayoutClip
{
get => _layoutClip;
set => SetObjectProperty(ref _layoutClip, value);
}

internal bool IsChildrenRenderOrderDirty { get; set; }

partial void InitializePartial()
Expand Down Expand Up @@ -43,6 +59,45 @@ internal void ResetRenderOrder()
IsChildrenRenderOrderDirty = false;
}

/// <remarks>This does NOT take the clipping into account.</remarks>
internal virtual bool HitTest(Point point) => new Rect(0, 0, Size.X, Size.Y).Contains(point);

/// <returns>true if a ViewBox exists</returns>
internal bool GetArrangeClipPathInElementCoordinateSpace(SKPath dst)
{
if (LayoutClip is not { isAncestorClip: var isAncestorClip, rect: var rect })
{
return false;
}

dst.Rewind();
var clipRect = rect.ToSKRect();
dst.AddRect(clipRect);
if (isAncestorClip)
{
Matrix4x4.Invert(TotalMatrix, out var totalMatrixInverted);
var childToParentTransform = Parent!.TotalMatrix * totalMatrixInverted;
if (!childToParentTransform.IsIdentity)
{
dst.Transform(childToParentTransform.ToSKMatrix());
}
}

return true;
}

private protected override void ApplyPrePaintingClipping(in SKCanvas canvas)
{
base.ApplyPrePaintingClipping(in canvas);
using (SkiaHelper.GetTempSKPath(out var prePaintingClipPath))
{
if (GetArrangeClipPathInElementCoordinateSpace(prePaintingClipPath))
{
canvas.ClipPath(prePaintingClipPath, antialias: true);
}
}
}

internal override bool SetMatrixDirty()
{
if (base.SetMatrixDirty())
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI.Composition/Composition/ShapeVisual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public CompositionViewBox? ViewBox
set => SetProperty(ref _viewBox, value);
}

// This is lazy as we are using the `ShapeVisual` for UIElement, but lot of them are not creating shapes, reduce memory pressure.
// This is lazy as we are using the `ShapeVisual` for some UIElements, but lot of them are not creating shapes, reduce memory pressure.
public CompositionShapeCollection Shapes
{
get
Expand Down
58 changes: 21 additions & 37 deletions src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs
Original file line number Diff line number Diff line change
@@ -1,67 +1,51 @@
#nullable enable

using System;
using System.Numerics;
using Windows.Foundation;
using SkiaSharp;
using Uno.UI.Composition;

namespace Microsoft.UI.Composition;

public partial class ShapeVisual
{
private protected override void ApplyPrePaintingClipping(in SKCanvas canvas)
{
base.ApplyPrePaintingClipping(in canvas);
using (SkiaHelper.GetTempSKPath(out var prePaintingClipPath))
{
if (GetViewBoxPathInElementCoordinateSpace(prePaintingClipPath))
{
canvas.ClipPath(prePaintingClipPath, antialias: true);
}
}
}

/// <inheritdoc />
internal override void Paint(in PaintingSession session)
{
if (_shapes is { Count: not 0 } shapes)
var canvas = session.Canvas;

if (Size.X == 0 || Size.Y == 0)
{
for (var i = 0; i < shapes.Count; i++)
{
shapes[i].Render(in session);
}
return;
}

base.Paint(in session);
}
// TODO: ShapeVisuals should be clipping to the size rect. However, this breaks shapes for us because
// we implement them with ShapeVisuals and they don't clip anything. The problem is that
// the WinUI implementation doesn't use ShapeVisuals for shapes, but a combination of ContainerVisuals and
// SpriteVisuals. When_StrokeThickness_Is_GreaterThan_Or_Equals_Width and
// When_Border_CornerRadius_HitTesting fail when you uncomment the following line.
// canvas.ClipRect(new SKRect(0, 0, Size.X, Size.Y));

Check warning on line 25 in src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs#L25

Remove this commented out code.

/// <returns>true if a ViewBox exists</returns>
internal bool GetViewBoxPathInElementCoordinateSpace(SKPath dst)
{
if (ViewBox is not { } viewBox)
// TODO: ViewBox.Stretch, ViewBox.HorizontalAlignmentRatio and ViewBox.VerticalAlignmentRatio
if (ViewBox is not null)
{
return false;
canvas.Scale(
ViewBox.Size.X > 0 ? Size.X / ViewBox.Size.X : 1,
ViewBox.Size.Y > 0 ? Size.Y / ViewBox.Size.Y : 1);
canvas.Translate(-ViewBox.Offset.X, -ViewBox.Offset.Y); // translate before scaling
}

dst.Rewind();
var clipRect = new SKRect(viewBox.Offset.X, viewBox.Offset.Y, viewBox.Offset.X + viewBox.Size.X, viewBox.Offset.Y + viewBox.Size.Y);
dst.AddRect(clipRect);
if (viewBox.IsAncestorClip)
if (_shapes is { Count: not 0 } shapes)
{
Matrix4x4.Invert(TotalMatrix, out var totalMatrixInverted);
var childToParentTransform = Parent!.TotalMatrix * totalMatrixInverted;
if (!childToParentTransform.IsIdentity)
for (var i = 0; i < shapes.Count; i++)
{
dst.Transform(childToParentTransform.ToSKMatrix());
shapes[i].Render(in session);
}
}

return true;
base.Paint(in session);
}

/// <remarks>This does NOT take the clipping into account.</remarks>
internal virtual bool HitTest(Point point)
internal override bool HitTest(Point point)
{
if (_shapes is null)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Uno.UI.Composition/Composition/SkiaHelper.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ namespace Microsoft.UI.Composition
{
internal static class SkiaHelper
{
private static readonly ObjectPool<SKPaint> _paintPool = new(() => new SKPaint(), 8);
private static readonly ObjectPool<SKPath> _pathPool = new(() => new SKPath(), 8);
private static readonly ObjectPool<SKPaint> _paintPool = new(() => new SKPaint());
private static readonly ObjectPool<SKPath> _pathPool = new(() => new SKPath());
jeromelaban marked this conversation as resolved.
Show resolved Hide resolved

public static DisposableStruct<SKPath> GetTempSKPath(out SKPath path)
{
Expand Down
Loading
Loading