diff --git a/SDL3-CS.Tests/MainCallbacksTest.cs b/SDL3-CS.Tests/MainCallbacksTest.cs new file mode 100644 index 0000000..e0c3cfa --- /dev/null +++ b/SDL3-CS.Tests/MainCallbacksTest.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// 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. + /// + [TestFixture] + [Apartment(ApartmentState.STA)] + public abstract unsafe class MainCallbacksTest + { + [Test] + public void TestEnterMainCallbacks() + { + var objectHandle = new ObjectHandle(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(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(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(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(appState, true); + if (objectHandle.GetTarget(out var target)) + target.Quit(); + } + } +} diff --git a/SDL3-CS.Tests/ObjectHandle.cs b/SDL3-CS.Tests/ObjectHandle.cs index f4c0ae8..7ba497f 100644 --- a/SDL3-CS.Tests/ObjectHandle.cs +++ b/SDL3-CS.Tests/ObjectHandle.cs @@ -28,7 +28,7 @@ namespace SDL.Tests private GCHandle handle; - private readonly bool fromPointer; + private readonly bool canFree; /// /// Wraps the provided object with a , using the given . @@ -38,18 +38,19 @@ namespace SDL.Tests public ObjectHandle(T target, GCHandleType handleType) { handle = GCHandle.Alloc(target, handleType); - fromPointer = false; + canFree = true; } /// /// Recreates an based on the passed . - /// Disposing this object will not free the handle, the original object must be disposed instead. + /// If is true, disposing this object will free the handle. /// - /// Handle. - public ObjectHandle(IntPtr handle) + /// from a previously constructed . + /// Whether this instance owns the underlying . + public ObjectHandle(IntPtr handle, bool ownsHandle = false) { this.handle = GCHandle.FromIntPtr(handle); - fromPointer = true; + canFree = ownsHandle; } /// @@ -86,7 +87,7 @@ namespace SDL.Tests public void Dispose() { - if (!fromPointer && handle.IsAllocated) + if (canFree && handle.IsAllocated) handle.Free(); } diff --git a/SDL3-CS.Tests/TestPositionalInputVisualisation.cs b/SDL3-CS.Tests/TestPositionalInputVisualisation.cs new file mode 100644 index 0000000..75405bb --- /dev/null +++ b/SDL3-CS.Tests/TestPositionalInputVisualisation.cs @@ -0,0 +1,138 @@ +// Copyright (c) ppy Pty Ltd . 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 activeMice = new SortedDictionary(); + private readonly SortedDictionary activePens = new SortedDictionary(); + + /// + /// Sets a random, but stable color for this object. + /// + 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); + } + } +}