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): SKCanvasElement #18181

Merged
merged 10 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/filters/Uno.UI-packages-skia.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"AddIns\\Uno.UI.MediaPlayer.Skia.Gtk\\Uno.UI.MediaPlayer.Skia.Gtk.csproj",
"AddIns\\Uno.UI.Svg\\Uno.UI.Svg.Skia.csproj",
"AddIns\\Uno.WinUI.Graphics3DGL\\Uno.WinUI.Graphics3DGL.csproj",
"AddIns\\Uno.WinUI.Graphics2DSK\\Uno.WinUI.Graphics2DSK.csproj",
"SourceGenerators\\System.Xaml\\Uno.Xaml.csproj",
"SourceGenerators\\Uno.UI.SourceGenerators.Internal\\Uno.UI.SourceGenerators.Internal.csproj",
"SourceGenerators\\Uno.UI.SourceGenerators\\Uno.UI.SourceGenerators.csproj",
Expand Down
45 changes: 26 additions & 19 deletions doc/articles/controls/SKCanvasElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ uid: Uno.Controls.SKCanvasElement

When creating an Uno Platform application, developers might want to create elaborate 2D graphics using a library such as [Skia](https://skia.org) or [Cairo](https://www.cairographics.org), rather than using, for example, a simple [Canvas](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.canvas). To support this use case, SkiaSharp comes with an [SKXamlCanvas](https://learn.microsoft.com/dotnet/api/skiasharp.views.windows.skxamlcanvas) element that allows for drawing in an area using SkiaSharp.

On Uno Platform Skia Desktop targets, we can utilize the pre-existing internal Skia canvas used to render the application window instead of creating additional Skia surfaces. This way, a lot of Skia functionally can be acquired "for free". For example, unlike `SKXamlCanvas` which doesn't support hardware acceleration on Skia targets, hardware acceleration comes out of the box if the Uno application is already using OpenGL to render. Moreover, `SKXamlCanvas` has to make additional buffer copying, which can be skipped with this implementation.
On Uno Platform Skia targets, we can utilize the pre-existing internal Skia canvas used to render the application window instead of creating additional Skia surfaces. Unlike `SKXamlCanvas` which doesn't support hardware acceleration on Skia targets, hardware acceleration comes out of the box if the Uno application is already using OpenGL to render. Moreover, `SKXamlCanvas` has to make additional buffer copying, which can be skipped with this implementation.
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved

> [!IMPORTANT]
> This functionality is only available on Skia Desktop (`netX.0-desktop`) targets.
> This functionality is only available on Skia targets.

## SKCanvasElement

Expand All @@ -19,13 +19,11 @@ On Uno Platform Skia Desktop targets, we can utilize the pre-existing internal S
protected abstract void RenderOverride(SKCanvas canvas, Size area);
```

When adding your drawing logic in `RenderOverride` on the provided canvas, you can assume that the origin is already translated so that `0,0` is the origin of the visual, not the entire window. Drawing outside this area will be clipped.
When adding your drawing logic in `RenderOverride` on the provided canvas, you can assume that the origin is already translated so that `0,0` is the origin of the element, not the entire window. Drawing outside this area will be clipped.

Additionally, `SKCanvasElement` has an `Invalidate` method that can be used at any time to tell the Uno Platform runtime to redraw the visual, calling `RenderOverride` in the process.
Additionally, `SKCanvasElement` has an `Invalidate` method that can be used at any time to tell the Uno Platform runtime to redraw the window, calling `RenderOverride` in the process.

By default, an `SKCanvasElement` takes all the available space given to it in the `Measure` cycle. If you want to customize how much space the element takes, you can override its `MeasureOverride` and `ArrangeOverride` methods.

Note that since an `SKCanvasElement` takes as much space as it can, it's not allowed to place an `SKCanvasElement` inside a `StackPanel`, a `Grid` with `Auto` sizing, or any other element that provides its child(ren) with infinite space. To work around this, you can explicitly set the `Width` and/or `Height` of the `SKCanvasElement`.
Since `SKCanvasElement` is just a FrameworkElement, controlling the dimensions of the drawing area is done by manipulating the layout of the element, e.g. by overriding MeasureOverride and ArrangeOverride.

## Full example

Expand All @@ -43,7 +41,7 @@ XAML:
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:skia="http://uno.ui/skia"
xmlns:not_skia="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
mc:Ignorable="d skia not_skia"
mc:Ignorable="d skia"
d:DesignHeight="300"
d:DesignWidth="400">

Expand All @@ -65,21 +63,35 @@ Code-behind:

```csharp
// SKCanvasElementExample.xaml.cs
public partial class SKCanvasElementExample : UserControl
using Uno.UI.Samples.Controls;
using Microsoft.UI.Xaml.Controls;

namespace BlankApp
{
public sealed partial class SKCanvasElement_Simple : UserControl
{
#if __SKIA__
public int MaxSampleIndex => SKCanvasElementImpl.SampleCount - 1;
public int MaxSampleIndex => SKCanvasElementImpl.SampleCount - 1;
#endif

public SKCanvasElementExample()
{
this.InitializeComponent();
public SKCanvasElement_Simple()
{
this.InitializeComponent();
}
}
}
```

```csharp
// SKCanvasElementImpl.skia.cs <-- NOTICE the `.skia`
using System;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using SkiaSharp;

namespace BlankApp;

public class SKCanvasElementImpl : SKCanvasElement
{
public static int SampleCount => 3;
Expand All @@ -104,7 +116,7 @@ public class SKCanvasElementImpl : SKCanvasElement
protected override void RenderOverride(SKCanvas canvas, Size area)
{
var minDim = Math.Min(area.Width, area.Height);
// rescale to fit the given area, assuming each drawing takes is 260x260
// rescale to fit the given area, assuming each drawing is 260x260
canvas.Scale((float)(minDim / 260), (float)(minDim / 260));

switch (Sample)
Expand All @@ -124,8 +136,6 @@ public class SKCanvasElementImpl : SKCanvasElement
// https://fiddle.skia.org/c/@shapes
private void SkiaDrawing0(SKCanvas canvas)
{
canvas.DrawColor(SKColors.White);

var paint = new SKPaint();
paint.Style = SKPaintStyle.Fill;
paint.IsAntialias = true;
Expand Down Expand Up @@ -153,8 +163,6 @@ public class SKCanvasElementImpl : SKCanvasElement
// https://fiddle.skia.org/c/@bezier_curves
private void SkiaDrawing1(SKCanvas canvas)
{
canvas.DrawColor(SKColors.White);

var paint = new SKPaint();
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 8;
Expand Down Expand Up @@ -187,7 +195,6 @@ public class SKCanvasElementImpl : SKCanvasElement
};
paint.Shader = SKShader.CreateLinearGradient(points[0], points[1], colors, SKShaderTileMode.Clamp);
paint.IsAntialias = true;
canvas.Clear(SKColors.White);
var path = Star();
canvas.DrawPath(path, paint);

Expand Down
45 changes: 45 additions & 0 deletions src/AddIns/Uno.WinUI.Graphics2DSK/SKCanvasElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Numerics;
using Windows.Foundation;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using SkiaSharp;

namespace Uno.WinUI.Graphics2DSK;

/// <summary>
/// A <see cref="FrameworkElement"/> that exposes the ability to draw directly on the window using SkiaSharp.
/// </summary>
/// <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());

/// <summary>
/// Queue a rendering cycle that will call <see cref="RenderOverride"/>.
/// </summary>
public void Invalidate() => _visual.Compositor.InvalidateRender(_visual);

/// <summary>
/// The SkiaSharp drawing logic goes here.
/// </summary>
/// <param name="canvas">The SKCanvas that should be drawn on.</param>
/// <param name="area">The dimensions of the clipping area.</param>
/// <remarks>
/// When called, the <paramref name="canvas"/> is already set up such that the origin (0,0) is at the top-left of the clipping area.
/// Drawing outside this area (i.e. outside the (0, 0, area.Width, area.Height) rectangle will be clipped out.
/// </remarks>
protected abstract void RenderOverride(SKCanvas canvas, Size area);

private class SKCanvasVisual(SKCanvasElement owner, Compositor compositor) : ShapeVisual(compositor)
{
internal override void Paint(in PaintingSession session)
{
// We save and restore the canvas state ourselves so that the inheritor doesn't accidentally forget to.
session.Canvas.Save();
// clipping here guarantees that drawing doesn't get outside the intended area
session.Canvas.ClipRect(new SKRect(0, 0, Size.X, Size.Y));
owner.RenderOverride(session.Canvas, Size.ToSize());
session.Canvas.Restore();
}
}
}
24 changes: 24 additions & 0 deletions src/AddIns/Uno.WinUI.Graphics2DSK/Uno.WinUI.Graphics2DSK.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetSkiaPreviousAndCurrent)</TargetFrameworks>

<RootNamespace>Uno.WinUI.Graphics2DSK</RootNamespace>
<GeneratePackageOnBuild Condition="'$(UNO_UWP_BUILD)'!='true' And '$(Configuration)'=='Release'">true</GeneratePackageOnBuild>

<Nullable>enable</Nullable>
</PropertyGroup>

<Import Project="../../targetframework-override.props" />

<ItemGroup>
<PackageReference Include="SkiaSharp" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Uno.UI\Uno.UI.Skia.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\Common\uno.png" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
12 changes: 1 addition & 11 deletions src/AddIns/Uno.WinUI.Graphics3DGL/Uno.WinUI.Graphics3DGL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetFrameworks>$(NetSkiaPreviousAndCurrent)</TargetFrameworks>
<TargetFrameworks Condition="$(BuildGraphics3DGLForWindows) != ''">$(NetUWPOrWinUI)</TargetFrameworks>

<RootNamespace>Uno.WinUI.GLCanvasElement</RootNamespace>
<RootNamespace>Uno.WinUI.Graphics3DGL</RootNamespace>
<GeneratePackageOnBuild Condition="'$(Configuration)'=='Release'">true</GeneratePackageOnBuild>

<Nullable>enable</Nullable>
Expand Down Expand Up @@ -51,14 +51,4 @@
<ItemGroup>
<None Include="..\..\Common\uno.png" Pack="true" PackagePath="\" />
</ItemGroup>

<Target Name="_UnoRemoveTransitiveWPFDependency" BeforeTargets="_CheckForTransitiveWindowsDesktopDependencies">
<ItemGroup>
<!--
This removes the transitive dependency to WPF which has a requirement to
have net8.0-windows and not net8.0.
-->
<TransitiveFrameworkReference Remove="Microsoft.WindowsDesktop.App.WPF" />
</ItemGroup>
</Target>
</Project>
1 change: 1 addition & 0 deletions src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

<ItemGroup>
<ProjectReference Include="..\..\AddIns\Uno.WinUI.Graphics3DGL\Uno.WinUI.Graphics3DGL.csproj" />
<ProjectReference Include="..\AddIns\Uno.WinUI.Graphics2DSK\Uno.WinUI.Graphics2DSK.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
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 @@ -4694,6 +4694,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SKCanvasElement_Simple.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\GLCanvasElement_Simple.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -6037,6 +6041,7 @@
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\RotatingCubeGlCanvasElement.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SimpleTriangleGlCanvasElement.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SKCanvasElementImpl.skia.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Core\CloseRequestedTests.xaml.cs">
<DependentUpon>CloseRequestedTests.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -8288,6 +8293,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\VisualSurfaceTests.xaml.cs">
<DependentUpon>VisualSurfaceTests.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\SKCanvasElement_Simple.xaml.cs">
<DependentUpon>SKCanvasElement_Simple.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Composition\GLCanvasElement_Simple.xaml.cs">
<DependentUpon>GLCanvasElement_Simple.xaml</DependentUpon>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#if __SKIA__
using System;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Uno.WinUI.Graphics2DSK;
using SkiaSharp;

namespace UITests.Shared.Windows_UI_Composition;

public class SKCanvasElementImpl : SKCanvasElement
{
public static int SampleCount => 3;

public static DependencyProperty SampleProperty { get; } = DependencyProperty.Register(
nameof(Sample),
typeof(int),
typeof(SKCanvasElementImpl),
new PropertyMetadata(0, (o, args) => ((SKCanvasElementImpl)o).SampleChanged((int)args.NewValue)));

public int Sample
{
get => (int)GetValue(SampleProperty);
set => SetValue(SampleProperty, value);
}

private void SampleChanged(int newIndex)
{
Sample = Math.Min(Math.Max(0, newIndex), SampleCount - 1);
}

protected override void RenderOverride(SKCanvas canvas, Size area)
{
var minDim = Math.Min(area.Width, area.Height);
// rescale to fit the given area, assuming each drawing is 260x260
canvas.Scale((float)(minDim / 260), (float)(minDim / 260));

switch (Sample)
{
case 0:
SkiaDrawing0(canvas);
break;
case 1:
SkiaDrawing1(canvas);
break;
case 2:
SkiaDrawing2(canvas);
break;
}
}

// https://fiddle.skia.org/c/@shapes
private void SkiaDrawing0(SKCanvas canvas)
{
var paint = new SKPaint();
paint.Style = SKPaintStyle.Fill;
paint.IsAntialias = true;
paint.StrokeWidth = 4;
paint.Color = new SKColor(0xff4285F4);

var rect = SKRect.Create(10, 10, 100, 160);
canvas.DrawRect(rect, paint);

var oval = new SKPath();
oval.AddRoundRect(rect, 20, 20);
oval.Offset(new SKPoint(40, 80));
paint.Color = new SKColor(0xffDB4437);
canvas.DrawPath(oval, paint);

paint.Color = new SKColor(0xff0F9D58);
canvas.DrawCircle(180, 50, 25, paint);

rect.Offset(80, 50);
paint.Color = new SKColor(0xffF4B400);
paint.Style = SKPaintStyle.Stroke;
canvas.DrawRoundRect(rect, 10, 10, paint);
}

// https://fiddle.skia.org/c/@bezier_curves
private void SkiaDrawing1(SKCanvas canvas)
{
var paint = new SKPaint();
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 8;
paint.Color = new SKColor(0xff4285F4);
paint.IsAntialias = true;
paint.StrokeCap = SKStrokeCap.Round;

var path = new SKPath();
path.MoveTo(10, 10);
path.QuadTo(256, 64, 128, 128);
path.QuadTo(10, 192, 250, 250);
canvas.DrawPath(path, paint);
}

// https://fiddle.skia.org/c/@shader
private void SkiaDrawing2(SKCanvas canvas)
{
var paint = new SKPaint();
using var pathEffect = SKPathEffect.CreateDiscrete(10.0f, 4.0f);
paint.PathEffect = pathEffect;
SKPoint[] points =
{
new SKPoint(0.0f, 0.0f),
new SKPoint(256.0f, 256.0f)
};
SKColor[] colors =
{
new SKColor(66, 133, 244),
new SKColor(15, 157, 88)
};
paint.Shader = SKShader.CreateLinearGradient(points[0], points[1], colors, SKShaderTileMode.Clamp);
paint.IsAntialias = true;
var path = Star();
canvas.DrawPath(path, paint);

SKPath Star()
{
const float R = 60.0f, C = 128.0f;
var path = new SKPath();
path.MoveTo(C + R, C);
for (var i = 1; i < 15; ++i)
{
var a = 0.44879895f * i;
var r = R + R * (i % 2);
path.LineTo((float)(C + r * Math.Cos(a)), (float)(C + r * Math.Sin(a)));
}
return path;
}
}
}
#endif
Loading
Loading