Skip to content

Add touch/pointer event support to Blazor views and revamp sample app#3528

Closed
Copilot wants to merge 25 commits intomainfrom
copilot/add-touch-event-blazor-canvas
Closed

Add touch/pointer event support to Blazor views and revamp sample app#3528
Copilot wants to merge 25 commits intomainfrom
copilot/add-touch-event-blazor-canvas

Conversation

Copy link
Contributor

Copilot AI commented Feb 26, 2026

Description

Adds touch/pointer event support to the Blazor SKCanvasView and SKGLView components, and revamps the Blazor WebAssembly sample app into a three-page showcase of SkiaSharp capabilities.

Touch/pointer event system

Shared touch types

SKTouchAction, SKTouchDeviceType, SKMouseButton, and SKTouchEventArgs are moved from SkiaSharp.Views.Maui.Core to SkiaSharp.Views.Shared with #if __BLAZOR__ / #if __MAUI__ namespace switching — the same shared-file pattern used by SKPaintSurfaceEventArgs.

JS interop (SKTouchInterop.ts)

A class-based module (matching the SizeWatcher/DpiWatcher pattern) that:

  • Listens for pointer* and wheel events on the canvas element.
  • Maps browser events to SKTouchAction/SKMouseButton/SKTouchDeviceType enum values.
  • Invokes the C# callback synchronously and reads back the Handled return value.
  • When Handled is true: calls preventDefault() + stopPropagation() (e.g., blocks page scrolling for handled wheel events).
  • When Handled is false: browser default behavior is preserved.
  • start() adds all listeners with setPointerCapture; stop() removes them and releases capture.
  • Normalizes wheel delta to the v120 standard (120 = one mouse notch) across browsers and platforms.

C# interop (SKTouchInterop.cs)

Inherits from JSModuleInterop with #if NET7_0_OR_GREATER split:

  • NET7+: [JSImport] static partial methods with Func<JSObject, bool> callback. The JSObject proxy is disposed via using var _ after property extraction to prevent GC/finalizer pressure from high-frequency pointer events.
  • Pre-NET7: DotNetObjectReference<SKTouchCallbackHelper> with [JSInvokable] bool OnPointerEvent(...).

View changes (SKCanvasView & SKGLView)

New component parameters:

Parameter Type Description
Touch Action<SKTouchEventArgs>? Callback for pointer/touch/wheel events
EnableTouchEvents bool Opt-in; when false (default), no JS listeners are attached

The JS module is imported on first render but listeners are not attached until EnableTouchEvents is set to true. Toggling at runtime calls Start()/Stop() to fully connect/disconnect all JS event listeners.

Blazor sample app revamp

The basic Blazor WebAssembly sample is rebuilt from the ground up into three pages:

Page What it demonstrates
Home CPU-rendered gradient background with animated circles, text rendering with SKFont/SKTextBlob, and SKCanvasView basics
GPU SKGLView with a real-time SkSL metaball shader, touch interaction (drag to attract blobs), and FPS overlay
Drawing Multi-touch drawing canvas with stroke recording, color picker toolbar, wheel-to-resize brush, and undo support

Sample features exercised

  • SKCanvasView with OnPaintSurface (Home)
  • SKGLView with EnableRenderLoop and OnPaintSurface (GPU)
  • Touch events: Pressed, Moved, Released, WheelChanged (Drawing, GPU)
  • Handled feedback loop — blocks page scroll during drawing, allows scroll on Home
  • SKRuntimeEffect.BuildShader with SkSL (GPU)
  • SKPath stroke recording and replay (Drawing)
  • Scoped .razor.css for component-level styling (GPU, Drawing)

API Changes

Added:

  • enum SKTouchAction { Entered, Pressed, Moved, Released, Cancelled, Exited, WheelChanged } (shared)
  • enum SKTouchDeviceType { Touch, Mouse, Pen } (shared)
  • enum SKMouseButton { Unknown, Left, Middle, Right } (shared)
  • class SKTouchEventArgs : EventArgs — Id, ActionType, DeviceType, MouseButton, Location, InContact, WheelDelta, Pressure, Handled (shared)
  • Action<SKTouchEventArgs>? SKCanvasView.Touch
  • bool SKCanvasView.EnableTouchEvents
  • Action<SKTouchEventArgs>? SKGLView.Touch
  • bool SKGLView.EnableTouchEvents

Moved (no breaking change):

  • SKTouchEventArgs, SKTouchAction, SKTouchDeviceType, SKMouseButton moved from SkiaSharp.Views.Maui.Core/SKTouchEventArgs.cs to SkiaSharp.Views.Shared/SKTouchEventArgs.cs with conditional namespace via #if __BLAZOR__ / #if __MAUI__

Behavioral Changes

Touch events are opt-in via EnableTouchEvents="true". When false (default), no JS event listeners are attached. Existing usage is unaffected.

When touch events are enabled:

  • e.Handled = true → JS calls preventDefault()/stopPropagation() on the browser event
  • e.Handled = false (default) → browser default behavior is preserved

