Skip to content

Commit da63fa0

Browse files
Copilotmattleibow
andcommitted
Add SKTouchCanvasView Blazor library from PR #357 with fixes for drawing and navigation
- Copy all source files from PR #357: SKTouchCanvasView component, touch types, JS interop, tests, Touch demo page, nav menu updates, solution/filter updates - Fix touch/mouse dragging not drawing: auto-invalidate canvas after handled touch events in SKTouchCanvasView.OnPointerEvent - Fix ResizeObserver error on navigation: don't manually dispose inner SKCanvasView (let Blazor manage its lifecycle) - Add setPointerCapture on pointerdown for reliable drag tracking Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
1 parent 87fbadb commit da63fa0

20 files changed

+954
-0
lines changed

SkiaSharp.Extended.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Extended.UI.Maui.
2121
EndProject
2222
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpDemo.Blazor", "samples\SkiaSharpDemo.Blazor\SkiaSharpDemo.Blazor.csproj", "{B7E4C45C-5CAB-444E-B2D3-294151544256}"
2323
EndProject
24+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Extended.UI.Blazor", "source\SkiaSharp.Extended.UI.Blazor\SkiaSharp.Extended.UI.Blazor.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
25+
EndProject
26+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharp.Extended.UI.Blazor.Tests", "tests\SkiaSharp.Extended.UI.Blazor.Tests\SkiaSharp.Extended.UI.Blazor.Tests.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
27+
EndProject
2428
Global
2529
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2630
Debug|Any CPU = Debug|Any CPU
@@ -53,6 +57,14 @@ Global
5357
{B7E4C45C-5CAB-444E-B2D3-294151544256}.Debug|Any CPU.Build.0 = Debug|Any CPU
5458
{B7E4C45C-5CAB-444E-B2D3-294151544256}.Release|Any CPU.ActiveCfg = Release|Any CPU
5559
{B7E4C45C-5CAB-444E-B2D3-294151544256}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
62+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
63+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
64+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
66+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
67+
{C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
5668
EndGlobalSection
5769
GlobalSection(SolutionProperties) = preSolution
5870
HideSolutionNode = FALSE
@@ -64,6 +76,8 @@ Global
6476
{2C67033A-2C49-4146-B942-9CDD2E0BA412} = {51B0C2C7-732B-4A5C-A4F2-55655D147866}
6577
{4B4EC78C-33B5-456D-BD7D-4358D16272F4} = {5555F827-12DF-4D15-BF07-3A720FC2EF3F}
6678
{B7E4C45C-5CAB-444E-B2D3-294151544256} = {51B0C2C7-732B-4A5C-A4F2-55655D147866}
79+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {5DEC7961-7CE3-44D7-A7FC-6185BA2D37FE}
80+
{C3D4E5F6-A7B8-9012-CDEF-123456789012} = {5555F827-12DF-4D15-BF07-3A720FC2EF3F}
6781
EndGlobalSection
6882
GlobalSection(ExtensibilityGlobals) = postSolution
6983
SolutionGuid = {08D78153-5DD7-4C52-A348-46AA448B2CFC}

samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@
3232
<span class="bi bi-image-nav-menu" aria-hidden="true"></span> BlurHash
3333
</NavLink>
3434
</div>
35+
36+
<div class="nav-group-header px-3">EXTENDED UI</div>
37+
38+
<div class="nav-item px-3">
39+
<NavLink class="nav-link" href="touch">
40+
<span class="bi bi-hand-index-nav-menu" aria-hidden="true"></span> Touch
41+
</NavLink>
42+
</div>
3543
</nav>
3644
</div>
3745

samples/SkiaSharpDemo.Blazor/Layout/NavMenu.razor.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z'/%3E%3Cpath d='M2.002 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2h-12zm12 1a1 1 0 0 1 1 1v6.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12V3a1 1 0 0 1 1-1h12z'/%3E%3C/svg%3E");
3838
}
3939

