|
| 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 | +} |
0 commit comments