diff --git a/samples/Gallery/Shared/Samples/CreatePdfSample.cs b/samples/Gallery/Shared/Samples/CreatePdfSample.cs
index e00f1f9791..308ec71943 100644
--- a/samples/Gallery/Shared/Samples/CreatePdfSample.cs
+++ b/samples/Gallery/Shared/Samples/CreatePdfSample.cs
@@ -50,7 +50,7 @@ protected override void OnDrawSample(SKCanvas canvas, int width, int height)
private void GenerateDocument()
{
- if (isSupported && File.Exists(path))
+ if (!isSupported || (isSupported && File.Exists(path)))
return;
var metadata = new SKDocumentPdfMetadata
@@ -70,7 +70,6 @@ private void GenerateDocument()
if (document == null)
{
isSupported = false;
- Refresh();
return;
}
diff --git a/samples/Gallery/Shared/Samples/CreateXpsSample.cs b/samples/Gallery/Shared/Samples/CreateXpsSample.cs
index 6fddfb9a17..5dbec24da8 100644
--- a/samples/Gallery/Shared/Samples/CreateXpsSample.cs
+++ b/samples/Gallery/Shared/Samples/CreateXpsSample.cs
@@ -52,7 +52,7 @@ protected override void OnDrawSample(SKCanvas canvas, int width, int height)
private void GenerateDocument()
{
- if (isSupported && File.Exists(path))
+ if (!isSupported || (isSupported && File.Exists(path)))
return;
using var document = SKDocument.CreateXps(path);
@@ -60,7 +60,6 @@ private void GenerateDocument()
if (document == null)
{
isSupported = false;
- Refresh();
return;
}
diff --git a/samples/Gallery/Uno/SkiaSharpSample.Android/Properties/AndroidManifest.xml b/samples/Gallery/Uno/SkiaSharpSample.Android/Properties/AndroidManifest.xml
index c859af2de8..10670ed130 100644
--- a/samples/Gallery/Uno/SkiaSharpSample.Android/Properties/AndroidManifest.xml
+++ b/samples/Gallery/Uno/SkiaSharpSample.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/samples/Gallery/Uno/SkiaSharpSample.Android/Resources/values/styles.xml b/samples/Gallery/Uno/SkiaSharpSample.Android/Resources/values/styles.xml
new file mode 100644
index 0000000000..09b05bf16e
--- /dev/null
+++ b/samples/Gallery/Uno/SkiaSharpSample.Android/Resources/values/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/samples/Gallery/Uno/SkiaSharpSample.Shared/MainPage.xaml b/samples/Gallery/Uno/SkiaSharpSample.Shared/MainPage.xaml
index 17aa82c4c7..7d2a40e257 100644
--- a/samples/Gallery/Uno/SkiaSharpSample.Shared/MainPage.xaml
+++ b/samples/Gallery/Uno/SkiaSharpSample.Shared/MainPage.xaml
@@ -44,6 +44,7 @@
+
@@ -54,6 +55,14 @@
OverflowButtonVisibility="Collapsed"
Foreground="White">
+
+
+
+
+
+
+
+
+ glTextureView?.CanvasSize ?? SKSize.Empty;
+
+ private GRContext GetGRContext() =>
+ glTextureView?.GRContext;
+
+ partial void DoLoaded()
+ {
+ glTextureView = new SKGLTextureView(Context);
+ DoEnableRenderLoop(EnableRenderLoop);
+ glTextureView.PaintSurface += OnPaintSurface;
+ AddView(glTextureView);
+ }
+
+ partial void DoUnloaded()
+ {
+ if (glTextureView == null)
+ return;
+
+ RemoveView(glTextureView);
+ glTextureView.PaintSurface -= OnPaintSurface;
+ glTextureView.Dispose();
+ glTextureView = null;
+ }
+
+ partial void DoEnableRenderLoop(bool enable)
+ {
+ if (glTextureView == null)
+ return;
+
+ glTextureView.RenderMode = enable
+ ? Rendermode.Continuously
+ : Rendermode.WhenDirty;
+ }
+
+ private void DoInvalidate() =>
+ glTextureView?.RequestRender();
+
+ private void OnPaintSurface(object sender, SKPaintGLSurfaceEventArgs e) =>
+ OnPaintSurface(e);
+ }
+}
diff --git a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Android/SkiaSharp.Views.Uno.Android.csproj b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Android/SkiaSharp.Views.Uno.Android.csproj
index 44b1a1d2d6..d12ec0721e 100644
--- a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Android/SkiaSharp.Views.Uno.Android.csproj
+++ b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Android/SkiaSharp.Views.Uno.Android.csproj
@@ -15,6 +15,9 @@
+
+
+
diff --git a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SKSwapChainPanel.macOS.cs b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SKSwapChainPanel.macOS.cs
new file mode 100644
index 0000000000..f4ed3f6001
--- /dev/null
+++ b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SKSwapChainPanel.macOS.cs
@@ -0,0 +1,83 @@
+using CoreVideo;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace SkiaSharp.Views.UWP
+{
+ public partial class SKSwapChainPanel : FrameworkElement
+ {
+ private SKGLView glView;
+ private CVDisplayLink displayLink;
+
+ public SKSwapChainPanel()
+ {
+ Initialize();
+ }
+
+ private SKSize GetCanvasSize() =>
+ glView?.CanvasSize ?? SKSize.Empty;
+
+ private GRContext GetGRContext() =>
+ glView?.GRContext;
+
+ partial void DoLoaded()
+ {
+ glView = new SKGLView(Bounds);
+ glView.PaintSurface += OnPaintSurface;
+ AddSubview(glView);
+ }
+
+ partial void DoUnloaded()
+ {
+ DoEnableRenderLoop(false);
+
+ if (glView != null)
+ {
+ glView.RemoveFromSuperview();
+ glView.PaintSurface -= OnPaintSurface;
+ glView.Dispose();
+ glView = null;
+ }
+ }
+
+ private void DoInvalidate() =>
+ DoEnableRenderLoop(true);
+
+ private void OnPaintSurface(object sender, SKPaintGLSurfaceEventArgs e) =>
+ OnPaintSurface(e);
+
+ partial void DoEnableRenderLoop(bool enable)
+ {
+ // stop the render loop
+ if (!enable)
+ {
+ if (displayLink != null)
+ {
+ displayLink.Stop();
+ displayLink.Dispose();
+ displayLink = null;
+ }
+ return;
+ }
+
+ // only start if we haven't already
+ if (displayLink != null)
+ return;
+
+ // create the loop
+ displayLink = new CVDisplayLink();
+ displayLink.SetOutputCallback(delegate
+ {
+ // redraw the view
+ glView?.BeginInvokeOnMainThread(() => glView?.Display());
+
+ // stop the render loop if it has been disabled or the views are disposed
+ if (glView == null || !EnableRenderLoop)
+ DoEnableRenderLoop(false);
+
+ return CVReturn.Success;
+ });
+ displayLink.Start();
+ }
+ }
+}
diff --git a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SkiaSharp.Views.Uno.Mac.csproj b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SkiaSharp.Views.Uno.Mac.csproj
index 6e5501b18d..ae44ed81c1 100644
--- a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SkiaSharp.Views.Uno.Mac.csproj
+++ b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Mac/SkiaSharp.Views.Uno.Mac.csproj
@@ -16,6 +16,7 @@
+
diff --git a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/SKSwapChainPanel.Wasm.cs b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/SKSwapChainPanel.Wasm.cs
new file mode 100644
index 0000000000..155ec446a9
--- /dev/null
+++ b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/SKSwapChainPanel.Wasm.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Threading;
+using Uno.Foundation;
+using Uno.Foundation.Interop;
+using Windows.UI.Xaml;
+
+namespace SkiaSharp.Views.UWP
+{
+ public partial class SKSwapChainPanel : FrameworkElement
+ {
+ private const int ResourceCacheBytes = 256 * 1024 * 1024; // 256 MB
+ private const SKColorType colorType = SKColorType.Rgba8888;
+ private const GRSurfaceOrigin surfaceOrigin = GRSurfaceOrigin.BottomLeft;
+
+ private readonly SKSwapChainPanelJsInterop jsInterop;
+
+ private GRGlInterface glInterface;
+ private GRContext context;
+ private JsInfo jsInfo;
+ private GRGlFramebufferInfo glInfo;
+ private GRBackendRenderTarget renderTarget;
+ private SKSurface surface;
+ private SKCanvas canvas;
+
+ private SKSizeI lastSize;
+
+ public SKSwapChainPanel()
+ : base("canvas")
+ {
+ jsInterop = new SKSwapChainPanelJsInterop(this);
+ Initialize();
+ }
+
+ private SKSize GetCanvasSize() => lastSize;
+
+ private GRContext GetGRContext() => context;
+
+ partial void DoLoaded()
+ {
+ jsInfo = jsInterop.CreateContext();
+
+ Invalidate();
+ }
+
+ partial void DoEnableRenderLoop(bool enable) =>
+ jsInterop.SetEnableRenderLoop(enable);
+
+ //partial void DoUpdateBounds() =>
+ // jsInterop.ResizeCanvas();
+
+ private void DoInvalidate()
+ {
+ if (designMode)
+ return;
+
+ if (!isVisible)
+ return;
+
+ if ((int)ActualWidth <= 0 || (int)ActualHeight <= 0)
+ return;
+
+ jsInterop.RequestAnimationFrame(EnableRenderLoop);
+ }
+
+ internal void RenderFrame()
+ {
+ if (!jsInfo.IsValid)
+ return;
+
+ // create the SkiaSharp context
+ if (context == null)
+ {
+ glInterface = GRGlInterface.Create();
+ context = GRContext.CreateGl(glInterface);
+
+ // bump the default resource cache limit
+ context.SetResourceCacheLimit(ResourceCacheBytes);
+ }
+
+ // get the new surface size
+ var newSize = new SKSizeI((int)(ActualWidth * ContentsScale), (int)(ActualHeight * ContentsScale));
+
+ // manage the drawing surface
+ if (renderTarget == null || lastSize != newSize || !renderTarget.IsValid)
+ {
+ // create or update the dimensions
+ lastSize = newSize;
+
+ glInfo = new GRGlFramebufferInfo(jsInfo.FboId, colorType.ToGlSizedFormat());
+
+ // destroy the old surface
+ surface?.Dispose();
+ surface = null;
+ canvas = null;
+
+ // re-create the render target
+ renderTarget?.Dispose();
+ renderTarget = new GRBackendRenderTarget(newSize.Width, newSize.Height, jsInfo.Samples, jsInfo.Stencil, glInfo);
+ }
+
+ // create the surface
+ if (surface == null)
+ {
+ surface = SKSurface.Create(context, renderTarget, surfaceOrigin, colorType);
+ canvas = surface.Canvas;
+ }
+
+ using (new SKAutoCanvasRestore(canvas, true))
+ {
+ // start drawing
+ OnPaintSurface(new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType, glInfo));
+ }
+
+ // update the control
+ canvas.Flush();
+ context.Flush();
+ }
+
+ private struct JsInfo
+ {
+ public bool IsValid { get; set; }
+
+ public int ContextId { get; set; }
+
+ public uint FboId { get; set; }
+
+ public int Stencil { get; set; }
+
+ public int Samples { get; set; }
+
+ public int Depth { get; set; }
+ }
+
+ private class SKSwapChainPanelJsInterop : IJSObject, IJSObjectMetadata
+ {
+ private static long handleCounter = 0L;
+
+ private readonly long jsHandle;
+
+ public SKSwapChainPanelJsInterop(SKSwapChainPanel panel)
+ {
+ Panel = panel ?? throw new ArgumentNullException(nameof(panel));
+
+ jsHandle = Interlocked.Increment(ref handleCounter);
+ Handle = JSObjectHandle.Create(this, this);
+ }
+
+ public SKSwapChainPanel Panel { get; }
+
+ public JSObjectHandle Handle { get; }
+
+ public void RenderFrame() =>
+ Panel.RenderFrame();
+
+ public void RequestAnimationFrame(bool renderLoop) =>
+ WebAssemblyRuntime.InvokeJSWithInterop($"{this}.requestAnimationFrame({(renderLoop ? "true" : "false")});");
+
+ public void SetEnableRenderLoop(bool enable) =>
+ WebAssemblyRuntime.InvokeJSWithInterop($"{this}.setEnableRenderLoop({(enable ? "true" : "false")});");
+
+ public void ResizeCanvas() =>
+ WebAssemblyRuntime.InvokeJSWithInterop($"{this}.resizeCanvas();");
+
+ public JsInfo CreateContext()
+ {
+ var resultString = WebAssemblyRuntime.InvokeJSWithInterop($"return {this}.createContext('{Panel.HtmlId}');");
+ var result = resultString?.Split(',');
+ if (result?.Length != 5)
+ return default;
+
+ return new JsInfo
+ {
+ IsValid = true,
+ ContextId = int.Parse(result[0]),
+ FboId = uint.Parse(result[1]),
+ Stencil = int.Parse(result[2]),
+ Samples = int.Parse(result[3]),
+ Depth = int.Parse(result[4]),
+ };
+ }
+
+ long IJSObjectMetadata.CreateNativeInstance(IntPtr managedHandle)
+ {
+ WebAssemblyRuntime.InvokeJS($"SkiaSharp.Views.UWP.SKSwapChainPanel.createInstance('{managedHandle}', '{jsHandle}')");
+ return jsHandle;
+ }
+
+ string IJSObjectMetadata.GetNativeInstance(IntPtr managedHandle, long jsHandle) =>
+ $"SkiaSharp.Views.UWP.SKSwapChainPanel.getInstance('{jsHandle}')";
+
+ void IJSObjectMetadata.DestroyNativeInstance(IntPtr managedHandle, long jsHandle) =>
+ WebAssemblyRuntime.InvokeJS($"SkiaSharp.Views.UWP.SKSwapChainPanel.destroyInstance('{jsHandle}')");
+
+ object IJSObjectMetadata.InvokeManaged(object instance, string method, string parameters)
+ {
+ switch (method)
+ {
+ case nameof(RenderFrame):
+ RenderFrame();
+ break;
+
+ default:
+ throw new ArgumentException($"Unable to execute method: {method}", nameof(method));
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/WasmScripts/SkiaSharp.Views.Uno.Wasm.js b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/WasmScripts/SkiaSharp.Views.Uno.Wasm.js
index e9dc668df9..de4ad9f683 100644
--- a/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/WasmScripts/SkiaSharp.Views.Uno.Wasm.js
+++ b/source/SkiaSharp.Views.Uno/SkiaSharp.Views.Uno.Wasm/WasmScripts/SkiaSharp.Views.Uno.Wasm.js
@@ -22,7 +22,149 @@
return true;
}
}
+
+ class SKSwapChainPanel {
+ static activeInstances = {};
+
+ constructor(managedHandle) {
+ this.managedHandle = managedHandle;
+ this.canvas = undefined;
+ this.jsInfo = undefined;
+ this.renderLoop = false;
+ this.currentRequest = 0;
+ }
+
+ // JSObject
+ static createInstance(managedHandle, jsHandle) {
+ SKSwapChainPanel.activeInstances[jsHandle] = new SKSwapChainPanel(managedHandle);
+ }
+ static getInstance(jsHandle) {
+ return SKSwapChainPanel.activeInstances[jsHandle];
+ }
+ static destroyInstance(jsHandle) {
+ delete SKSwapChainPanel.activeInstances[jsHandle];
+ }
+
+ requestAnimationFrame(renderLoop) {
+ // optionally update the render loop
+ if (renderLoop !== undefined && this.renderLoop !== renderLoop)
+ this.setEnableRenderLoop(renderLoop);
+
+ // skip because we have a render loop
+ if (this.currentRequest !== 0)
+ return;
+
+ // make sure the canvas is scaled correctly for the drawing
+ this.resizeCanvas();
+
+ // add the draw to the next frame
+ this.currentRequest = window.requestAnimationFrame(() => {
+ Uno.Foundation.Interop.ManagedObject.dispatch(this.managedHandle, 'RenderFrame', null);
+
+ this.currentRequest = 0;
+
+ // we may want to draw the next frame
+ if (this.renderLoop)
+ this.requestAnimationFrame();
+ });
+ }
+
+ resizeCanvas() {
+ if (!this.canvas)
+ return;
+
+ var scale = window.devicePixelRatio || 1;
+ var w = this.canvas.clientWidth * scale
+ var h = this.canvas.clientHeight * scale;
+
+ if (this.canvas.width !== w)
+ this.canvas.width = w;
+ if (this.canvas.height !== h)
+ this.canvas.height = h;
+ }
+
+ setEnableRenderLoop(enable) {
+ this.renderLoop = enable;
+
+ // either start the new frame or cancel the existing one
+ if (enable) {
+ this.requestAnimationFrame();
+ } else if (this.currentRequest !== 0) {
+ window.cancelAnimationFrame(this.currentRequest);
+ this.currentRequest = 0;
+ }
+ }
+
+ createContext(canvasOrCanvasId) {
+ if (!canvasOrCanvasId)
+ throw 'No