40+
.bi-hand-index-nav-menu {
41+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M6.75 1a.75.75 0 0 1 .75.75V8a.5.5 0 0 0 1 0V5.467l.086-.004c.317-.012.637-.008.816.027.134.027.294.096.448.182.077.042.15.147.15.314V8a.5.5 0 1 0 1 0V6.435a4.9 4.9 0 0 1 .106-.01c.316-.024.584-.01.708.04.118.046.3.207.486.43.081.096.15.19.2.259V8.5a.5.5 0 0 0 1 0v-1h.342a1 1 0 0 1 .995 1.1l-.271 2.715a2.5 2.5 0 0 1-.317.991l-1.395 2.442a.5.5 0 0 1-.434.252H6.035a.5.5 0 0 1-.416-.223l-1.433-2.15a1.5 1.5 0 0 1-.243-.666L3.4 8.18a.978.978 0 0 1 .56-1.048 .636.636 0 0 1 .544.025l.082.04c.258.126.458.265.575.37V1.75A.75.75 0 0 1 6.75 1z'/%3E%3C/svg%3E");
42+
}
43+
4044
.nav-group-header {
4145
font-size: 0.7rem;
4246
font-weight: 600;
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
@page "/touch"
2+
3+
<PageTitle>Touch</PageTitle>
4+
5+
<h1>Touch Events</h1>
6+
7+
<p>
8+
Draw on the canvas below with mouse, finger, or stylus.
9+
Each device type draws in a different color. Action counters and an event log update live.
10+
</p>
11+
12+
<div class="toolbar">
13+
<button class="btn btn-sm btn-outline-danger" @onclick="Clear">Clear</button>
14+
<span class="text-muted ms-2">Strokes: @_strokes.Count</span>
15+
</div>
16+
17+
<div class="canvas-container">
18+
<SKTouchCanvasView OnPaintSurface="OnPaintSurface"
19+
Touch="OnTouch"
20+
EnableTouchEvents="true"
21+
style="width: 100%; height: 100%;" />
22+
</div>
23+
24+
<div class="stats-row">
25+
<div class="stats-group">
26+
<div class="stats-header">Device Types</div>
27+
<div class="event-grid">
28+
@foreach (var device in DeviceColors)
29+
{
30+
var count = _deviceCounts.GetValueOrDefault(device.Name);
31+
<div class="event-card">
32+
<span class="color-dot" style="background: @device.Css"></span>
33+
<div class="event-name">@device.Name</div>
34+
<div class="event-count">@count</div>
35+
</div>
36+
}
37+
</div>
38+
</div>
39+
<div class="stats-group">
40+
<div class="stats-header">Touch Actions</div>
41+
<div class="event-grid">
42+
@foreach (var action in ActionNames)
43+
{
44+
var count = _actionCounts.GetValueOrDefault(action);
45+
var active = _lastAction == action;
46+
<div class="event-card @(active ? "active" : "")">
47+
<div class="event-name">@action</div>
48+
<div class="event-count">@count</div>
49+
</div>
50+
}
51+
</div>
52+
</div>
53+
</div>
54+
55+
<div class="log-section">
56+
<div class="stats-header">Event Log <button class="btn btn-sm btn-link" @onclick="ClearLog">clear</button></div>
57+
<div class="event-log">
58+
@foreach (var entry in _log)
59+
{
60+
<div class="log-entry">@entry</div>
61+
}
62+
@if (_log.Count == 0)
63+
{
64+
<div class="log-entry text-muted">Interact with the canvas…</div>
65+
}
66+
</div>
67+
</div>
68+
69+
<style>
70+
.toolbar {
71+
display: flex;
72+
align-items: center;
73+
margin-bottom: 0.5rem;
74+
}
75+
.canvas-container {
76+
width: 100%;
77+
height: 480px;
78+
border: 1px solid #ccc;
79+
border-radius: 6px;
80+
overflow: hidden;
81+
margin-bottom: 1rem;
82+
}
83+
.stats-row {
84+
display: flex;
85+
gap: 2rem;
86+
flex-wrap: wrap;
87+
margin-bottom: 1rem;
88+
}
89+
.stats-group {
90+
flex: 1;
91+
min-width: 260px;
92+
}
93+
.stats-header {
94+
font-weight: 600;
95+
font-size: 0.85rem;
96+
margin-bottom: 0.4rem;
97+
color: #555;
98+
}
99+
.event-grid {
100+
display: flex;
101+
flex-wrap: wrap;
102+
gap: 0.4rem;
103+
}
104+
.event-card {
105+
border: 1px solid #ddd;
106+
border-radius: 4px;
107+
padding: 0.35rem 0.75rem;
108+
min-width: 90px;
109+
text-align: center;
110+
transition: background-color 0.15s, border-color 0.15s;
111+
}
112+
.event-card.active {
113+
background-color: #e0f0ff;
114+
border-color: #3399ff;
115+
}
116+
.color-dot {
117+
display: inline-block;
118+
width: 10px;
119+
height: 10px;
120+
border-radius: 50%;
121+
margin-bottom: -1px;
122+
}
123+
.event-name {
124+
font-weight: 600;
125+
font-size: 0.8rem;
126+
}
127+
.event-count {
128+
font-size: 1.1rem;
129+
color: #333;
130+
}
131+
.log-section {
132+
margin-bottom: 2rem;
133+
}
134+
.event-log {
135+
font-family: monospace;
136+
font-size: 0.78rem;
137+
max-height: 160px;
138+
overflow-y: auto;
139+
border: 1px solid #eee;
140+
border-radius: 4px;
141+
padding: 0.4rem 0.6rem;
142+
background: #fafafa;
143+
}
144+
.log-entry {
145+
white-space: nowrap;
146+
}
147+
</style>
148+
149+
@code {
150+
// ── Colors per device type ──
151+
152+
private static readonly (string Name, string Css, SKColor Sk)[] DeviceColors =
153+
{
154+
("Mouse", "#6495ED", new SKColor(0xFF6495ED)), // CornflowerBlue
155+
("Touch", "#FF7F50", new SKColor(0xFFFF7F50)), // Coral
156+
("Stylus", "#3CB371", new SKColor(0xFF3CB371)), // MediumSeaGreen
157+
};
158+
159+
private static readonly string[] ActionNames =
160+
{ "Entered", "Pressed", "Moved", "Released", "Exited", "Cancelled", "WheelChanged" };
161+
162+
// ── Stroke data ──
163+
164+
private record struct StrokeSegment(SKPoint Point);
165+
166+
private class Stroke
167+
{
168+
public SKTouchDeviceType DeviceType { get; init; }
169+
public List<SKPoint> Points { get; } = new();
170+
}
171+
172+
private readonly List<Stroke> _strokes = new();
173+
private readonly Dictionary<long, Stroke> _activeStrokes = new();
174+
175+
// ── Counters ──
176+
177+
private readonly Dictionary<string, int> _actionCounts = new();
178+
private readonly Dictionary<string, int> _deviceCounts = new();
179+
private string _lastAction = "";
180+
181+
// ── Event log ──
182+
183+
private const int MaxLogEntries = 80;
184+
private readonly List<string> _log = new();
185+
186+
// ── Touch handler ──
187+
188+
private void OnTouch(SKTouchEventArgs e)
189+
{
190+
// Count action
191+
var actionName = e.ActionType.ToString();
192+
_actionCounts[actionName] = _actionCounts.GetValueOrDefault(actionName) + 1;
193+
_lastAction = actionName;
194+
195+
// Count device
196+
var deviceName = e.DeviceType.ToString();
197+
_deviceCounts[deviceName] = _deviceCounts.GetValueOrDefault(deviceName) + 1;
198+
199+
// Log (newest first, cap at MaxLogEntries)
200+
var logText = $"{actionName,-14} {deviceName,-7} ({e.Location.X:F0}, {e.Location.Y:F0})";
201+
if (e.ActionType == SKTouchAction.WheelChanged)
202+
logText += $" Δ={e.WheelDelta}";
203+
_log.Insert(0, logText);
204+
if (_log.Count > MaxLogEntries)
205+
_log.RemoveAt(_log.Count - 1);
206+
207+
// Drawing strokes
208+
switch (e.ActionType)
209+
{
210+
case SKTouchAction.Pressed:
211+
var stroke = new Stroke { DeviceType = e.DeviceType };
212+
stroke.Points.Add(e.Location);
213+
_strokes.Add(stroke);
214+
_activeStrokes[e.Id] = stroke;
215+
break;
216+
217+
case SKTouchAction.Moved:
218+
if (_activeStrokes.TryGetValue(e.Id, out var active))
219+
active.Points.Add(e.Location);
220+
break;
221+
222+
case SKTouchAction.Released:
223+
case SKTouchAction.Cancelled:
224+
if (_activeStrokes.TryGetValue(e.Id, out var finished))
225+
{
226+
finished.Points.Add(e.Location);
227+
_activeStrokes.Remove(e.Id);
228+
}
229+
break;
230+
}
231+
232+
e.Handled = true;
233+
}
234+
235+
// ── Paint ──
236+
237+
private void OnPaintSurface(SKPaintSurfaceEventArgs e)
238+
{
239+
var canvas = e.Surface.Canvas;
240+
canvas.Clear(SKColors.White);
241+
242+
using var paint = new SKPaint
243+
{
244+
IsAntialias = true,
245+
StrokeWidth = 3,
246+
Style = SKPaintStyle.Stroke,
247+
StrokeCap = SKStrokeCap.Round,
248+
StrokeJoin = SKStrokeJoin.Round,
249+
};
250+
251+
foreach (var stroke in _strokes)
252+
{
253+
if (stroke.Points.Count < 2)
254+
continue;
255+
256+
paint.Color = GetDeviceColor(stroke.DeviceType);
257+
258+
using var path = new SKPath();
259+
path.MoveTo(stroke.Points[0]);
260+
for (int i = 1; i < stroke.Points.Count; i++)
261+
path.LineTo(stroke.Points[i]);
262+
canvas.DrawPath(path, paint);
263+
}
264+
265+
// Legend in top-left corner
266+
using var legendFont = new SKFont { Size = 12, Edging = SKFontEdging.SubpixelAntialias };
267+
using var legendPaint = new SKPaint { IsAntialias = true };
268+
var y = 16f;
269+
foreach (var (name, _, color) in DeviceColors)
270+
{
271+
legendPaint.Color = color;
272+
canvas.DrawCircle(12, y - 4, 5, legendPaint);
273+
legendPaint.Color = SKColors.Black;
274+
canvas.DrawText(name, 22, y, SKTextAlign.Left, legendFont, legendPaint);
275+
y += 18;
276+
}
277+
}
278+
279+
private static SKColor GetDeviceColor(SKTouchDeviceType type) => type switch
280+
{
281+
SKTouchDeviceType.Mouse => DeviceColors[0].Sk,
282+
SKTouchDeviceType.Touch => DeviceColors[1].Sk,
283+
SKTouchDeviceType.Stylus => DeviceColors[2].Sk,
284+
_ => SKColors.Gray,
285+
};
286+
287+
// ── Clear ──
288+
289+
private void Clear()
290+
{
291+
_strokes.Clear();
292+
_activeStrokes.Clear();
293+
_actionCounts.Clear();
294+
_deviceCounts.Clear();
295+
_lastAction = "";
296+
_log.Clear();
297+
}
298+
299+
private void ClearLog() => _log.Clear();
300+
}

samples/SkiaSharpDemo.Blazor/SkiaSharpDemo.Blazor.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
<ItemGroup>
3535
<ProjectReference Include="..\..\source\SkiaSharp.Extended\SkiaSharp.Extended.csproj" />
36+
<ProjectReference Include="..\..\source\SkiaSharp.Extended.UI.Blazor\SkiaSharp.Extended.UI.Blazor.csproj" />
3637
</ItemGroup>
3738

3839
</Project>

samples/SkiaSharpDemo.Blazor/_Imports.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@
99
@using SkiaSharp
1010
@using SkiaSharp.Extended
1111
@using SkiaSharp.Views.Blazor
12+
@using SkiaSharp.Extended.UI.Blazor
13+
@using SkiaSharp.Extended.UI.Blazor.Controls
1214
@using SkiaSharpDemo.Blazor
1315
@using SkiaSharpDemo.Blazor.Layout

scripts/SkiaSharp.Extended-Pack.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"solution": {
33
"path": "..\\SkiaSharp.Extended.sln",
44
"projects": [
5+
"source\\SkiaSharp.Extended.UI.Blazor\\SkiaSharp.Extended.UI.Blazor.csproj",
56
"source\\SkiaSharp.Extended.UI.Maui\\SkiaSharp.Extended.UI.Maui.csproj",
67
"source\\SkiaSharp.Extended\\SkiaSharp.Extended.csproj",
78
]

scripts/SkiaSharp.Extended-Test.slnf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
"solution": {
33
"path": "..\\SkiaSharp.Extended.sln",
44
"projects": [
5+
"source\\SkiaSharp.Extended.UI.Blazor\\SkiaSharp.Extended.UI.Blazor.csproj",
56
"source\\SkiaSharp.Extended.UI.Maui\\SkiaSharp.Extended.UI.Maui.csproj",
67
"source\\SkiaSharp.Extended\\SkiaSharp.Extended.csproj",
78
"tests\\SkiaSharp.Extended.Tests\\SkiaSharp.Extended.Tests.csproj",
9+
"tests\\SkiaSharp.Extended.UI.Blazor.Tests\\SkiaSharp.Extended.UI.Blazor.Tests.csproj",
810
"tests\\SkiaSharp.Extended.UI.Maui.Tests\\SkiaSharp.Extended.UI.Maui.Tests.csproj",
911
]
1012
}

0 commit comments

Comments
 (0)