Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add positional input visualization test #77

Merged
merged 3 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 107 additions & 0 deletions SDL3-CS.Tests/MainCallbacksTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NUnit.Framework;

namespace SDL.Tests
{
/// <summary>
/// Base class for tests that use SDL3 main callbacks.
/// See https://wiki.libsdl.org/SDL3/README/main-functions#how-to-use-main-callbacks-in-sdl3.
/// </summary>
[TestFixture]
[Apartment(ApartmentState.STA)]
public abstract unsafe class MainCallbacksTest
{
[Test]
public void TestEnterMainCallbacks()
{
var objectHandle = new ObjectHandle<MainCallbacksTest>(this, GCHandleType.Normal);
SDL3.SDL_EnterAppMainCallbacks(0, (byte**)objectHandle.Handle, &AppInit, &AppIterate, &AppEvent, &AppQuit);
}

protected virtual int Init()
{
SDL3.SDL_SetLogPriorities(SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE);
SDL3.SDL_SetLogOutputFunction(&LogOutput, IntPtr.Zero);
return CONTINUE;
}

protected const int TERMINATE_ERROR = -1;
protected const int CONTINUE = 0;
protected const int TERMINATE_SUCCESS = 1;

protected virtual int Iterate()
{
Thread.Sleep(10);
return CONTINUE;
}

protected virtual int Event(SDL_Event e)
{
switch (e.Type)
{
case SDL_EventType.SDL_EVENT_QUIT:
case SDL_EventType.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case SDL_EventType.SDL_EVENT_TERMINATING:
case SDL_EventType.SDL_EVENT_KEY_DOWN when e.key.keysym.sym == SDL_Keycode.SDLK_ESCAPE:
return TERMINATE_SUCCESS;
}

return CONTINUE;
}

protected virtual void Quit()
{
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void LogOutput(IntPtr userdata, SDL_LogCategory category, SDL_LogPriority priority, byte* message)
{
Console.WriteLine(SDL3.PtrToStringUTF8(message));
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static int AppInit(IntPtr* appState, int argc, byte** argv)
{
IntPtr handle = (IntPtr)argv;
*appState = handle;

var objectHandle = new ObjectHandle<MainCallbacksTest>(handle, true);
if (objectHandle.GetTarget(out var target))
return target.Init();

return TERMINATE_ERROR;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static int AppIterate(IntPtr appState)
{
var objectHandle = new ObjectHandle<MainCallbacksTest>(appState, true);
if (objectHandle.GetTarget(out var target))
return target.Iterate();

return TERMINATE_ERROR;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static int AppEvent(IntPtr appState, SDL_Event* e)
{
var objectHandle = new ObjectHandle<MainCallbacksTest>(appState, true);
if (objectHandle.GetTarget(out var target))
return target.Event(*e);

return TERMINATE_ERROR;
}

[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
private static void AppQuit(IntPtr appState)
{
using var objectHandle = new ObjectHandle<MainCallbacksTest>(appState, true);
if (objectHandle.GetTarget(out var target))
target.Quit();
}
}
}
15 changes: 8 additions & 7 deletions SDL3-CS.Tests/ObjectHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct ObjectHandle<T> : IDisposable

private GCHandle handle;

private readonly bool fromPointer;
private readonly bool canFree;

/// <summary>
/// Wraps the provided object with a <see cref="GCHandle" />, using the given <see cref="GCHandleType" />.
Expand All @@ -38,18 +38,19 @@ public struct ObjectHandle<T> : IDisposable
public ObjectHandle(T target, GCHandleType handleType)
{
handle = GCHandle.Alloc(target, handleType);
fromPointer = false;
canFree = true;
}

/// <summary>
/// Recreates an <see cref="ObjectHandle{T}" /> based on the passed <see cref="IntPtr" />.
/// Disposing this object will not free the handle, the original object must be disposed instead.
/// If <paramref name="ownsHandle"/> is <c>true</c>, disposing this object will free the handle.
/// </summary>
/// <param name="handle">Handle.</param>
public ObjectHandle(IntPtr handle)
/// <param name="handle"><see cref="Handle"/> from a previously constructed <see cref="ObjectHandle{T}(T, GCHandleType)"/>.</param>
/// <param name="ownsHandle">Whether this instance owns the underlying <see cref="GCHandle"/>.</param>
public ObjectHandle(IntPtr handle, bool ownsHandle = false)
{
this.handle = GCHandle.FromIntPtr(handle);
fromPointer = true;
canFree = ownsHandle;
}

/// <summary>
Expand Down Expand Up @@ -86,7 +87,7 @@ public bool GetTarget(out T target)

public void Dispose()
{
if (!fromPointer && handle.IsAllocated)
if (canFree && handle.IsAllocated)
handle.Free();
}

Expand Down
138 changes: 138 additions & 0 deletions SDL3-CS.Tests/TestPositionalInputVisualisation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Drawing;
using static SDL.SDL3;

namespace SDL.Tests
{
public unsafe class TestPositionalInputVisualisation : MainCallbacksTest
{
private SDL_Window* window;
private SDL_Renderer* renderer;

protected override int Init()
{
// decouple pen, mouse and touch events
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
SDL_SetHint(SDL_HINT_PEN_NOT_MOUSE, "2");

SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO);

window = SDL_CreateWindow(nameof(TestPositionalInputVisualisation), 1800, 950, SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_HIGH_PIXEL_DENSITY);
renderer = SDL_CreateRenderer(window, (Utf8String)null);

return base.Init();
}

private readonly SortedDictionary<(SDL_TouchID TouchID, SDL_FingerID FingerID), PointF> activeTouches = new SortedDictionary<(SDL_TouchID TouchID, SDL_FingerID FingerID), PointF>();
private readonly SortedDictionary<SDL_MouseID, PointF> activeMice = new SortedDictionary<SDL_MouseID, PointF>();
private readonly SortedDictionary<SDL_PenID, PointF> activePens = new SortedDictionary<SDL_PenID, PointF>();

/// <summary>
/// Sets a random, but stable color for this object.
/// </summary>
private void setColor(object o, byte alpha)
{
int color = o.ToString()?.GetHashCode() ?? 0;
byte b1 = (byte)color;
byte b2 = (byte)(color / 256);
byte b3 = (byte)(color / 256 / 256);
SDL_SetRenderDrawColor(renderer, b1, b2, b3, alpha);
}

private void fillRect(RectangleF rect)
{
var r = new SDL_FRect { x = rect.X, y = rect.Y, h = rect.Height, w = rect.Width };
SDL_RenderFillRect(renderer, &r);
}

protected override int Iterate()
{
const float gray = 0.1f;
SDL_SetRenderDrawColorFloat(renderer, gray, gray, gray, 1.0f);
SDL_RenderClear(renderer);

// mice are horizontal lines: -
foreach (var p in activeMice)
{
setColor(p.Key, 200);
RectangleF rect = new RectangleF(p.Value, SizeF.Empty);
rect.Inflate(50, 20);
fillRect(rect);
}

// fingers are vertical lines: |
foreach (var p in activeTouches)
{
setColor(p.Key, 200);
RectangleF rect = new RectangleF(p.Value, SizeF.Empty);
rect.Inflate(20, 50);
fillRect(rect);
}

// pens are squares: □
foreach (var p in activePens)
{
setColor(p.Key, 200);
RectangleF rect = new RectangleF(p.Value, SizeF.Empty);
rect.Inflate(30, 30);
fillRect(rect);
}

SDL_RenderPresent(renderer);

return base.Iterate();
}

protected override int Event(SDL_Event e)
{
SDL_ConvertEventToRenderCoordinates(renderer, &e);

switch (e.Type)
{
case SDL_EventType.SDL_EVENT_MOUSE_MOTION:
activeMice[e.motion.which] = new PointF(e.motion.x, e.motion.y);
break;

case SDL_EventType.SDL_EVENT_MOUSE_REMOVED:
activeMice.Remove(e.mdevice.which);
break;

case SDL_EventType.SDL_EVENT_FINGER_DOWN:
case SDL_EventType.SDL_EVENT_FINGER_MOTION:
activeTouches[(e.tfinger.touchID, e.tfinger.fingerID)] = new PointF(e.tfinger.x, e.tfinger.y);
break;

case SDL_EventType.SDL_EVENT_FINGER_UP:
activeTouches.Remove((e.tfinger.touchID, e.tfinger.fingerID));
break;

case SDL_EventType.SDL_EVENT_PEN_MOTION:
activePens[e.pmotion.which] = new PointF(e.pmotion.x, e.pmotion.y);
break;

case SDL_EventType.SDL_EVENT_KEY_DOWN:
switch (e.key.keysym.sym)
{
case SDL_Keycode.SDLK_r:
SDL_SetRelativeMouseMode(SDL_GetRelativeMouseMode() == SDL_bool.SDL_TRUE ? SDL_bool.SDL_FALSE : SDL_bool.SDL_TRUE);
break;

case SDL_Keycode.SDLK_f:
SDL_SetWindowFullscreen(window, SDL_bool.SDL_TRUE);
break;

case SDL_Keycode.SDLK_w:
SDL_SetWindowFullscreen(window, SDL_bool.SDL_FALSE);
break;
}

break;
}

return base.Event(e);
}
}
}