Skip to content

Commit

Permalink
Merge pull request #18181 from ramezgerges/skcanvaselement
Browse files Browse the repository at this point in the history
feat(skia): SKCanvasElement
  • Loading branch information
ramezgerges authored Sep 21, 2024
2 parents 56f9237 + a769608 commit e4d84cd
Show file tree
Hide file tree
Showing 19 changed files with 393 additions and 33 deletions.
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 yet 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.

> [!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 @@ -4698,6 +4698,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 @@ -6044,6 +6048,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 @@ -8295,6 +8300,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

0 comments on commit e4d84cd

Please sign in to comment.