Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
e3343e8
Initial plan
Copilot Feb 5, 2026
301a925
Add gesture surface controls for MAUI (port of PR #79)
Copilot Feb 5, 2026
156564d
Fix code review and security issues
Copilot Feb 5, 2026
7aa5a70
Delete copilot-setup.yml
mattleibow Feb 19, 2026
0e56bc5
Delete copilot-instructions.md
mattleibow Feb 19, 2026
cd25f6a
Delete PR79-MAUI-PORT-TRACKING.md
mattleibow Feb 19, 2026
2eccdb4
Refactor gesture view with testable engine and sample demo
Copilot Feb 19, 2026
b8e6328
Fix pinch and fling gesture tests
Copilot Feb 19, 2026
da09be3
Address review comments: use BCL timer, add SKGLView support, simplifโ€ฆ
Copilot Feb 20, 2026
ce0911e
Remove lock by using SynchronizationContext for UI-thread-only operation
Copilot Feb 20, 2026
00df4a7
Fix GesturePage to render stickers with grid background
Copilot Feb 20, 2026
ef2d30d
Add smooth fling animation with deceleration
Copilot Feb 20, 2026
69aec18
Fix gesture engine bugs found by 3-model code review
mattleibow Feb 20, 2026
06ea747
Fix display density scaling for touch coordinates and canvas size
mattleibow Feb 20, 2026
d198c00
Add double-tap zoom at tapped point in gesture demo
mattleibow Feb 20, 2026
f0a3915
Draw grid inside canvas transform so it zooms with content
mattleibow Feb 20, 2026
1fe9f69
Fix grid checker pattern and rotation pivot center
mattleibow Feb 20, 2026
8615896
Pivot pinch/rotate/zoom around touch center point
mattleibow Feb 20, 2026
2711ba8
Transform pan/fling deltas from screen to content space
mattleibow Feb 20, 2026
b72576a
Track pinch/rotate center movement to pan content with fingers
mattleibow Feb 20, 2026
337fec1
Fix 2x pan during pinch by removing duplicate center delta
mattleibow Feb 20, 2026
a26f053
Add settings page with gesture toggles and threshold sliders
mattleibow Feb 20, 2026
072d9fb
Fix HitTest matrix order and gate pinch pan on _enablePan
mattleibow Feb 20, 2026
b54eefa
Fix HitTest matrix: PreConcat in same order as canvas calls
mattleibow Feb 20, 2026
60e7ccf
Add comprehensive gesture engine test coverage (70 tests)
mattleibow Feb 20, 2026
0857939
Refactor: SK prefix and one-type-per-file for gesture types
mattleibow Feb 20, 2026
1d55a0e
Apply dotnet format to gesture types
mattleibow Feb 20, 2026
4251c9c
Move fling animation into SKGestureEngine
mattleibow Feb 20, 2026
119bcdc
Fix hover without prior touch + add mouse wheel scroll zoom
mattleibow Feb 20, 2026
9ced4ac
Wire HoverDetected to sample for testing on macOS
mattleibow Feb 20, 2026
1b6ea28
Add touch event inspector to Playground page
mattleibow Feb 20, 2026
537e3cc
Revert "Add touch event inspector to Playground page"
mattleibow Feb 20, 2026
56f1efd
Fix 10 issues from 5-model code review
mattleibow Feb 20, 2026
9e731cd
Merge remote-tracking branch 'origin/main' into copilot/copy-skia-to-โ€ฆ
mattleibow Feb 24, 2026
2aa7a69
Fix 2x pan speed by moving event subscription to Loaded/Unloaded
mattleibow Feb 24, 2026
adf34c6
Add fling and threshold settings to demo settings page
mattleibow Feb 24, 2026
ec9dd5f
Add 3-layer gesture architecture with SKGestureTracker
mattleibow Feb 25, 2026
73b3bf5
Fix pan/drag interaction and sticker movement
mattleibow Feb 25, 2026
ece50d3
Remove SKGestureSurfaceView, use SKGestureTracker directly in sample
Copilot Feb 28, 2026
293f3db
Merge branch 'main' into copilot/copy-skia-to-maui
mattleibow Feb 28, 2026
ffb2e93
Add Blazor gesture sample, remove SKGestureTrackerExtensions from libโ€ฆ
Copilot Feb 28, 2026
ecc6225
Fix pan speed and rotation/zoom pivot point calculations
Copilot Feb 28, 2026
5874832
Revert transform math, fix Blazor DPI scaling
Copilot Feb 28, 2026
001540c
Fix coordinate space mismatch and fling after rotate/drag
mattleibow Feb 28, 2026
c07eac3
Remove DisplayScale from SKGestureTracker
mattleibow Feb 28, 2026
1865242
Make SKGestureState and SKGestureStateEventArgs internal
mattleibow Feb 28, 2026
7f81fd3
Make GestureStarted/GestureEnded public on engine and tracker
mattleibow Feb 28, 2026
dc01d30
Remove dead SKGestureStateEventArgs class
mattleibow Feb 28, 2026
74d2f42
Nest SKTouchState as private record in SKGestureEngine
mattleibow Feb 28, 2026
facd2a2
Nest SKPinchState as private record in SKGestureEngine
mattleibow Feb 28, 2026
f290357
Convert FlingEvent to positional record struct
mattleibow Feb 28, 2026
9c841a5
Extract engine config into SKGestureEngineOptions
mattleibow Feb 28, 2026
77d6b2c
Extract tracker config into SKGestureTrackerOptions
mattleibow Feb 28, 2026
e0ac6aa
Add velocity to SKPanEventArgs
mattleibow Mar 1, 2026
c23fb86
Rename Center/PreviousCenter to FocalPoint/PreviousFocalPoint
mattleibow Mar 1, 2026
2e37daa
Rename EventArgs to SK*GestureEventArgs pattern
mattleibow Mar 1, 2026
bfbb428
Add missing feature toggles for all gesture types
mattleibow Mar 1, 2026
f7ae824
Add Gestures to Blazor home page
mattleibow Mar 1, 2026
af4d3c0
Add config/settings UI to Blazor gesture demo
mattleibow Mar 1, 2026
74f0039
Add comprehensive tests for feature toggles, options, pan velocity
mattleibow Mar 1, 2026
9215298
Rename SKGestureEngine โ†’ SKGestureDetector, fix netstandard2.0 build
mattleibow Mar 1, 2026
eb5dafd
Add conceptual docs for gesture system
mattleibow Mar 1, 2026
8181a0f
Create SKGestureEventArgs base class for all gesture event args
mattleibow Mar 1, 2026
d79267a
Create SKGestureLifecycleEventArgs for GestureStarted/Ended events
mattleibow Mar 1, 2026
920fdd5
Create SKLongPressGestureEventArgs with Location and Duration
mattleibow Mar 1, 2026
2a2312f
Fix PanDetected firing when IsPanEnabled=false
mattleibow Mar 1, 2026
6290b4e
Rename Scale to ScaleDelta on SKPinchGestureEventArgs
mattleibow Mar 1, 2026
de4f475
Add options validation to SKGestureDetectorOptions and SKGestureTrackโ€ฆ
mattleibow Mar 1, 2026
5431b2e
Thread safety: capture SyncContext to local before null-check
mattleibow Mar 1, 2026
3b6fe17
Rename Flinging event to FlingUpdated in SKGestureTracker
mattleibow Mar 1, 2026
414d2fc
Move feature toggles (Is*Enabled) to SKGestureTrackerOptions
mattleibow Mar 1, 2026
ffc92c5
Add SetTransform, SetScale, SetRotation, SetOffset methods
mattleibow Mar 1, 2026
4e504c9
Remove duplicate forwarding properties from SKGestureTracker
mattleibow Mar 1, 2026
da97821
Store isMouse in TouchState for reliable mouse detection
mattleibow Mar 1, 2026
57c52ee
Update gesture docs for API changes
mattleibow Mar 1, 2026
aced369
Add comprehensive XML documentation to gesture API
mattleibow Mar 1, 2026
be6b522
Enable GenerateDocumentationFile for API docs
mattleibow Mar 1, 2026
97e091d
Refactor tracker to use (0,0) matrix origin
mattleibow Mar 1, 2026
36b07ce
Fix GestureStarted multi-fire, MinScale validation, and pinch radius
mattleibow Mar 2, 2026
ddda281
Add 28 new tests: options validation, EventArgs verification, strongeโ€ฆ
mattleibow Mar 2, 2026
552b2d5
Merge branch 'main' into copilot/copy-skia-to-maui
mattleibow Mar 2, 2026
f2a1a13
Remove SKGestureEventArgs base class; inline Handled into types that โ€ฆ
mattleibow Mar 2, 2026
57cddfc
Seal SKGestureDetector and SKGestureTracker
mattleibow Mar 2, 2026
bc2daee
Add tests: double-dispose, touch ID reuse, SetScale boundaries, Transโ€ฆ
mattleibow Mar 2, 2026
41e5732
Fix tracker disposed on settings navigation; add all missing settings
mattleibow Mar 2, 2026
d0e5bb2
Dispose and recreate tracker on navigation to avoid resource leaks
mattleibow Mar 2, 2026
c5d1ffb
Use OnHandlerChanged for tracker lifecycle instead of OnAppearing/OnDโ€ฆ
mattleibow Mar 2, 2026
20c4bb5
Fix gesture recognition issues: ProcessTouchCancel transitions, ZoomTโ€ฆ
mattleibow Mar 2, 2026
3e1ccb6
test: add failing tests proving gesture recognition bugs
mattleibow Mar 2, 2026
473d7f0
fix: correct gesture recognition bugs in SKGestureTracker and SKGestuโ€ฆ
mattleibow Mar 2, 2026
0fff5df
fix: correct AdjustOffsetForPivot math, tapCount desync, fling TimePrโ€ฆ
mattleibow Mar 3, 2026
4e0b5f9
refactor: move gesture classes to root SkiaSharp.Extended namespace
mattleibow Mar 3, 2026
c94d195
docs: restructure gestures into quick-start and sub-pages, extract CSS
mattleibow Mar 3, 2026
be135a4
fix: add missing using SkiaSharp.Extended in GesturePage after namespโ€ฆ
mattleibow Mar 3, 2026
8100549
refactor: make event args consistent across gesture system
mattleibow Mar 3, 2026
56e22e7
refactor: split large gesture test files into smaller area-specific fโ€ฆ
mattleibow Mar 3, 2026
ee295f5
refactor: rename PrevLocation to PreviousLocation for consistency
mattleibow Mar 3, 2026
11c2788
Remove regions from test files
mattleibow Mar 3, 2026
0f0298c
Fix review findings: dead code, event gating, allocations, time, docsโ€ฆ
mattleibow Mar 3, 2026
dbe5229
Fix critical review findings: NaN Clamp, scroll zoom negative delta, โ€ฆ
mattleibow Mar 3, 2026
59c8828
Merge remote-tracking branch 'origin/main' into copilot/copy-skia-to-โ€ฆ
mattleibow Mar 4, 2026
0002754
Better skill
mattleibow Mar 4, 2026
17099a5
Merge branch 'main' into copilot/copy-skia-to-maui
mattleibow Mar 4, 2026
7223663
Fix netstandard2.0 build and add edge case tests
mattleibow Mar 4, 2026
ab159a7
Merge remote-tracking branch 'origin/main' into copilot/copy-skia-to-โ€ฆ
mattleibow Mar 5, 2026
fbe6b20
fix: PinchDetected event leak, false DoubleTapDetected, MinScale/MaxSโ€ฆ
mattleibow Mar 5, 2026
b45d663
fix: lock pinch zoom pivot when pan is disabled
mattleibow Mar 6, 2026
1e6280a
refactor: centralize gesture pivot locking in GetEffectiveGesturePivot
mattleibow Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
namespace SkiaSharp.Extended.UI.Controls;

/// <summary>
/// A SkiaSharp view that can dynamically switch between hardware (OpenGL) and software (Canvas) rendering.
/// </summary>
/// <remarks>
/// This view provides the flexibility to toggle between GPU-accelerated rendering using OpenGL/Metal
/// and CPU-based software rendering at runtime. This is useful when:
/// - You want to give users the option to choose based on their device capabilities
/// - You need to switch rendering modes based on content being drawn
/// - You want to compare performance between rendering modes
/// </remarks>
public class SKDynamicSurfaceView : TemplatedView
{
/// <summary>
/// Identifies the IsHardwareAccelerated bindable property.
/// </summary>
public static readonly BindableProperty IsHardwareAcceleratedProperty = BindableProperty.Create(
nameof(IsHardwareAccelerated),
typeof(bool),
typeof(SKDynamicSurfaceView),
false,
propertyChanged: OnIsHardwareAcceleratedChanged);

/// <summary>
/// Identifies the EnableTouchEvents bindable property.
/// </summary>
public static readonly BindableProperty EnableTouchEventsProperty = BindableProperty.Create(
nameof(EnableTouchEvents),
typeof(bool),
typeof(SKDynamicSurfaceView),
false,
propertyChanged: OnEnableTouchEventsChanged);

private SKCanvasView? canvasView;
private SKGLView? glView;

/// <summary>
/// Creates a new instance of SKDynamicSurfaceView.
/// </summary>
public SKDynamicSurfaceView()
{
ResourceLoader<Themes.SKDynamicSurfaceViewResources>.EnsureRegistered(this);

DebugUtils.LogPropertyChanged(this);
}

/// <summary>
/// Gets or sets whether hardware acceleration is enabled.
/// </summary>
/// <remarks>
/// When true, rendering uses OpenGL/Metal for GPU acceleration.
/// When false, rendering uses the CPU-based canvas renderer.
/// </remarks>
public bool IsHardwareAccelerated
{
get => (bool)GetValue(IsHardwareAcceleratedProperty);
set => SetValue(IsHardwareAcceleratedProperty, value);
}

/// <summary>
/// Gets or sets whether touch events are enabled.
/// </summary>
public bool EnableTouchEvents
{
get => (bool)GetValue(EnableTouchEventsProperty);
set => SetValue(EnableTouchEventsProperty, value);
}

/// <summary>
/// Occurs when the surface needs to be painted.
/// </summary>
public event EventHandler<SKPaintDynamicSurfaceEventArgs>? PaintSurface;

/// <summary>
/// Occurs when a touch event is detected.
/// </summary>
public event EventHandler<SKTouchEventArgs>? Touch;

/// <summary>
/// Gets the current canvas size.
/// </summary>
public SKSize CanvasSize => canvasView?.CanvasSize ?? glView?.CanvasSize ?? SKSize.Empty;

/// <summary>
/// Invalidates the surface and requests a redraw.
/// </summary>
public void InvalidateSurface()
{
if (canvasView?.IsLoadedEx() == true)
canvasView?.InvalidateSurface();

if (glView?.IsLoadedEx() == true)
glView?.InvalidateSurface();
}

/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();

// Unsubscribe from old views
if (canvasView is not null)
{
canvasView.PaintSurface -= OnCanvasPaintSurface;
canvasView.Touch -= OnTouch;
canvasView = null;
}

if (glView is not null)
{
glView.PaintSurface -= OnGLPaintSurface;
glView.Touch -= OnTouch;
glView = null;
}

// Get the new canvas view from template
var canvasChild = GetTemplateChild("PART_CanvasSurface");
if (canvasChild is SKCanvasView cv)
{
canvasView = cv;
canvasView.PaintSurface += OnCanvasPaintSurface;
canvasView.Touch += OnTouch;
canvasView.EnableTouchEvents = EnableTouchEvents;
canvasView.IsVisible = !IsHardwareAccelerated;
}

// Get the new GL view from template
var glChild = GetTemplateChild("PART_GLSurface");
if (glChild is SKGLView gv)
{
glView = gv;
glView.PaintSurface += OnGLPaintSurface;
glView.Touch += OnTouch;
glView.EnableTouchEvents = EnableTouchEvents;
glView.IsVisible = IsHardwareAccelerated;
}
}

/// <summary>
/// Called when the surface needs to be painted.
/// </summary>
protected virtual void OnPaintSurface(SKPaintDynamicSurfaceEventArgs e) =>
PaintSurface?.Invoke(this, e);

/// <summary>
/// Called when a touch event occurs.
/// </summary>
protected virtual void OnTouch(SKTouchEventArgs e) =>
Touch?.Invoke(this, e);

private void OnCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e) =>
OnPaintSurface(new SKPaintDynamicSurfaceEventArgs(e));