Usage

<SKCanvasView OnPaintSurface="OnPaintSurface"
              Touch="OnTouch" EnableTouchEvents="true" />

@code {
    void OnTouch(SKTouchEventArgs e)
    {
        var point = e.Location;    // CSS pixels
        var device = e.DeviceType; // Touch, Mouse, or Pen
        e.Handled = true;          // preventDefault + stopPropagation in browser
    }
}

PR Checklist

  • Has tests (no existing Blazor test project; recommend tests/SkiaSharp.Views.Blazor.Tests with bUnit)
  • Rebased on top of main
  • Changes adhere to coding standard
  • Updated sample app

Copilot AI changed the title [WIP] Add Touch event to Blazor canvas view Add touch/pointer event support to Blazor SKCanvasView Feb 26, 2026
@mattleibow
Copy link
Contributor

@copilot we need to add this to the GL view as well.

Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

@copilot we need to add this to the GL view as well.

Added the same Touch EventCallback, EnableTouchEvents parameter, touch interop initialization, and IAsyncDisposable to SKGLView in commit 20724c4.

Copilot AI changed the title Add touch/pointer event support to Blazor SKCanvasView Add touch/pointer event support to Blazor SKCanvasView and SKGLView Feb 26, 2026
Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • crl.godaddy.com
    • Triggering command: /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/SkiaSharp/SkiaSharp/samples/Basic/UnoPlatform/SkiaSharpSample.sln --packages /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true /p:EnableWindowsTargeting=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • crl.godaddy.com
    • Triggering command: /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/SkiaSharp/SkiaSharp/samples/Basic/UnoPlatform/SkiaSharpSample.sln --packages /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true /p:EnableWindowsTargeting=true (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in pointer/touch event support to the Blazor SKCanvasView and SKGLView, aligning the event args/enums shape with MAUI for source compatibility and forwarding browser pointer/wheel events via JS interop.

Changes:

  • Move/share SKTouchEventArgs + related enums into SkiaSharp.Views.Shared with __MAUI__ / __BLAZOR__ namespace switching.
  • Add Blazor JS + C# interop (SKTouchInterop.*, SKTouchCallbackHelper) to capture pointer/wheel events and forward them to .NET.
  • Add Touch and EnableTouchEvents parameters to SKCanvasView/SKGLView and implement async disposal for JS cleanup.

Reviewed changes

Copilot reviewed 9 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
source/SkiaSharp.Views/SkiaSharp.Views.Shared/SKTouchEventArgs.cs Shared touch event args/enums with MAUI/Blazor namespace switching.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/wwwroot/SKTouchInterop.ts Pointer/wheel event listeners and mapping to touch args payload.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/wwwroot/SKTouchInterop.js Built JS asset for the touch interop module.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/wwwroot/SKTouchInterop.js.map Source map for the built touch interop JS.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/SkiaSharp.Views.Blazor.csproj Includes shared touch types in Blazor build.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/SKCanvasView.razor.cs Adds Touch/EnableTouchEvents, creates/disposes touch interop.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/SKGLView.razor.cs Adds Touch/EnableTouchEvents, creates/disposes touch interop.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/Internal/SKTouchInterop.cs Manages JS module lifetime and listener initialization/disposal.
source/SkiaSharp.Views/SkiaSharp.Views.Blazor/Internal/SKTouchCallbackHelper.cs Receives [JSInvokable] callbacks and forwards payloads to C# handlers.
source/SkiaSharp.Views.Maui/SkiaSharp.Views.Maui.Core/SkiaSharp.Views.Maui.Core.csproj Includes shared touch types in MAUI build.
.gitignore Ignores .nuget/ artifacts.
Comments suppressed due to low confidence (1)

source/SkiaSharp.Views/SkiaSharp.Views.Shared/SKTouchEventArgs.cs:5

  • using Microsoft.Maui; is now wrapped in #if __MAUI__, but this file no longer references any Microsoft.Maui types, so the using is unused and can generate warnings in MAUI builds. Consider removing it (or adding an alias/usage if still intended).

Copy link
Contributor

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please look at how we did interop for other features and make sure to match that.

Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

@copilot Please look at how we did interop for other features and make sure to match that.

Refactored all touch interop to match the existing patterns in commit d047799:

  • SKTouchInterop inherits from JSModuleInterop with #if NET7_0_OR_GREATER split (like SizeWatcherInterop, DpiWatcherInterop)
  • SKTouchCallbackHelper guarded with #if !NET7_0_OR_GREATER (like ActionHelper, FloatFloatActionHelper)
  • Touch parameter changed to Action<SKTouchEventArgs>? (like OnPaintSurface)
  • TypeScript rewritten as class-based module with invokeMethod (sync) and typeof callback === 'function' pattern (like SizeWatcher, DpiWatcher)
  • No more IAsyncDisposable — disposal goes through JSModuleInterop.Dispose()

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • crl.godaddy.com
    • Triggering command: /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/SkiaSharp/SkiaSharp/samples/Basic/UnoPlatform/SkiaSharpSample.sln --packages /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true /p:EnableWindowsTargeting=true SkiaSharp.Views.Blazor/wwwroot/SKTouchInterop.js.map (dns block)
    • Triggering command: /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/SkiaSharp/SkiaSharp/samples/Gallery/Uno.WinUI/SkiaSharpSample.Skia.Gtk/SkiaSharpSample.Skia.Gtk.csproj --packages /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true /p:EnableWindowsTargeting=true SkiaSharp.Views//tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages (dns block)
    • Triggering command: /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/.dotnet/dotnet restore --no-dependencies /home/REDACTED/work/SkiaSharp/SkiaSharp/samples/Gallery/Uno/SkiaSharpSample.Wasm/SkiaSharpSample.Wasm.csproj --packages /tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/packages /p:DisableImplicitNuGetFallbackFolder=true --verbosity normal /p:TargetFrameworkRootPath=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:NetCoreTargetingPackRoot=/tmp/codeql-scratch-04aa162449f907a2/dbs/csharp/working/emptyFakeDotnetRoot /p:AllowMissingPrunePackageData=true /p:EnableWindowsTargeting=true SkiaSharp.Views/SkiaSharp.Views.Blazor/SKGLView.razor.cs embly/SkiaSharpSample.sln working/.dotnet/dotnet-install.sh (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Contributor

@mattleibow mattleibow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Please update the blazor basic sample to now use these touch events and remove the other attributes.

Also address the comments

mattleibow and others added 9 commits February 28, 2026 02:12
- Home: gradient background with scattered circles and feature overview
- Raster: simplified to pure rendering demo (shapes, gradient, text)
- GPU: polished description for animated SKGLView clock
- Drawing (NEW): interactive canvas with touch drawing, mouse wheel
  brush size control (v120 delta), pressure sensitivity, color picker,
  and clear button — demonstrates touch, wheel, and Handled feedback
- NavMenu: updated with all four page links

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Home: gradient + circles landing page (SKCanvasView basics)
- GPU: pure SkSL lava lamp with 6 metaballs, touch interaction,
  SKRuntimeEffect.BuildShader() + Uniforms + Build() pattern
- Drawing: touch/pointer drawing with wheel brush size control,
  pressure sensitivity, color picker, overlaid toolbar
- Remove redundant Raster page (covered by Home + Drawing)
- Full-viewport flex layout with overflow: hidden
- Add detailed comments for new users learning SkiaSharp
- Update NavMenu with GPU card and pencil icons

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract all inline CSS from GPU.razor and Drawing.razor into
page-scoped stylesheets (GPU.razor.css, Drawing.razor.css).
Only dynamic values (color swatch backgrounds) remain inline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract GPU and Drawing code to .razor.cs partial classes
- Use C# 11 raw string literal for SkSL shader source
- Make DrawingStroke a record
- Remove unused SKGLView ref from GPU page
- Remove pen pressure logic from Drawing for simplicity
- Initialize shader builder as a field instead of lazy in render

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Revert code-behind split — keep everything in single .razor files
for simplicity. Use @"" verbatim string for SkSL since Razor
doesn't support C# 11 raw string literals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move @code blocks to .razor.cs code-behind partial classes to
enable C# 11 raw string literals (triple-quote) for the SkSL
shader source. Razor markup stays in .razor files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Razor compiler doesn't support C# 11 raw string literals until
.NET 10. Use @"" verbatim strings for SkSL and keep everything
in single .razor files for simplicity. Delete code-behind files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Lift reusable SKPaint to readonly fields with target-typed new()
- Use collection expression syntax instead of new[]
- Add period-terminated comments on fields
- Wrap <p> content across multiple lines
- Add blank line after @code {

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

@mattleibow mattleibow changed the title Add touch/pointer event support to Blazor SKCanvasView and SKGLView Add touch/pointer event support to Blazor views and revamp sample app Feb 28, 2026
@mattleibow
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 22 changed files in this pull request and generated 3 comments.

The file lives in SkiaSharp.Views.Shared which is compiled by
SkiaSharp.Views.csproj for native platforms (iOS, Android, etc.)
via a wildcard include. Those platforms define neither __MAUI__
nor __BLAZOR__, so the namespace block was stripped and the bare
braces caused CS1513/CS1022. Wrapping the entire file ensures it
compiles to nothing on native platform builds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

No pipelines are associated with this pull request.

Copilot AI added a commit that referenced this pull request Feb 28, 2026
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
@mattleibow mattleibow marked this pull request as draft March 1, 2026 20:43
@mattleibow
Copy link
Contributor

Closing this as we do not want to be adding APIs to platforms. And there is no need to actually, the events are fine.

@mattleibow mattleibow closed this Mar 2, 2026
@github-project-automation github-project-automation bot moved this from Changes Requested to Done in SkiaSharp Backlog Mar 2, 2026
@mattleibow mattleibow deleted the copilot/add-touch-event-blazor-canvas branch March 2, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Add wheel support to Blazor WASM views with v120 normalization

3 participants