+ Draw on the canvas below with mouse, finger, or stylus.
+ Each device type draws in a different color. Action counters and an event log update live.
+
+
+
+
+ Strokes: @_strokes.Count
+
+
+
+
+
+
+
+
+
Device Types
+
+ @foreach (var device in DeviceColors)
+ {
+ var count = _deviceCounts.GetValueOrDefault(device.Name);
+
+
+
@device.Name
+
@count
+
+ }
+
+
+
+
Touch Actions
+
+ @foreach (var action in ActionNames)
+ {
+ var count = _actionCounts.GetValueOrDefault(action);
+ var active = _lastAction == action;
+
+
@action
+
@count
+
+ }
+
+
+
+
+
+
Event Log
+
+ @foreach (var entry in _log)
+ {
+
@entry
+ }
+ @if (_log.Count == 0)
+ {
+
Interact with the canvas…
+ }
+
+
+
+@code {
+ // ── Canvas reference ──
+
+ private SKTouchCanvasView? _canvasView;
+
+ // ── Colors per device type ──
+
+ private static readonly (string Name, string Css, SKColor Sk)[] DeviceColors =
+ {
+ ("Mouse", "#6495ED", new SKColor(0xFF6495ED)), // CornflowerBlue
+ ("Touch", "#FF7F50", new SKColor(0xFFFF7F50)), // Coral
+ ("Stylus", "#3CB371", new SKColor(0xFF3CB371)), // MediumSeaGreen
+ };
+
+ private static readonly string[] ActionNames =
+ { "Entered", "Pressed", "Moved", "Released", "Exited", "Cancelled", "WheelChanged" };
+
+ // ── Stroke data ──
+
+ private record struct StrokeSegment(SKPoint Point);
+
+ private class Stroke
+ {
+ public SKTouchDeviceType DeviceType { get; init; }
+ public List Points { get; } = new();
+ }
+
+ private readonly List _strokes = new();
+ private readonly Dictionary _activeStrokes = new();
+
+ // ── Counters ──
+
+ private readonly Dictionary _actionCounts = new();
+ private readonly Dictionary _deviceCounts = new();
+ private string _lastAction = "";
+
+ // ── Event log ──
+
+ private const int MaxLogEntries = 80;
+ private readonly List _log = new();
+
+ // ── Touch handler ──
+
+ private void OnTouch(SKTouchEventArgs e)
+ {
+ // Count action
+ var actionName = e.ActionType.ToString();
+ _actionCounts[actionName] = _actionCounts.GetValueOrDefault(actionName) + 1;
+ _lastAction = actionName;
+
+ // Count device
+ var deviceName = e.DeviceType.ToString();
+ _deviceCounts[deviceName] = _deviceCounts.GetValueOrDefault(deviceName) + 1;
+
+ // Log (newest first, cap at MaxLogEntries)
+ var logText = $"{actionName,-14} {deviceName,-7} ({e.Location.X:F0}, {e.Location.Y:F0})";
+ if (e.ActionType == SKTouchAction.WheelChanged)
+ logText += $" Δ={e.WheelDelta}";
+ _log.Insert(0, logText);
+ if (_log.Count > MaxLogEntries)
+ _log.RemoveAt(_log.Count - 1);
+
+ // Scale CSS-pixel coordinates to canvas pixels (device DPI).
+ // Touch events arrive in CSS pixels, but the SkiaSharp surface
+ // renders at device pixel ratio when IgnorePixelScaling is false.
+ var dpi = _canvasView?.Dpi ?? 1f;
+ var scaledLocation = new SKPoint(e.Location.X * dpi, e.Location.Y * dpi);
+
+ // Drawing strokes
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ var stroke = new Stroke { DeviceType = e.DeviceType };
+ stroke.Points.Add(scaledLocation);
+ _strokes.Add(stroke);
+ _activeStrokes[e.Id] = stroke;
+ break;
+
+ case SKTouchAction.Moved:
+ if (_activeStrokes.TryGetValue(e.Id, out var active))
+ active.Points.Add(scaledLocation);
+ break;
+
+ case SKTouchAction.Released:
+ case SKTouchAction.Cancelled:
+ if (_activeStrokes.TryGetValue(e.Id, out var finished))
+ {
+ finished.Points.Add(scaledLocation);
+ _activeStrokes.Remove(e.Id);
+ }
+ break;
+ }
+
+ e.Handled = true;
+ _canvasView?.Invalidate();
+ }
+
+ // ── Paint ──
+
+ private void OnPaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ var canvas = e.Surface.Canvas;
+ canvas.Clear(SKColors.White);
+
+ // Scale stroke width and legend by DPI so they look correct at device resolution
+ var dpi = _canvasView?.Dpi ?? 1f;
+
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ StrokeWidth = 3 * dpi,
+ Style = SKPaintStyle.Stroke,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round,
+ };
+
+ foreach (var stroke in _strokes)
+ {
+ if (stroke.Points.Count < 2)
+ continue;
+
+ paint.Color = GetDeviceColor(stroke.DeviceType);
+
+ using var path = new SKPath();
+ path.MoveTo(stroke.Points[0]);
+ for (int i = 1; i < stroke.Points.Count; i++)
+ path.LineTo(stroke.Points[i]);
+ canvas.DrawPath(path, paint);
+ }
+
+ // Legend in top-left corner (scaled by DPI)
+ using var legendFont = new SKFont { Size = 12 * dpi, Edging = SKFontEdging.SubpixelAntialias };
+ using var legendPaint = new SKPaint { IsAntialias = true };
+ var y = 16f * dpi;
+ foreach (var (name, _, color) in DeviceColors)
+ {
+ legendPaint.Color = color;
+ canvas.DrawCircle(12 * dpi, y - 4 * dpi, 5 * dpi, legendPaint);
+ legendPaint.Color = SKColors.Black;
+ canvas.DrawText(name, 22 * dpi, y, SKTextAlign.Left, legendFont, legendPaint);
+ y += 18 * dpi;
+ }
+ }
+
+ private static SKColor GetDeviceColor(SKTouchDeviceType type) => type switch
+ {
+ SKTouchDeviceType.Mouse => DeviceColors[0].Sk,
+ SKTouchDeviceType.Touch => DeviceColors[1].Sk,
+ SKTouchDeviceType.Stylus => DeviceColors[2].Sk,
+ _ => SKColors.Gray,
+ };
+
+ // ── Clear ──
+
+ private void Clear()
+ {
+ _strokes.Clear();
+ _activeStrokes.Clear();
+ _actionCounts.Clear();
+ _deviceCounts.Clear();
+ _lastAction = "";
+ _log.Clear();
+ _canvasView?.Invalidate();
+ }
+
+ private void ClearLog() => _log.Clear();
+}
diff --git a/samples/SkiaSharpDemo.Blazor/Pages/Touch.razor.css b/samples/SkiaSharpDemo.Blazor/Pages/Touch.razor.css
new file mode 100644
index 0000000000..5c3607c9d9
--- /dev/null
+++ b/samples/SkiaSharpDemo.Blazor/Pages/Touch.razor.css
@@ -0,0 +1,91 @@
+.toolbar {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.5rem;
+}
+
+.canvas-container {
+ width: 100%;
+ height: 480px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ overflow: hidden;
+ margin-bottom: 1rem;
+ touch-action: none;
+}
+
+.stats-row {
+ display: flex;
+ gap: 2rem;
+ flex-wrap: wrap;
+ margin-bottom: 1rem;
+}
+
+.stats-group {
+ flex: 1;
+ min-width: 260px;
+}
+
+.stats-header {
+ font-weight: 600;
+ font-size: 0.85rem;
+ margin-bottom: 0.4rem;
+ color: #555;
+}
+
+.event-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.event-card {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 0.35rem 0.75rem;
+ min-width: 90px;
+ text-align: center;
+ transition: background-color 0.15s, border-color 0.15s;
+}
+
+.event-card.active {
+ background-color: #e0f0ff;
+ border-color: #3399ff;
+}
+
+.color-dot {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-bottom: -1px;
+}
+
+.event-name {
+ font-weight: 600;
+ font-size: 0.8rem;
+}
+
+.event-count {
+ font-size: 1.1rem;
+ color: #333;
+}
+
+.log-section {
+ margin-bottom: 2rem;
+}
+
+.event-log {
+ font-family: monospace;
+ font-size: 0.78rem;
+ max-height: 160px;
+ overflow-y: auto;
+ border: 1px solid #eee;
+ border-radius: 4px;
+ padding: 0.4rem 0.6rem;
+ background: #fafafa;
+}
+
+.log-entry {
+ white-space: nowrap;
+}
diff --git a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
index e22a8ae99e..6f57739f6d 100644
--- a/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
+++ b/samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj
@@ -33,6 +33,7 @@
+
diff --git a/samples/SkiaSharpDemo.Blazor/_Imports.razor b/samples/SkiaSharpDemo.Blazor/_Imports.razor
index 10f52e8c0e..a7dcbf3cf7 100644
--- a/samples/SkiaSharpDemo.Blazor/_Imports.razor
+++ b/samples/SkiaSharpDemo.Blazor/_Imports.razor
@@ -9,5 +9,7 @@
@using SkiaSharp
@using SkiaSharp.Extended
@using SkiaSharp.Views.Blazor
+@using SkiaSharp.Extended.UI.Blazor
+@using SkiaSharp.Extended.UI.Blazor.Controls
@using SkiaSharpDemo.Blazor
@using SkiaSharpDemo.Blazor.Layout
diff --git a/scripts/SkiaSharp.Extended-Pack.slnf b/scripts/SkiaSharp.Extended-Pack.slnf
index c8b1cf8142..f26c00b929 100644
--- a/scripts/SkiaSharp.Extended-Pack.slnf
+++ b/scripts/SkiaSharp.Extended-Pack.slnf
@@ -2,6 +2,7 @@
"solution": {
"path": "..\\SkiaSharp.Extended.sln",
"projects": [
+ "source\\SkiaSharp.Extended.UI.Blazor\\SkiaSharp.Extended.UI.Blazor.csproj",
"source\\SkiaSharp.Extended.UI.Maui\\SkiaSharp.Extended.UI.Maui.csproj",
"source\\SkiaSharp.Extended\\SkiaSharp.Extended.csproj",
]
diff --git a/scripts/SkiaSharp.Extended-Test.slnf b/scripts/SkiaSharp.Extended-Test.slnf
index 3fb62aed78..35db2aa96a 100644
--- a/scripts/SkiaSharp.Extended-Test.slnf
+++ b/scripts/SkiaSharp.Extended-Test.slnf
@@ -2,9 +2,11 @@
"solution": {
"path": "..\\SkiaSharp.Extended.sln",
"projects": [
+ "source\\SkiaSharp.Extended.UI.Blazor\\SkiaSharp.Extended.UI.Blazor.csproj",
"source\\SkiaSharp.Extended.UI.Maui\\SkiaSharp.Extended.UI.Maui.csproj",
"source\\SkiaSharp.Extended\\SkiaSharp.Extended.csproj",
"tests\\SkiaSharp.Extended.Tests\\SkiaSharp.Extended.Tests.csproj",
+ "tests\\SkiaSharp.Extended.UI.Blazor.Tests\\SkiaSharp.Extended.UI.Blazor.Tests.csproj",
"tests\\SkiaSharp.Extended.UI.Maui.Tests\\SkiaSharp.Extended.UI.Maui.Tests.csproj",
]
}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor
new file mode 100644
index 0000000000..c2d4ce81b3
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor
@@ -0,0 +1,10 @@
+@namespace SkiaSharp.Extended.UI.Blazor.Controls
+@inherits ComponentBase
+@implements IAsyncDisposable
+
+
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor.cs b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor.cs
new file mode 100644
index 0000000000..909ac99cd5
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Controls/SKTouchCanvasView.razor.cs
@@ -0,0 +1,208 @@
+using Microsoft.JSInterop;
+using SkiaSharp.Views.Blazor;
+
+namespace SkiaSharp.Extended.UI.Blazor.Controls;
+
+///
+/// A SkiaSharp canvas view for Blazor that adds touch/pointer event support
+/// using the same API as the MAUI SKCanvasView.
+///
+///
+/// This view translates browser pointer events (mouse, touch, stylus) into
+/// and raises the event,
+/// matching the MAUI touch API for shared source compatibility.
+/// Pointer events are captured using JavaScript interop on the canvas element.
+/// No wrapper element is added; the upstream SKCanvasView splats
+/// AdditionalAttributes directly onto its <canvas>, so a
+/// data-sk-touch-id attribute is used to locate the element from JS.
+///
+public partial class SKTouchCanvasView : ComponentBase, IAsyncDisposable
+{
+ private SkiaSharp.Views.Blazor.SKCanvasView? _skCanvasView;
+ private readonly string _touchId = Guid.NewGuid().ToString("N");
+ private IJSObjectReference? _jsModule;
+ private DotNetObjectReference? _dotNetRef;
+ private bool _touchInitialized;
+ private bool _disposed;
+
+ [Inject]
+ private IJSRuntime JSRuntime { get; set; } = default!;
+
+ /// Gets or sets the paint surface callback.
+ [Parameter]
+ public Action? OnPaintSurface { get; set; }
+
+ ///
+ /// Gets or sets the touch event callback.
+ /// This event is raised for all pointer interactions (mouse, touch, stylus).
+ ///
+ [Parameter]
+ public EventCallback Touch { get; set; }
+
+ /// Gets or sets whether touch events are enabled.
+ [Parameter]
+ public bool EnableTouchEvents { get; set; } = true;
+
+ /// Gets or sets whether continuous rendering is enabled.
+ [Parameter]
+ public bool EnableRenderLoop { get; set; }
+
+ /// Gets or sets whether pixel scaling is ignored.
+ [Parameter]
+ public bool IgnorePixelScaling { get; set; }
+
+ /// Gets or sets additional HTML attributes passed to the inner canvas.
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IReadOnlyDictionary? AdditionalAttributes { get; set; }
+
+ /// Gets the current DPI of the display.
+ public float Dpi => _skCanvasView is not null ? (float)_skCanvasView.Dpi : 1f;
+
+ /// Requests a redraw of the canvas.
+ public void Invalidate() => _skCanvasView?.Invalidate();
+
+ ///
+ /// Merges the user-supplied with the
+ /// internal data-sk-touch-id marker attribute so JS interop can locate
+ /// the rendered <canvas> element without a wrapper div.
+ ///
+ private IReadOnlyDictionary MergedAttributes
+ {
+ get
+ {
+ var attrs = new Dictionary();
+
+ if (AdditionalAttributes is not null)
+ {
+ foreach (var kvp in AdditionalAttributes)
+ attrs[kvp.Key] = kvp.Value;
+ }
+
+ // Set after user attributes so it cannot be overwritten
+ attrs["data-sk-touch-id"] = _touchId;
+
+ return attrs;
+ }
+ }
+
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+
+ if (firstRender && EnableTouchEvents)
+ {
+ await InitializeTouchAsync();
+ }
+ }
+
+ ///
+ protected override async Task OnParametersSetAsync()
+ {
+ await base.OnParametersSetAsync();
+
+ if (_touchInitialized && !EnableTouchEvents)
+ {
+ await DisposeTouchAsync();
+ }
+ }
+
+ private async Task InitializeTouchAsync()
+ {
+ if (_touchInitialized)
+ return;
+
+ try
+ {
+ _jsModule = await JSRuntime.InvokeAsync(
+ "import", "./_content/SkiaSharp.Extended.UI.Blazor/SKTouchInterop.js");
+
+ _dotNetRef = DotNetObjectReference.Create(this);
+
+ await _jsModule.InvokeVoidAsync("initializeTouchEvents", _touchId, _dotNetRef);
+
+ _touchInitialized = true;
+ }
+ catch (Exception ex) when (ex is JSException || ex is InvalidOperationException)
+ {
+ // JS interop may not be available in pre-rendering scenarios
+ }
+ }
+
+ private async Task DisposeTouchAsync()
+ {
+ _touchInitialized = false;
+ if (_jsModule is not null)
+ {
+ try
+ {
+ await _jsModule.InvokeVoidAsync("disposeTouchEvents", _touchId);
+ await _jsModule.DisposeAsync();
+ _jsModule = null;
+ }
+ catch (JSDisconnectedException) { }
+ catch (JSException) { }
+ }
+ _dotNetRef?.Dispose();
+ _dotNetRef = null;
+ }
+
+ ///
+ /// Invoked from JavaScript when a pointer event occurs.
+ ///
+ [JSInvokable]
+ public async Task OnPointerEvent(PointerEventData data)
+ {
+ if (!EnableTouchEvents || !Touch.HasDelegate)
+ return;
+
+ var args = new SKTouchEventArgs(
+ id: data.Id,
+ actionType: (SKTouchAction)data.Action,
+ deviceType: (SKTouchDeviceType)data.DeviceType,
+ location: new SKPoint(data.X, data.Y),
+ inContact: data.InContact,
+ pressure: data.Pressure,
+ wheelDelta: data.WheelDelta);
+
+ await Touch.InvokeAsync(args);
+ }
+
+ private void OnPaintSurfaceInternal(SKPaintSurfaceEventArgs e)
+ {
+ OnPaintSurface?.Invoke(e);
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ return;
+ _disposed = true;
+
+ await DisposeTouchAsync();
+ // Do not manually dispose _skCanvasView – Blazor manages its lifecycle.
+ // Disposing it here before the DOM is cleaned up causes ResizeObserver errors.
+ }
+
+ /// Data transfer object for pointer events from JavaScript.
+ public sealed class PointerEventData
+ {
+ /// Gets or sets the pointer ID.
+ public long Id { get; set; }
+ /// Gets or sets the action type (maps to ).
+ public int Action { get; set; }
+ /// Gets or sets the device type (maps to ).
+ public int DeviceType { get; set; }
+ /// Gets or sets the X coordinate in CSS pixels.
+ public float X { get; set; }
+ /// Gets or sets the Y coordinate in CSS pixels.
+ public float Y { get; set; }
+ /// Gets or sets the pressure (0.0–1.0).
+ public float Pressure { get; set; }
+ /// Gets or sets whether the pointer is in contact.
+ public bool InContact { get; set; }
+ /// Gets or sets the wheel delta.
+ public int WheelDelta { get; set; }
+ }
+}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj b/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj
new file mode 100644
index 0000000000..a4af0e2c06
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/SkiaSharp.Extended.UI.Blazor.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net9.0;net10.0
+ enable
+
+ SkiaSharp.Extended.UI.Blazor
+ SkiaSharp.Extended.UI.Blazor
+ false
+ Default
+
+
+
+ SkiaSharp.Extended.UI.Blazor
+ Additional SkiaSharp controls for Blazor
+ This package adds a touch-enabled SkiaSharp canvas view for Blazor, providing unified touch/pointer event support across mouse, touch, and stylus inputs.
+
+
+
+ es2015
+ es2015
+ True
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchAction.cs b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchAction.cs
new file mode 100644
index 0000000000..684fd2d288
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchAction.cs
@@ -0,0 +1,23 @@
+namespace SkiaSharp.Extended.UI.Blazor;
+
+///
+/// Specifies the action that caused a touch event.
+/// Matches the SkiaSharp.Views.Maui SKTouchAction API for source sharing.
+///
+public enum SKTouchAction
+{
+ /// The touch was cancelled.
+ Cancelled = 0,
+ /// The pointer entered the element.
+ Entered = 1,
+ /// The touch started (pointer down).
+ Pressed = 2,
+ /// The touch moved.
+ Moved = 3,
+ /// The touch ended (pointer up).
+ Released = 4,
+ /// The pointer exited the element.
+ Exited = 5,
+ /// The mouse wheel changed.
+ WheelChanged = 6,
+}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchDeviceType.cs b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchDeviceType.cs
new file mode 100644
index 0000000000..582e3c8911
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchDeviceType.cs
@@ -0,0 +1,15 @@
+namespace SkiaSharp.Extended.UI.Blazor;
+
+///
+/// Specifies the type of device that caused a touch event.
+/// Matches the SkiaSharp.Views.Maui SKTouchDeviceType API for source sharing.
+///
+public enum SKTouchDeviceType
+{
+ /// A touch input device (finger).
+ Touch = 0,
+ /// A mouse input device.
+ Mouse = 1,
+ /// A stylus/pen input device.
+ Stylus = 2,
+}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchEventArgs.cs b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchEventArgs.cs
new file mode 100644
index 0000000000..873b3b4376
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/Touch/SKTouchEventArgs.cs
@@ -0,0 +1,56 @@
+namespace SkiaSharp.Extended.UI.Blazor;
+
+///
+/// Provides data for the event.
+/// Uses the same API as SkiaSharp.Views.Maui SKTouchEventArgs for source sharing.
+///
+public class SKTouchEventArgs : EventArgs
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ public SKTouchEventArgs(
+ long id,
+ SKTouchAction actionType,
+ SKTouchDeviceType deviceType,
+ SKPoint location,
+ bool inContact,
+ float pressure = 1f,
+ int wheelDelta = 0)
+ {
+ Id = id;
+ ActionType = actionType;
+ DeviceType = deviceType;
+ Location = location;
+ InContact = inContact;
+ Pressure = pressure;
+ WheelDelta = wheelDelta;
+ }
+
+ /// Gets the unique identifier for this touch point.
+ public long Id { get; }
+
+ /// Gets the action type that caused this event.
+ public SKTouchAction ActionType { get; }
+
+ /// Gets the type of input device.
+ public SKTouchDeviceType DeviceType { get; }
+
+ /// Gets the location of the touch point in canvas coordinates (points, not pixels).
+ public SKPoint Location { get; }
+
+ /// Gets a value indicating whether the touch point is in contact with the surface.
+ public bool InContact { get; }
+
+ /// Gets the pressure of the touch (0.0–1.0).
+ public float Pressure { get; }
+
+ /// Gets the mouse wheel delta (only valid for ).
+ public int WheelDelta { get; }
+
+ ///
+ /// Gets or sets whether this event has been handled.
+ /// Set to true to prevent further processing.
+ ///
+ public bool Handled { get; set; }
+}
diff --git a/source/SkiaSharp.Extended.UI.Blazor/_Imports.razor b/source/SkiaSharp.Extended.UI.Blazor/_Imports.razor
new file mode 100644
index 0000000000..06139a8039
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/_Imports.razor
@@ -0,0 +1,6 @@
+@using SkiaSharp.Extended.UI.Blazor
+@using SkiaSharp.Extended.UI.Blazor.Controls
+@using SkiaSharp.Views.Blazor
+@using Microsoft.AspNetCore.Components
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.JSInterop
diff --git a/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js
new file mode 100644
index 0000000000..91ff08153a
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js
@@ -0,0 +1,109 @@
+// SKTouchInterop.ts
+// Handles pointer events on the SkiaSharp canvas and forwards them to .NET
+// using the same event model as the MAUI SKTouchEventArgs API.
+let currentElement = null;
+let currentDotNetRef = null;
+function findElement(touchId) {
+ return document.querySelector(`[data-sk-touch-id="${touchId}"]`);
+}
+export function initializeTouchEvents(touchId, dotNetRef) {
+ const element = findElement(touchId);
+ if (!element)
+ return;
+ currentElement = element;
+ currentDotNetRef = dotNetRef;
+ element.style.touchAction = "none";
+ element.style.userSelect = "none";
+ element.addEventListener("pointerdown", onPointerDown);
+ element.addEventListener("pointermove", onPointerMove);
+ element.addEventListener("pointerup", onPointerUp);
+ element.addEventListener("pointercancel", onPointerCancel);
+ element.addEventListener("pointerenter", onPointerEnter);
+ element.addEventListener("pointerleave", onPointerLeave);
+ element.addEventListener("wheel", onWheel);
+}
+export function disposeTouchEvents(touchId) {
+ const element = findElement(touchId);
+ if (!element)
+ return;
+ element.style.touchAction = "";
+ element.style.userSelect = "";
+ element.removeEventListener("pointerdown", onPointerDown);
+ element.removeEventListener("pointermove", onPointerMove);
+ element.removeEventListener("pointerup", onPointerUp);
+ element.removeEventListener("pointercancel", onPointerCancel);
+ element.removeEventListener("pointerenter", onPointerEnter);
+ element.removeEventListener("pointerleave", onPointerLeave);
+ element.removeEventListener("wheel", onWheel);
+ currentElement = null;
+ currentDotNetRef = null;
+}
+function onPointerDown(e) {
+ sendPointerEvent(e, 2 /* SKTouchAction.Pressed */);
+ try {
+ e.currentTarget.setPointerCapture(e.pointerId);
+ }
+ catch ( /* ignore */_a) { /* ignore */ }
+}
+function onPointerMove(e) {
+ sendPointerEvent(e, 3 /* SKTouchAction.Moved */);
+}
+function onPointerUp(e) {
+ sendPointerEvent(e, 4 /* SKTouchAction.Released */);
+}
+function onPointerCancel(e) {
+ sendPointerEvent(e, 0 /* SKTouchAction.Cancelled */);
+}
+function onPointerEnter(e) {
+ sendPointerEvent(e, 1 /* SKTouchAction.Entered */);
+}
+function onPointerLeave(e) {
+ sendPointerEvent(e, 5 /* SKTouchAction.Exited */);
+}
+function onWheel(e) {
+ if (!currentDotNetRef)
+ return;
+ const rect = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const delta = e.deltaMode === 0
+ ? Math.round(-e.deltaY / 10)
+ : (e.deltaY < 0 ? 1 : -1);
+ currentDotNetRef.invokeMethodAsync("OnPointerEvent", {
+ id: -1,
+ action: 6 /* SKTouchAction.WheelChanged */,
+ deviceType: 1 /* SKTouchDeviceType.Mouse */,
+ x,
+ y,
+ pressure: 0,
+ inContact: false,
+ wheelDelta: delta,
+ });
+ e.preventDefault();
+}
+function getDeviceType(pointerType) {
+ switch (pointerType) {
+ case "mouse": return 1 /* SKTouchDeviceType.Mouse */;
+ case "pen": return 2 /* SKTouchDeviceType.Stylus */;
+ default: return 0 /* SKTouchDeviceType.Touch */;
+ }
+}
+function sendPointerEvent(e, action) {
+ if (!currentDotNetRef)
+ return;
+ const rect = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const deviceType = getDeviceType(e.pointerType);
+ const inContact = e.buttons !== 0 || action === 2 /* SKTouchAction.Pressed */;
+ currentDotNetRef.invokeMethodAsync("OnPointerEvent", {
+ id: e.pointerId,
+ action,
+ deviceType,
+ x,
+ y,
+ pressure: e.pressure,
+ inContact,
+ });
+}
+//# sourceMappingURL=SKTouchInterop.js.map
\ No newline at end of file
diff --git a/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js.map b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js.map
new file mode 100644
index 0000000000..7edab005ae
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"SKTouchInterop.js","sourceRoot":"","sources":["SKTouchInterop.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.ts b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.ts
new file mode 100644
index 0000000000..2c048eafe5
--- /dev/null
+++ b/source/SkiaSharp.Extended.UI.Blazor/wwwroot/SKTouchInterop.ts
@@ -0,0 +1,157 @@
+// SKTouchInterop.ts
+// Handles pointer events on the SkiaSharp canvas and forwards them to .NET
+// using the same event model as the MAUI SKTouchEventArgs API.
+
+interface DotNetObjectReference {
+ invokeMethodAsync(methodName: string, args: PointerEventPayload): void;
+}
+
+interface PointerEventPayload {
+ id: number;
+ action: number;
+ deviceType: number;
+ x: number;
+ y: number;
+ pressure: number;
+ inContact: boolean;
+ wheelDelta?: number;
+}
+
+const enum SKTouchAction {
+ Cancelled = 0,
+ Entered = 1,
+ Pressed = 2,
+ Moved = 3,
+ Released = 4,
+ Exited = 5,
+ WheelChanged = 6,
+}
+
+const enum SKTouchDeviceType {
+ Touch = 0,
+ Mouse = 1,
+ Stylus = 2,
+}
+
+let currentElement: HTMLElement | null = null;
+let currentDotNetRef: DotNetObjectReference | null = null;
+
+function findElement(touchId: string): HTMLElement | null {
+ return document.querySelector(`[data-sk-touch-id="${touchId}"]`);
+}
+
+export function initializeTouchEvents(touchId: string, dotNetRef: DotNetObjectReference): void {
+ const element = findElement(touchId);
+ if (!element) return;
+
+ currentElement = element;
+ currentDotNetRef = dotNetRef;
+
+ element.style.touchAction = "none";
+ element.style.userSelect = "none";
+
+ element.addEventListener("pointerdown", onPointerDown);
+ element.addEventListener("pointermove", onPointerMove);
+ element.addEventListener("pointerup", onPointerUp);
+ element.addEventListener("pointercancel", onPointerCancel);
+ element.addEventListener("pointerenter", onPointerEnter);
+ element.addEventListener("pointerleave", onPointerLeave);
+ element.addEventListener("wheel", onWheel);
+}
+
+export function disposeTouchEvents(touchId: string): void {
+ const element = findElement(touchId);
+ if (!element) return;
+
+ element.style.touchAction = "";
+ element.style.userSelect = "";
+
+ element.removeEventListener("pointerdown", onPointerDown);
+ element.removeEventListener("pointermove", onPointerMove);
+ element.removeEventListener("pointerup", onPointerUp);
+ element.removeEventListener("pointercancel", onPointerCancel);
+ element.removeEventListener("pointerenter", onPointerEnter);
+ element.removeEventListener("pointerleave", onPointerLeave);
+ element.removeEventListener("wheel", onWheel);
+
+ currentElement = null;
+ currentDotNetRef = null;
+}
+
+function onPointerDown(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Pressed);
+ try { (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); } catch { /* ignore */ }
+}
+
+function onPointerMove(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Moved);
+}
+
+function onPointerUp(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Released);
+}
+
+function onPointerCancel(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Cancelled);
+}
+
+function onPointerEnter(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Entered);
+}
+
+function onPointerLeave(e: PointerEvent): void {
+ sendPointerEvent(e, SKTouchAction.Exited);
+}
+
+function onWheel(e: WheelEvent): void {
+ if (!currentDotNetRef) return;
+
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+
+ const delta = e.deltaMode === 0
+ ? Math.round(-e.deltaY / 10)
+ : (e.deltaY < 0 ? 1 : -1);
+
+ currentDotNetRef.invokeMethodAsync("OnPointerEvent", {
+ id: -1,
+ action: SKTouchAction.WheelChanged,
+ deviceType: SKTouchDeviceType.Mouse,
+ x,
+ y,
+ pressure: 0,
+ inContact: false,
+ wheelDelta: delta,
+ });
+
+ e.preventDefault();
+}
+
+function getDeviceType(pointerType: string): SKTouchDeviceType {
+ switch (pointerType) {
+ case "mouse": return SKTouchDeviceType.Mouse;
+ case "pen": return SKTouchDeviceType.Stylus;
+ default: return SKTouchDeviceType.Touch;
+ }
+}
+
+function sendPointerEvent(e: PointerEvent, action: SKTouchAction): void {
+ if (!currentDotNetRef) return;
+
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const deviceType = getDeviceType(e.pointerType);
+ const inContact = e.buttons !== 0 || action === SKTouchAction.Pressed;
+
+ currentDotNetRef.invokeMethodAsync("OnPointerEvent", {
+ id: e.pointerId,
+ action,
+ deviceType,
+ x,
+ y,
+ pressure: e.pressure,
+ inContact,
+ });
+}
diff --git a/tests/SkiaSharp.Extended.UI.Blazor.Tests/SKTouchEventArgsTest.cs b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SKTouchEventArgsTest.cs
new file mode 100644
index 0000000000..0d43f3f9d4
--- /dev/null
+++ b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SKTouchEventArgsTest.cs
@@ -0,0 +1,120 @@
+namespace SkiaSharp.Extended.UI.Blazor.Tests;
+
+public class SKTouchEventArgsTest
+{
+ [Fact]
+ public void Constructor_SetsAllProperties()
+ {
+ var location = new SKPoint(100, 200);
+
+ var args = new SKTouchEventArgs(
+ id: 42,
+ actionType: SKTouchAction.Pressed,
+ deviceType: SKTouchDeviceType.Touch,
+ location: location,
+ inContact: true,
+ pressure: 0.8f,
+ wheelDelta: 0);
+
+ Assert.Equal(42, args.Id);
+ Assert.Equal(SKTouchAction.Pressed, args.ActionType);
+ Assert.Equal(SKTouchDeviceType.Touch, args.DeviceType);
+ Assert.Equal(location, args.Location);
+ Assert.True(args.InContact);
+ Assert.Equal(0.8f, args.Pressure);
+ Assert.Equal(0, args.WheelDelta);
+ Assert.False(args.Handled);
+ }
+
+ [Fact]
+ public void Handled_CanBeSetToTrue()
+ {
+ var args = new SKTouchEventArgs(1, SKTouchAction.Pressed, SKTouchDeviceType.Mouse,
+ new SKPoint(0, 0), false);
+
+ args.Handled = true;
+
+ Assert.True(args.Handled);
+ }
+
+ [Fact]
+ public void WheelDelta_IsSetForWheelChangedAction()
+ {
+ var args = new SKTouchEventArgs(
+ id: -1,
+ actionType: SKTouchAction.WheelChanged,
+ deviceType: SKTouchDeviceType.Mouse,
+ location: new SKPoint(50, 50),
+ inContact: false,
+ pressure: 0f,
+ wheelDelta: 3);
+
+ Assert.Equal(SKTouchAction.WheelChanged, args.ActionType);
+ Assert.Equal(3, args.WheelDelta);
+ }
+
+ [Fact]
+ public void DefaultPressure_IsOne()
+ {
+ var args = new SKTouchEventArgs(1, SKTouchAction.Pressed, SKTouchDeviceType.Touch,
+ SKPoint.Empty, true);
+
+ Assert.Equal(1f, args.Pressure);
+ }
+
+ [Fact]
+ public void SKTouchAction_HasCorrectValues()
+ {
+ // These values must match MAUI's SKTouchAction enum for source sharing
+ Assert.Equal(0, (int)SKTouchAction.Cancelled);
+ Assert.Equal(1, (int)SKTouchAction.Entered);
+ Assert.Equal(2, (int)SKTouchAction.Pressed);
+ Assert.Equal(3, (int)SKTouchAction.Moved);
+ Assert.Equal(4, (int)SKTouchAction.Released);
+ Assert.Equal(5, (int)SKTouchAction.Exited);
+ Assert.Equal(6, (int)SKTouchAction.WheelChanged);
+ }
+
+ [Fact]
+ public void SKTouchDeviceType_HasCorrectValues()
+ {
+ // These values must match MAUI's SKTouchDeviceType enum for source sharing
+ Assert.Equal(0, (int)SKTouchDeviceType.Touch);
+ Assert.Equal(1, (int)SKTouchDeviceType.Mouse);
+ Assert.Equal(2, (int)SKTouchDeviceType.Stylus);
+ }
+
+ [Theory]
+ [InlineData(SKTouchAction.Pressed, SKTouchDeviceType.Touch)]
+ [InlineData(SKTouchAction.Moved, SKTouchDeviceType.Mouse)]
+ [InlineData(SKTouchAction.Released, SKTouchDeviceType.Stylus)]
+ [InlineData(SKTouchAction.Cancelled, SKTouchDeviceType.Touch)]
+ [InlineData(SKTouchAction.Entered, SKTouchDeviceType.Mouse)]
+ [InlineData(SKTouchAction.Exited, SKTouchDeviceType.Mouse)]
+ [InlineData(SKTouchAction.WheelChanged, SKTouchDeviceType.Mouse)]
+ public void Constructor_AcceptsAllActionAndDeviceTypeCombinations(SKTouchAction action, SKTouchDeviceType device)
+ {
+ var args = new SKTouchEventArgs(1, action, device, new SKPoint(10, 20), true);
+
+ Assert.Equal(action, args.ActionType);
+ Assert.Equal(device, args.DeviceType);
+ }
+
+ [Fact]
+ public void DefaultWheelDelta_IsZero()
+ {
+ var args = new SKTouchEventArgs(1, SKTouchAction.Pressed, SKTouchDeviceType.Touch,
+ new SKPoint(0, 0), true);
+
+ Assert.Equal(0, args.WheelDelta);
+ }
+
+ [Fact]
+ public void InheritsFromEventArgs()
+ {
+ var args = new SKTouchEventArgs(1, SKTouchAction.Pressed, SKTouchDeviceType.Touch,
+ SKPoint.Empty, true);
+
+ Assert.IsAssignableFrom(args);
+ }
+}
diff --git a/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj
new file mode 100644
index 0000000000..c531cc8a00
--- /dev/null
+++ b/tests/SkiaSharp.Extended.UI.Blazor.Tests/SkiaSharp.Extended.UI.Blazor.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/SkiaSharp.Extended.UI.Blazor.Tests/xunit.runner.json b/tests/SkiaSharp.Extended.UI.Blazor.Tests/xunit.runner.json
new file mode 100644
index 0000000000..a7840670b8
--- /dev/null
+++ b/tests/SkiaSharp.Extended.UI.Blazor.Tests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "xunit.diagnosticMessages": true,
+ "xunit.methodDisplay": "classAndMethod"
+}