private void OnGLPaintSurface(object? sender, SKPaintGLSurfaceEventArgs e) =>
OnPaintSurface(new SKPaintDynamicSurfaceEventArgs(e));

private void OnTouch(object? sender, SKTouchEventArgs e) =>
OnTouch(e);

private void UpdateSurfaceVisibility()
{
if (canvasView is not null)
canvasView.IsVisible = !IsHardwareAccelerated;

if (glView is not null)
glView.IsVisible = IsHardwareAccelerated;

InvalidateSurface();
}

private void UpdateTouchEvents()
{
if (canvasView is not null)
canvasView.EnableTouchEvents = EnableTouchEvents;

if (glView is not null)
glView.EnableTouchEvents = EnableTouchEvents;
}

private static void OnIsHardwareAcceleratedChanged(BindableObject bindable, object? oldValue, object? newValue)
{
if (bindable is SKDynamicSurfaceView view)
view.UpdateSurfaceVisibility();
}

private static void OnEnableTouchEventsChanged(BindableObject bindable, object? oldValue, object? newValue)
{
if (bindable is SKDynamicSurfaceView view)
view.UpdateTouchEvents();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SkiaSharp.Extended.UI.Controls"
xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="SkiaSharp.Extended.UI.Controls.Themes.SKDynamicSurfaceViewResources">

<!--
Control template for SKDynamicSurfaceView.
Uses both a Canvas view (software) and GL view (hardware) with visibility toggling.
-->
<ControlTemplate x:Key="SKDynamicSurfaceViewControlTemplate">
<Grid>
<skia:SKCanvasView x:Name="PART_CanvasSurface" />
<skia:SKGLView x:Name="PART_GLSurface" IsVisible="False" />
</Grid>
</ControlTemplate>

<!-- The explicit style that allows for extension -->
<Style x:Key="SKDynamicSurfaceViewStyle" TargetType="local:SKDynamicSurfaceView">
<Setter Property="ControlTemplate"
Value="{StaticResource SKDynamicSurfaceViewControlTemplate}" />
</Style>

<!-- The implicit style that applies to all controls -->
<Style TargetType="local:SKDynamicSurfaceView"
ApplyToDerivedTypes="True"
BasedOn="{StaticResource SKDynamicSurfaceViewStyle}" />

</ResourceDictionary>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace SkiaSharp.Extended.UI.Controls.Themes;

/// <summary>
/// XAML resources for <see cref="SKDynamicSurfaceView"/>.
/// </summary>
public partial class SKDynamicSurfaceViewResources : ResourceDictionary
{
/// <summary>
/// Creates a new instance.
/// </summary>
public SKDynamicSurfaceViewResources()
{
InitializeComponent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace SkiaSharp.Extended.UI.Controls;

/// <summary>
/// Event arguments for when a fling gesture is detected.
/// </summary>
public class SKFlingDetectedEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance with the specified velocities.
/// </summary>
/// <param name="velocityX">The velocity in the X direction in pixels per second.</param>
/// <param name="velocityY">The velocity in the Y direction in pixels per second.</param>
public SKFlingDetectedEventArgs(float velocityX, float velocityY)
{
VelocityX = velocityX;
VelocityY = velocityY;
}

/// <summary>
/// Gets the velocity in the X direction in pixels per second.
/// </summary>
public float VelocityX { get; }

/// <summary>
/// Gets the velocity in the Y direction in pixels per second.
/// </summary>
public float VelocityY { get; }

/// <summary>
/// Gets or sets whether the event has been handled.
/// </summary>
public bool Handled { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace SkiaSharp.Extended.UI.Controls;

/// <summary>
/// Event arguments for gesture start and end events.
/// </summary>
public class SKGestureEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance with the specified touch locations.
/// </summary>
/// <param name="locations">The locations of all active touch points.</param>
public SKGestureEventArgs(IEnumerable<SKPoint> locations)
{
ArgumentNullException.ThrowIfNull(locations);
Locations = locations.ToArray();
}

/// <summary>
/// Gets the locations of all active touch points.
/// </summary>
public IReadOnlyList<SKPoint> Locations { get; }

/// <summary>
/// Gets or sets whether the event has been handled.
/// </summary>
public bool Handled { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
namespace SkiaSharp.Extended.UI.Controls;

public partial class SKGestureSurfaceView
{
/// <summary>
/// Tracks touch events to calculate fling velocity.
/// </summary>
private sealed class FlingTracker
{
// Use only events from the last 200 ms
private const long ThresholdTicks = 200 * TimeSpan.TicksPerMillisecond;
private const int MaxSize = 2;

private readonly Dictionary<long, Queue<FlingTrackerEvent>> events = new();

public void AddEvent(long id, SKPoint location, long ticks)
{
if (!events.TryGetValue(id, out var queue))
{
queue = new Queue<FlingTrackerEvent>();
events[id] = queue;
}

queue.Enqueue(new FlingTrackerEvent(location.X, location.Y, ticks));

if (queue.Count > MaxSize)
queue.Dequeue();
}

public void RemoveId(long id)
{
events.Remove(id);
}

public void Clear()
{
events.Clear();
}

public SKPoint CalculateVelocity(long id, long now)
{
float velocityX = 0;
float velocityY = 0;

if (!events.TryGetValue(id, out var queue) || queue.Count != 2)
return SKPoint.Empty;

var array = queue.ToArray();

var lastItem = array[0];
var nowItem = array[1];

// Use last 2 events to calculate velocity
if (now - lastItem.TimeTicks < ThresholdTicks)
{
var timeDelta = nowItem.TimeTicks - lastItem.TimeTicks;
if (timeDelta > 0)
{
velocityX = (nowItem.X - lastItem.X) * TimeSpan.TicksPerSecond / timeDelta;
velocityY = (nowItem.Y - lastItem.Y) * TimeSpan.TicksPerSecond / timeDelta;
}
}

// Return the velocity in pixels per second
return new SKPoint(velocityX, velocityY);
}

/// <summary>
/// Represents a single event used for fling velocity calculation.
/// </summary>
private readonly struct FlingTrackerEvent
{
public FlingTrackerEvent(float x, float y, long timeTicks)
{
X = x;
Y = y;
TimeTicks = timeTicks;
}

public float X { get; }
public float Y { get; }
public long TimeTicks { get; }
}
}
}
Loading
Loading