From 5fca0ea52045ca3b0a611d601757ccecd91a9793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Tue, 2 Apr 2024 18:52:31 +0200 Subject: [PATCH 01/62] Add Program.UsingGLES to OpenTK.Backends.Tests. Add VsyncTest application to be able to test vsync issues. --- tests/OpenTK.Backends.Tests/ImGuiUtils.cs | 23 +++++- .../OpenGLComponentView.cs | 20 ++--- tests/OpenTK.Backends.Tests/Program.cs | 6 +- .../TestApps/VsyncTest.cs | 76 +++++++++++++++++++ .../WindowComponentView.cs | 5 ++ 5 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs diff --git a/tests/OpenTK.Backends.Tests/ImGuiUtils.cs b/tests/OpenTK.Backends.Tests/ImGuiUtils.cs index 0514c6ae81..b5eb74abba 100644 --- a/tests/OpenTK.Backends.Tests/ImGuiUtils.cs +++ b/tests/OpenTK.Backends.Tests/ImGuiUtils.cs @@ -2,11 +2,7 @@ using OpenTK.Mathematics; using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; namespace OpenTK.Backends.Tests { @@ -69,5 +65,24 @@ public static void HelpMarker(string desc) ImGui.EndTooltip(); } } + + public static void ReadonlyText(string label, string str) + { + ImGui.BeginDisabled(true); + ImGui.InputText(label, ref str, (uint)(str.Length + 1)); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.ForTooltip | ImGuiHoveredFlags.AllowWhenDisabled)) + { + ImGui.EndDisabled(); + if (ImGui.BeginTooltip()) + { + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f); + ImGui.TextUnformatted(str); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + ImGui.BeginDisabled(true); + } + } + ImGui.EndDisabled(); + } } } diff --git a/tests/OpenTK.Backends.Tests/OpenGLComponentView.cs b/tests/OpenTK.Backends.Tests/OpenGLComponentView.cs index a3e2eb723a..b40d55ac23 100644 --- a/tests/OpenTK.Backends.Tests/OpenGLComponentView.cs +++ b/tests/OpenTK.Backends.Tests/OpenGLComponentView.cs @@ -59,7 +59,7 @@ public override void Initialize() for(int i = 0; i < numExtensions; i++) { - // FIXME: odd c# binding??? + // FIXME: odd c# binding? string? extension = GL.GetStringi(StringName.Extensions, (uint)i); // Doubt this will ever hit. @@ -173,10 +173,9 @@ public override void Paint(double deltaTime) if (appWindow.Application != null) { - // FIXME: Make it only a single place where we actuall initialize test apps. + // FIXME: Make it only a single place where we actually initialize test apps. Program.OpenGLComp.SetCurrentContext(appWindow.Context); - // FIXME: Proper check for GLES. - appWindow.Application.Initialize(appWindow.Window, appWindow.Context, PlatformComponents.PreferANGLE); + appWindow.Application.Initialize(appWindow.Window, appWindow.Context, Program.UsingGLES); Program.OpenGLComp.SetCurrentContext(Program.WindowContext); } } @@ -212,8 +211,7 @@ public override void Paint(double deltaTime) if (appWindow.Context != null) { Program.OpenGLComp.SetCurrentContext(appWindow.Context); - // FIXME: Proper check for GLES. - appWindow.Application.Initialize(appWindow.Window, appWindow.Context, PlatformComponents.PreferANGLE); + appWindow.Application.Initialize(appWindow.Window, appWindow.Context, Program.UsingGLES); Program.OpenGLComp.SetCurrentContext(Program.WindowContext); } } @@ -221,12 +219,10 @@ public override void Paint(double deltaTime) ImGui.EndDisabled(); ImGui.SeparatorText("Main Context Information"); - ImGui.BeginDisabled(); - ImGui.InputText("OpenGL Version", ref glVersion, 1024); - ImGui.InputText("GLSL Version", ref glslVersion, 1024); - ImGui.InputText("OpenGL Vendor", ref vendorString, 1024); - ImGui.InputText("OpenGL Renderer", ref renderer, 1024); - ImGui.EndDisabled(); + ImGuiUtils.ReadonlyText("OpenGL Version", glVersion); + ImGuiUtils.ReadonlyText("GLSL Version", glslVersion); + ImGuiUtils.ReadonlyText("OpenGL Vendor", vendorString); + ImGuiUtils.ReadonlyText("OpenGL Renderer", renderer); ImGui.SeparatorText(extensionHeader); ImGui.InputText(string.Empty, ref savePath, 4096); ImGui.SameLine(); diff --git a/tests/OpenTK.Backends.Tests/Program.cs b/tests/OpenTK.Backends.Tests/Program.cs index 9c1306fe25..31383c6faf 100644 --- a/tests/OpenTK.Backends.Tests/Program.cs +++ b/tests/OpenTK.Backends.Tests/Program.cs @@ -50,6 +50,8 @@ public ApplicationWindow(WindowHandle window) public static List ApplicationWindows = new List(); + public static bool UsingGLES = false; + public static WindowHandle Window; public static OpenGLContextHandle WindowContext; @@ -233,8 +235,8 @@ static bool IsExtensionSupported(string name) (WindowComp as MacOSWindowComponent)?.GetFramebufferSize(Window, out width, out height); GL.Viewport(0, 0, width, height); - bool useGLES = OpenGLComp is ANGLEOpenGLComponent; - ImGuiController = new ImGuiController(width, height, useGLES); + UsingGLES = OpenGLComp is ANGLEOpenGLComponent; + ImGuiController = new ImGuiController(width, height, UsingGLES); float fontSize = 13f; try diff --git a/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs new file mode 100644 index 0000000000..3d29a49494 --- /dev/null +++ b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs @@ -0,0 +1,76 @@ +using OpenTK.Core.Platform; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTK.Backends.Tests +{ + [TestApp] + public class VsyncTest : ITestApp + { + public string Name => "Vsync test"; + + private WindowHandle Window; + private OpenGLContextHandle Context; + + private bool UseOpenGLES; + + public void Initialize(WindowHandle window, OpenGLContextHandle context, bool useGLES) + { + Window = window; + Context = context; + UseOpenGLES = useGLES; + } + + public void HandleEvent(EventArgs args) + { + if (args is WindowResizeEventArgs resize) + { + var prevContext = Program.OpenGLComp.GetCurrentContext(); + Program.OpenGLComp.SetCurrentContext(Context); + + GL.Viewport(0, 0, resize.NewSize.X, resize.NewSize.Y); + + // Re-render the window to make resize live. + Render(); + + Program.OpenGLComp.SetCurrentContext(prevContext); + } + } + + public void Update(float deltaTime) + { + } + + static readonly Color4 Red = new Color4(1.0f, 0.0f, 0.0f, 1.0f); + static readonly Color4 Cyan = new Color4(0.0f, 1.0f, 1.0f, 1.0f); + + bool FlipFlop = false; + + public void Render() + { + if (FlipFlop) + { + FlipFlop = false; + GL.ClearColor(Red); + } + else + { + FlipFlop = true; + GL.ClearColor(Cyan); + } + + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); + + Program.OpenGLComp.SwapBuffers(Context); + } + + public void Deinitialize() + { + } + } +} diff --git a/tests/OpenTK.Backends.Tests/WindowComponentView.cs b/tests/OpenTK.Backends.Tests/WindowComponentView.cs index c2ef6126ab..72c839f505 100644 --- a/tests/OpenTK.Backends.Tests/WindowComponentView.cs +++ b/tests/OpenTK.Backends.Tests/WindowComponentView.cs @@ -33,6 +33,11 @@ public override void Initialize() try { canCaptureCursor = WindowComponent.CanCaptureCursor; } catch { canCaptureCursor = false; } try { canSetIcon = WindowComponent.CanSetIcon; } catch { canSetIcon = false; } + if (Program.UsingGLES) + { + openglSettings.Version = new Version(3, 1); + } + // FIXME: Make a useful hit test callback as part of this view. //WindowComponent.SetHitTestCallback(Program.Window, HitTest); } From 3f332db9ade8000751e5d7633ef09cd6104f2a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Tue, 2 Apr 2024 21:15:25 +0200 Subject: [PATCH 02/62] Add optional win32 OpenGLComponent.UseDwmFlushIfApplicable to enable DwmFlush usage. Fixed OpenTK.Backends.Tests imgui input. --- src/OpenTK.Platform.Native/Windows/Handles.cs | 2 + .../Windows/OpenGLComponent.cs | 34 +++ src/OpenTK.Platform.Native/Windows/Win32.cs | 35 +++ tests/OpenTK.Backends.Tests/Program.cs | 201 +++++++++--------- .../TestApps/VsyncTest.cs | 14 ++ 5 files changed, 190 insertions(+), 96 deletions(-) diff --git a/src/OpenTK.Platform.Native/Windows/Handles.cs b/src/OpenTK.Platform.Native/Windows/Handles.cs index beb74bd79e..c0648bd6dd 100644 --- a/src/OpenTK.Platform.Native/Windows/Handles.cs +++ b/src/OpenTK.Platform.Native/Windows/Handles.cs @@ -64,6 +64,8 @@ internal class HGLRC : OpenGLContextHandle public HGLRC? SharedContext { get; private set; } + public bool UseDwmFlush { get; set; } = false; + public HGLRC(IntPtr hGlrc, IntPtr hdc, HGLRC? sharedContext) { HGlrc = hGlrc; diff --git a/src/OpenTK.Platform.Native/Windows/OpenGLComponent.cs b/src/OpenTK.Platform.Native/Windows/OpenGLComponent.cs index 25600de864..3121ee7a41 100644 --- a/src/OpenTK.Platform.Native/Windows/OpenGLComponent.cs +++ b/src/OpenTK.Platform.Native/Windows/OpenGLComponent.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text.Json; @@ -765,6 +766,23 @@ public void SwapBuffers(OpenGLContextHandle handle) { HGLRC hglrc = handle.As(this); + // FIXME: Don't do this for fullscreen windows? + if (hglrc.UseDwmFlush == true) + { + // FIXME: Should we always use + int result = Win32.DwmIsCompositionEnabled(out bool compositionEnabled); + if (result != Win32.S_OK) + { + // FIXME: Does Win32Exception take hresult values? + throw new Win32Exception(result); + } + + if (OperatingSystem.IsWindowsVersionAtLeast(6, 2) || compositionEnabled) + { + Win32.DwmFlush(); + } + } + bool success = Win32.SwapBuffers(hglrc.HDC); if (success == false) { @@ -772,6 +790,22 @@ public void SwapBuffers(OpenGLContextHandle handle) } } + /// + /// Sets whether calls to should use DwmFlush() to sync if DWM compositing is enabled. + /// This can improve vsync performance on systems with multiple monitors using different refresh rates, but is likely to break in a multi-window scenario. + /// If using multiple windows, only one window should have this property set. + /// + /// By default this value is set to false. + /// + /// The OpenGL context that should DwmFlush() setting. + /// Whether to enable DwmFlush() sync or not. + public void UseDwmFlushIfApplicable(OpenGLContextHandle handle, bool enable) + { + HGLRC hglrc = handle.As(this); + + hglrc.UseDwmFlush = enable; + } + /// /// Gets the win32 HGLRC opengl context handle associated with the context. /// This OpenGL context is associated with the window or surface that was used to create this context. diff --git a/src/OpenTK.Platform.Native/Windows/Win32.cs b/src/OpenTK.Platform.Native/Windows/Win32.cs index c23048ff3d..847d539580 100644 --- a/src/OpenTK.Platform.Native/Windows/Win32.cs +++ b/src/OpenTK.Platform.Native/Windows/Win32.cs @@ -547,6 +547,9 @@ internal struct ICONINFO [DllImport("user32.dll", SetLastError = true)] internal static extern IntPtr /* HICON or HCURSOR */ CreateIconIndirect(in ICONINFO piconinfo); + [DllImport("gdi32.dll", CharSet = CharSet.Auto)] + internal static extern IntPtr /* HDC */ CreateDC([MarshalAs(UnmanagedType.LPTStr)] string? pwszDriver, [MarshalAs(UnmanagedType.LPTStr)] string pwszDevice, [MarshalAs(UnmanagedType.LPTStr)] string? pszPort, IntPtr pdm); + [DllImport("gdi32.dll", SetLastError = true)] internal static extern IntPtr /* HDC */ CreateCompatibleDC(IntPtr /* HDC */ hdc); @@ -1062,6 +1065,38 @@ internal struct MINMAXINFO public POINT ptMaxTrackSize; } + [DllImport("gdi32.dll")] + internal static extern int /* NTSTATUS */ D3DKMTWaitForVerticalBlankEvent(in D3DKMT_WAITFORVERTICALBLANKEVENT unnamedParam1); + internal struct D3DKMT_WAITFORVERTICALBLANKEVENT + { + public uint /* D3DKMT_HANDLE */ hAdapter; + public uint /* D3DKMT_HANDLE */ hDevice; + public uint /* D3DDDI_VIDEO_PRESENT_SOURCE_ID */ VidPnSourceId; + } + + [DllImport("gdi32.dll")] + internal static extern int /* NTSTATUS */ D3DKMTOpenAdapterFromHdc(D3DKMT_OPENADAPTERFROMHDC* unnamedParam1); + + internal struct D3DKMT_OPENADAPTERFROMHDC + { + public IntPtr /* HDC */ hDc; + public uint /* D3DKMT_HANDLE */ hAdapter; + public LUID AdapterLuid; + public uint /* D3DDDI_VIDEO_PRESENT_SOURCE_ID */ VidPnSourceId; + } + + internal struct LUID + { + uint LowPart; + int HighPart; + } + + [DllImport("dwmapi.dll")] + internal static extern int /* HRESULT */ DwmFlush(); + + [DllImport("dwmapi.dll")] + internal static extern int /* HRESULT */ DwmIsCompositionEnabled(out bool pfEnabled); + [DllImport("dwmapi.dll")] internal static extern int /* HRESULT */ DwmGetWindowAttribute( IntPtr /* HWND */ hwnd, diff --git a/tests/OpenTK.Backends.Tests/Program.cs b/tests/OpenTK.Backends.Tests/Program.cs index 31383c6faf..51f45575ea 100644 --- a/tests/OpenTK.Backends.Tests/Program.cs +++ b/tests/OpenTK.Backends.Tests/Program.cs @@ -89,7 +89,7 @@ static void Main(string[] args) // when we use Toolkit.Init to actually create the components... // - Noggin_bops 2024-03-02 PlatformComponents.PreferSDL2 = false; - PlatformComponents.PreferANGLE = true; + PlatformComponents.PreferANGLE = false; if (PlatformComponents.PreferANGLE) { @@ -475,43 +475,123 @@ static ImGuiKey ToImgui(Key key) private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType type, EventArgs args) { - if (args is WindowEventArgs windowEvent && windowEvent.Window != Window) + if (args is WindowEventArgs windowEvent) { - if (args is CloseEventArgs close2) + if (windowEvent.Window != Window) { - Console.WriteLine($"Closing window: '{WindowComp.GetTitle(close2.Window)}'"); - - // If this is one of our other windows we want to gracefully close it before we delete the window. - int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == close2.Window); - ApplicationWindow appWindow = ApplicationWindows[index]; - if (appWindow != null) + if (args is CloseEventArgs close2) { - if (appWindow.Context != null) + Console.WriteLine($"Closing window: '{WindowComp.GetTitle(close2.Window)}'"); + + // If this is one of our other windows we want to gracefully close it before we delete the window. + int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == close2.Window); + ApplicationWindow appWindow = ApplicationWindows[index]; + if (appWindow != null) { - if (appWindow.Application != null) + if (appWindow.Context != null) { - // FIXME: Make there only be one place where we actually deinit applications. - OpenGLComp.SetCurrentContext(appWindow.Context); - appWindow.Application?.Deinitialize(); - OpenGLComp.SetCurrentContext(WindowContext); + if (appWindow.Application != null) + { + // FIXME: Make there only be one place where we actually deinit applications. + OpenGLComp.SetCurrentContext(appWindow.Context); + appWindow.Application?.Deinitialize(); + OpenGLComp.SetCurrentContext(WindowContext); + } + + OpenGLComp.DestroyContext(appWindow.Context); } - OpenGLComp.DestroyContext(appWindow.Context); + ApplicationWindows.RemoveAt(index); + } + + WindowComp.Destroy(close2.Window); + return; + } + else + { + // If this is a window event for an application window, send the event to that window. + int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == windowEvent.Window); + if (index != -1) + { + ApplicationWindows[index].Application?.HandleEvent(windowEvent); } - - ApplicationWindows.RemoveAt(index); } - - WindowComp.Destroy(close2.Window); - return; } else { - // If this is a window event for an application window, send the event to that window. - int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == windowEvent.Window); - if (index != -1) + // Only update imgui stuff for the main window + if (args is KeyDownEventArgs keyDown) + { + ImGuiKey ikey = ToImgui(keyDown.Key); + ImGui.GetIO().AddKeyEvent(ikey, true); + } + else if (args is KeyUpEventArgs keyUp) + { + ImGuiKey ikey = ToImgui(keyUp.Key); + ImGui.GetIO().AddKeyEvent(ikey, false); + } + else if (args is TextInputEventArgs textInput) + { + ImGui.GetIO().AddInputCharactersUTF8(textInput.Text); + } + else if (args is MouseMoveEventArgs mouseMove) { - ApplicationWindows[index].Application?.HandleEvent(windowEvent); + ImGui.GetIO().AddMousePosEvent(mouseMove.Position.X, mouseMove.Position.Y); + } + else if (args is ScrollEventArgs scroll) + { + ImGui.GetIO().AddMouseWheelEvent(scroll.Delta.X, scroll.Delta.Y); + } + else if (args is MouseButtonDownEventArgs mouseDown) + { + int button = ((int)mouseDown.Button); + ImGui.GetIO().AddMouseButtonEvent(button, true); + } + else if (args is MouseButtonUpEventArgs mouseUp) + { + int button = ((int)mouseUp.Button); + ImGui.GetIO().AddMouseButtonEvent(button, false); + } + else if (args is WindowDpiChangeEventArgs dpiChange) + { + try + { + if (DisplayComponent != null) + { + // FIXME: Should we only scale on Y? or something else? + float scale = MathF.Max(dpiChange.ScaleX, dpiChange.ScaleY); + if (scale != 1) + { + ImFontConfig config = new ImFontConfig(); + config.FontDataOwnedByAtlas = 1; + config.OversampleH = 3; + config.OversampleV = 3; + config.PixelSnapH = 1; + config.GlyphMaxAdvanceX = float.PositiveInfinity; + config.RasterizerMultiply = 1; + config.EllipsisChar = 0xFFFF; + unsafe + { + ImFontConfigPtr configPtr = new ImFontConfigPtr(&config); + ImGui.GetIO().Fonts.AddFontFromFileTTF("Resources\\ProggyVector\\ProggyVectorDotted.ttf", float.Floor(13 * scale), configPtr); + + // FIXME: This is not perfect and does introduce some visual artifacts + // if the window dpi changes multiple times. + // We could make a copy of the unscaled style and scale that. (https://github.com/ocornut/imgui/issues/5452) + // Thought that would mean we have to update both styles if we want to change some styling. + ImGui.GetStyle().ScaleAllSizes(1 / CurrentImGuiScale); + ImGui.GetStyle().ScaleAllSizes(scale); + CurrentImGuiScale = scale; + } + } + } + } + catch (Exception e) + { + Logger.LogWarning($"Could not get scale factor, or loading the font file failed:\n{e}"); + } + + ImGuiController.RecreateFontDeviceTexture(); } } } @@ -535,10 +615,6 @@ private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType } else if (args is KeyDownEventArgs keyDown) { - ImGuiKey ikey = ToImgui(keyDown.Key); - - ImGui.GetIO().AddKeyEvent(ikey, true); - // FIXME: Track modifiers! InputData.KeysPressed[(int)keyDown.Key] = true; @@ -547,36 +623,10 @@ private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType } else if (args is KeyUpEventArgs keyUp) { - ImGuiKey ikey = ToImgui(keyUp.Key); - - ImGui.GetIO().AddKeyEvent(ikey, false); - InputData.KeysPressed[(int)keyUp.Key] = false; Logger.LogDebug($"Key Up: {keyUp.Key}, Scancode: {keyUp.Scancode}, Modifiers: {keyUp.Modifiers}"); } - else if (args is TextInputEventArgs textInput) - { - ImGui.GetIO().AddInputCharactersUTF8(textInput.Text); - } - else if (args is MouseMoveEventArgs mouseMove) - { - ImGui.GetIO().AddMousePosEvent(mouseMove.Position.X, mouseMove.Position.Y); - } - else if (args is ScrollEventArgs scroll) - { - ImGui.GetIO().AddMouseWheelEvent(scroll.Delta.X, scroll.Delta.Y); - } - else if (args is MouseButtonDownEventArgs mouseDown) - { - int button = ((int)mouseDown.Button); - ImGui.GetIO().AddMouseButtonEvent(button, true); - } - else if (args is MouseButtonUpEventArgs mouseUp) - { - int button = ((int)mouseUp.Button); - ImGui.GetIO().AddMouseButtonEvent(button, false); - } else if (args is WindowResizeEventArgs resize) { if (resize.Window == Window) @@ -619,47 +669,6 @@ private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType { Logger.LogInfo($"Dropped files:\n\t{string.Join("\n\t", fileDrop.FilePaths)}"); } - else if (args is WindowDpiChangeEventArgs dpiChange) - { - try - { - if (DisplayComponent != null) - { - // FIXME: Should we only scale on Y? or something else? - float scale = MathF.Max(dpiChange.ScaleX, dpiChange.ScaleY); - if (scale != 1) - { - ImFontConfig config = new ImFontConfig(); - config.FontDataOwnedByAtlas = 1; - config.OversampleH = 3; - config.OversampleV = 3; - config.PixelSnapH = 1; - config.GlyphMaxAdvanceX = float.PositiveInfinity; - config.RasterizerMultiply = 1; - config.EllipsisChar = 0xFFFF; - unsafe - { - ImFontConfigPtr configPtr = new ImFontConfigPtr(&config); - ImGui.GetIO().Fonts.AddFontFromFileTTF("Resources\\ProggyVector\\ProggyVectorDotted.ttf", float.Floor(13 * scale), configPtr); - - // FIXME: This is not perfect and does introduce some visual artifacts - // if the window dpi changes multiple times. - // We could make a copy of the unscaled style and scale that. (https://github.com/ocornut/imgui/issues/5452) - // Thought that would mean we have to update both styles if we want to change some styling. - ImGui.GetStyle().ScaleAllSizes(1 / CurrentImGuiScale); - ImGui.GetStyle().ScaleAllSizes(scale); - CurrentImGuiScale = scale; - } - } - } - } - catch (Exception e) - { - Logger.LogWarning($"Could not get scale factor, or loading the font file failed:\n{e}"); - } - - ImGuiController.RecreateFontDeviceTexture(); - } else if (args is DisplayConnectionChangedEventArgs displayChanged) { ((DisplayComponentView?)MainTabContainer[typeof(DisplayComponentView)])?.HandleConnectionChange(displayChanged); diff --git a/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs index 3d29a49494..0ce442797d 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs @@ -19,6 +19,8 @@ public class VsyncTest : ITestApp private bool UseOpenGLES; + private bool UseDwmFlush = false; + public void Initialize(WindowHandle window, OpenGLContextHandle context, bool useGLES) { Window = window; @@ -40,6 +42,18 @@ public void HandleEvent(EventArgs args) Program.OpenGLComp.SetCurrentContext(prevContext); } + else if (args is KeyDownEventArgs keyDown) + { + if (OperatingSystem.IsWindows()) + { + if (keyDown.Key == Key.V) + { + UseDwmFlush = !UseDwmFlush; + (Program.OpenGLComp as OpenTK.Platform.Native.Windows.OpenGLComponent)?.UseDwmFlushIfApplicable(Context, UseDwmFlush); + Console.WriteLine($"DwmFlush: {UseDwmFlush}"); + } + } + } } public void Update(float deltaTime) From 01aa655fe4ecfc581418d7b4b12c24d8b03ac1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Thu, 11 Apr 2024 20:22:05 +0200 Subject: [PATCH 03/62] Fixed bug where SetMaxClientSize would assign to the min client size variable. Added SetFullscreenDisplayNoSpace to MacOSWindowComponent to make a window fullscreen without opening a new space. Started implementing MacOSWindowComponent.SetFullscreenDisplay. --- src/OpenTK.Platform.Native/macOS/Handles.cs | 6 ++ .../macOS/MacOSWindowComponent.cs | 89 +++++++++++++++++-- .../WindowComponentView.cs | 17 ++++ 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/OpenTK.Platform.Native/macOS/Handles.cs b/src/OpenTK.Platform.Native/macOS/Handles.cs index 6a20807d81..bbf8eee0d6 100644 --- a/src/OpenTK.Platform.Native/macOS/Handles.cs +++ b/src/OpenTK.Platform.Native/macOS/Handles.cs @@ -24,6 +24,12 @@ internal class NSWindowHandle : WindowHandle public HitTest? HitTest { get; set; } = null; public bool BackgroundDragEnabled { get; set; } = false; + public bool InNonSpaceFullscreen { get; set; } = false; + public NSWindowLevel PreviousLevel { get; set; } + // FIXME: Should we store the intended style? + public NSWindowStyleMask PreviousStyleMask { get; set; } + public CGRect PreviousFrame { get; set; } + public NSWindowHandle(IntPtr window, IntPtr view, GraphicsApiHints graphicsApiHints) : base(graphicsApiHints) { Window = window; diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index 5109423924..955ba014e4 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -96,6 +96,8 @@ public class MacOSWindowComponent : IWindowComponent internal static readonly SEL selLevel = sel_registerName("level"u8); internal static readonly SEL selSetLevel = sel_registerName("setLevel:"u8); + internal static readonly SEL selHidesOnDeactivate = sel_registerName("hidesOnDeactivate"u8); + internal static readonly SEL selSetHidesOnDeactivate = sel_registerName("setHidesOnDeactivate:"u8); internal static readonly SEL selScreens = sel_registerName("screens"u8); internal static readonly SEL selCount = sel_registerName("count"u8); @@ -1442,8 +1444,8 @@ public void SetMaxClientSize(WindowHandle handle, int? width, int? height) { NSWindowHandle nswindow = handle.As(this); - nswindow.MinWidth = width; - nswindow.MinHeight = height; + nswindow.MaxWidth = width; + nswindow.MaxHeight = height; float fWidth = width ?? 10000.0f; float fheight = height ?? 10000.0f; @@ -1583,6 +1585,35 @@ public void SetMode(WindowHandle handle, WindowMode mode) { NSWindowHandle nswindow = handle.As(this); + if (nswindow.InNonSpaceFullscreen) + { + // Undo changes. + + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetHidesOnDeactivate, false); + + // Reapply window size limits + NFloat INF = NFloat.PositiveInfinity; + objc_msgSend(nswindow.Window, selSetContentMaxSize, new NSSize(nswindow.MaxWidth ?? INF, nswindow.MaxHeight ?? INF)); + objc_msgSend(nswindow.Window, selSetContentMinSize, new NSSize(nswindow.MinWidth ?? 0, nswindow.MinHeight ?? 0)); + + objc_msgSend(nswindow.Window, selSetStyleMask, (IntPtr)nswindow.PreviousStyleMask); + objc_msgSend(nswindow.Window, selSetLevel, (IntPtr)nswindow.PreviousLevel); + + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetFrame_Display, nswindow.PreviousFrame, true); + + nswindow.InNonSpaceFullscreen = false; + } + + NSWindowStyleMask mask = (NSWindowStyleMask)objc_msgSend_IntPtr(nswindow.Window, selStyleMask); + // FIXME: Only go out of the space if we need to... + if (mask.HasFlag(NSWindowStyleMask.FullScreen)) + { + // Go out of fullscreen. + objc_msgSend(nswindow.Window, selToggleFullScreen, IntPtr.Zero); + } + switch (mode) { case WindowMode.Hidden: @@ -1647,9 +1678,7 @@ public void SetMode(WindowHandle handle, WindowMode mode) case WindowMode.WindowedFullscreen: { // FIXME: deminituarize, un-zoom or whatever else needs to be done. - - // FIXME: Add a menu item for fullscreen - objc_msgSend(nswindow.Window, selToggleFullScreen, nswindow.Window); + SetFullscreenDisplay(nswindow, null); break; } case WindowMode.ExclusiveFullscreen: @@ -1662,10 +1691,56 @@ public void SetMode(WindowHandle handle, WindowMode mode) } } + /// + /// Put a window into 'windowed fullscreen' on a specific display without creating a space for it. + /// If is null then the window will be made fullscreen on the 'nearest' display. + /// + /// The window to make fullscreen. + /// The display to make the window fullscreen on. + public void SetFullscreenDisplayNoSpace(WindowHandle window, DisplayHandle? display) + { + if (display == null) + { + display = GetDisplay(window); + } + + NSWindowHandle nswindow = window.As(this); + NSScreenHandle nsscreen = display.As(this); + + // Disable min/max size constraints while fullscreen. + objc_msgSend(nswindow.Window, selSetContentMaxSize, new NSSize(NFloat.PositiveInfinity, NFloat.PositiveInfinity)); + objc_msgSend(nswindow.Window, selSetContentMinSize, new NSSize(0, 0)); + + nswindow.PreviousStyleMask = (NSWindowStyleMask)objc_msgSend_IntPtr(nswindow.Window, selStyleMask); + objc_msgSend(nswindow.Window, selSetStyleMask, (IntPtr)NSWindowStyleMask.Borderless); + + nswindow.PreviousLevel = (NSWindowLevel)objc_msgSend_IntPtr(nswindow.Window, selLevel); + objc_msgSend(nswindow.Window, selSetLevel, (IntPtr)(NSWindowLevel.MainMenu + 1)); + + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetHidesOnDeactivate, true); + + // FIXME: Remember the original frame! + CGRect frame = objc_msgSend_CGRect(nsscreen.Screen, selFrame); + // FIXME: Is this the correct frame to get? + nswindow.PreviousFrame = objc_msgSend_CGRect(nswindow.Window, selFrame); + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetFrame_Display, frame, true); + + objc_msgSend(nswindow.Window, selMakeKeyAndOrderFront, nswindow.Window); + + nswindow.InNonSpaceFullscreen = true; + } + /// - public void SetFullscreenDisplay(WindowHandle window, DisplayHandle? display) + public void SetFullscreenDisplay(WindowHandle handle, DisplayHandle? display) { - throw new NotImplementedException(); + NSWindowHandle nswindow = handle.As(this); + NSScreenHandle? nsscreen = display?.As(this); + + // FIXME: Set the frame if we passed a screen. + + objc_msgSend(nswindow.Window, selToggleFullScreen, nswindow.Window); } /// diff --git a/tests/OpenTK.Backends.Tests/WindowComponentView.cs b/tests/OpenTK.Backends.Tests/WindowComponentView.cs index 72c839f505..1f6d0b50fc 100644 --- a/tests/OpenTK.Backends.Tests/WindowComponentView.cs +++ b/tests/OpenTK.Backends.Tests/WindowComponentView.cs @@ -1,6 +1,7 @@ using ImGuiNET; using OpenTK.Core.Platform; using OpenTK.Mathematics; +using OpenTK.Platform.Native.macOS; using OpenTK.Platform.Native.Windows; using System; using System.Collections.Generic; @@ -197,6 +198,22 @@ void PaintWindow(WindowHandle window, int i) Program.Logger.LogInfo($"WindowComponent.SetMode({WindowModeNames[modeIndex]})"); } + // FIXME: change to toggle! + if (ImGui.Button("Set windowed fullscreen")) + { + DisplayHandle display = WindowComponent.GetDisplay(window); + WindowComponent.SetFullscreenDisplay(window, display); + } + + if (WindowComponent is MacOSWindowComponent macosWindowComponent) + { + if (ImGui.Button("Set windowed fullscreen without opening its own space")) + { + DisplayHandle display = macosWindowComponent.GetDisplay(window); + macosWindowComponent.SetFullscreenDisplayNoSpace(window, display); + } + } + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Border style"); ImGui.SameLine(); ImGui.Combo("##border_style", ref borderStyleIndex, WindowBorderStyleNames, WindowBorderStyleNames.Length); ImGui.SameLine(); From 50c19a0945a627961cb9aa1dcdcdadf83c1df5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sat, 13 Apr 2024 23:58:38 +0200 Subject: [PATCH 04/62] Implemented MacOSWindowComponent.GetFullscreenDisplay. Implemented MacOSWindowComponent.SetFullscreenDisplay(WindowHandle, DisplayHandle?). Implemented MacOSWindowComponent.GetCursorCaptureMode. Implemented the Normal and Confined cursor capture modes for MacOSWindowComponent.SetCursorCaptureMode. --- src/OpenTK.Platform.Native/macOS/Handles.cs | 3 + .../macOS/MacOSWindowComponent.cs | 105 +++++++++++++++--- src/OpenTK.Platform.Native/macOS/ObjC.cs | 18 +++ .../WindowComponentView.cs | 12 ++ 4 files changed, 121 insertions(+), 17 deletions(-) diff --git a/src/OpenTK.Platform.Native/macOS/Handles.cs b/src/OpenTK.Platform.Native/macOS/Handles.cs index bbf8eee0d6..bbdda07239 100644 --- a/src/OpenTK.Platform.Native/macOS/Handles.cs +++ b/src/OpenTK.Platform.Native/macOS/Handles.cs @@ -24,12 +24,15 @@ internal class NSWindowHandle : WindowHandle public HitTest? HitTest { get; set; } = null; public bool BackgroundDragEnabled { get; set; } = false; + public NSScreenHandle? FullscreenScreen { get; set; } = null; public bool InNonSpaceFullscreen { get; set; } = false; public NSWindowLevel PreviousLevel { get; set; } // FIXME: Should we store the intended style? public NSWindowStyleMask PreviousStyleMask { get; set; } public CGRect PreviousFrame { get; set; } + public CursorCaptureMode CursorCaptureMode { get; set; } = CursorCaptureMode.Normal; + public NSWindowHandle(IntPtr window, IntPtr view, GraphicsApiHints graphicsApiHints) : base(graphicsApiHints) { Window = window; diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index 955ba014e4..c8e3ce95c6 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -134,8 +134,16 @@ public class MacOSWindowComponent : IWindowComponent internal static readonly SEL selDefaultCenter = sel_registerName("defaultCenter"u8); internal static readonly SEL selAddObserver_selector_name_object = sel_registerName("addObserver:selector:name:object:"u8); + // The mouseConfinementRect property is not part of public headers, but + // somehow SDL developers figured out it exists... + // See: https://github.com/libsdl-org/SDL/commit/35d90f17e1c7d3740c75641ef94b5e5c938c20c6 + // - Noggin_bops 2024-04-13 + internal static readonly SEL selSetMouseConfinementRect = sel_registerName("setMouseConfinementRect:"u8); + internal static readonly SEL selMouseConfinementRect = sel_registerName("mouseConfinementRect:"u8); + internal static readonly IntPtr NSDefaultRunLoop = GetStringConstant(FoundationLibrary, "NSDefaultRunLoopMode"u8); + internal static readonly ObjCClass NSWindowClass = objc_getClass("NSWindow"u8); internal static readonly ObjCClass NSApplicationClass = objc_getClass("NSApplication"); internal static readonly ObjCClass NSMenuClass = objc_getClass("NSMenu"u8); internal static readonly ObjCClass NSMenuItemClass = objc_getClass("NSMenuItem"u8); @@ -168,7 +176,7 @@ public void Initialize(PalComponents which) { throw new PalException(this, "MacOSWindowComponent can only initialize the Window component."); } - + // This method is called from the Quit menu option. class_addMethod(NSApplicationClass, selQuit, Menu_QuitInst, "v@:"u8); @@ -224,7 +232,7 @@ public void Initialize(PalComponents which) } // Allocate a window class - NSOpenTKWindowClass = objc_allocateClassPair(objc_getClass("NSWindow"u8), "NSOpenTKWindow"u8, 0); + NSOpenTKWindowClass = objc_allocateClassPair(NSWindowClass, "NSOpenTKWindow"u8, 0); // Define a Ivar where we can pass a GCHandle so we can retreive it in callbacks. class_addIvar(NSOpenTKWindowClass, "otkPALWindowComponent"u8, (nuint)nuint.Size, (nuint)int.Log2(nuint.Size), "^v"u8); @@ -1585,25 +1593,36 @@ public void SetMode(WindowHandle handle, WindowMode mode) { NSWindowHandle nswindow = handle.As(this); - if (nswindow.InNonSpaceFullscreen) + if (nswindow.FullscreenScreen != null) { - // Undo changes. + if (nswindow.InNonSpaceFullscreen) + { + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetHidesOnDeactivate, false); - // FIXME: BOOL - objc_msgSend(nswindow.Window, selSetHidesOnDeactivate, false); + // Reapply window size limits + NFloat INF = NFloat.PositiveInfinity; + objc_msgSend(nswindow.Window, selSetContentMaxSize, new NSSize(nswindow.MaxWidth ?? INF, nswindow.MaxHeight ?? INF)); + objc_msgSend(nswindow.Window, selSetContentMinSize, new NSSize(nswindow.MinWidth ?? 0, nswindow.MinHeight ?? 0)); - // Reapply window size limits - NFloat INF = NFloat.PositiveInfinity; - objc_msgSend(nswindow.Window, selSetContentMaxSize, new NSSize(nswindow.MaxWidth ?? INF, nswindow.MaxHeight ?? INF)); - objc_msgSend(nswindow.Window, selSetContentMinSize, new NSSize(nswindow.MinWidth ?? 0, nswindow.MinHeight ?? 0)); + objc_msgSend(nswindow.Window, selSetStyleMask, (IntPtr)nswindow.PreviousStyleMask); + objc_msgSend(nswindow.Window, selSetLevel, (IntPtr)nswindow.PreviousLevel); - objc_msgSend(nswindow.Window, selSetStyleMask, (IntPtr)nswindow.PreviousStyleMask); - objc_msgSend(nswindow.Window, selSetLevel, (IntPtr)nswindow.PreviousLevel); + nswindow.InNonSpaceFullscreen = false; + } + else + { + objc_msgSend(nswindow.Window, selToggleFullScreen, IntPtr.Zero); + } + // FIXME: There seems like there might be some timing related things going on here + // where running this code normally it doesn't reset the frame after being in it's own space + // but placing a breakpoint here and stepping through it the frame actually gets reset properly. + // - Noggin_bops 2024-04-13 // FIXME: BOOL objc_msgSend(nswindow.Window, selSetFrame_Display, nswindow.PreviousFrame, true); - nswindow.InNonSpaceFullscreen = false; + nswindow.FullscreenScreen = null; } NSWindowStyleMask mask = (NSWindowStyleMask)objc_msgSend_IntPtr(nswindow.Window, selStyleMask); @@ -1730,6 +1749,7 @@ public void SetFullscreenDisplayNoSpace(WindowHandle window, DisplayHandle? disp objc_msgSend(nswindow.Window, selMakeKeyAndOrderFront, nswindow.Window); nswindow.InNonSpaceFullscreen = true; + nswindow.FullscreenScreen = nsscreen; } /// @@ -1738,7 +1758,20 @@ public void SetFullscreenDisplay(WindowHandle handle, DisplayHandle? display) NSWindowHandle nswindow = handle.As(this); NSScreenHandle? nsscreen = display?.As(this); - // FIXME: Set the frame if we passed a screen. + nswindow.PreviousFrame = objc_msgSend_CGRect(nswindow.Window, selFrame); + + if (nsscreen != null) + { + CGRect frame = objc_msgSend_CGRect(nsscreen.Screen, selFrame); + // FIXME: BOOL + objc_msgSend(nswindow.Window, selSetFrame_Display, frame, true); + } + else + { + nsscreen = GetDisplay(nswindow).As(this); + } + + nswindow.FullscreenScreen = nsscreen; objc_msgSend(nswindow.Window, selToggleFullScreen, nswindow.Window); } @@ -1752,7 +1785,9 @@ public void SetFullscreenDisplay(WindowHandle window, DisplayHandle display, Vid /// public bool GetFullscreenDisplay(WindowHandle window, [NotNullWhen(true)] out DisplayHandle? display) { - throw new NotImplementedException(); + NSWindowHandle nswindow = window.As(this); + display = nswindow.FullscreenScreen; + return display != null; } /// @@ -1892,13 +1927,49 @@ public void SetCursor(WindowHandle handle, CursorHandle? cursor) /// public CursorCaptureMode GetCursorCaptureMode(WindowHandle handle) { - throw new NotImplementedException(); + NSWindowHandle nswindow = handle.As(this); + return nswindow.CursorCaptureMode; } /// public void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode) { - throw new NotImplementedException(); + NSWindowHandle nswindow = handle.As(this); + + if (nswindow.CursorCaptureMode == CursorCaptureMode.Confined && + mode != CursorCaptureMode.Confined) + { + // Remove the confinement rectangle. + NFloat INF = NFloat.PositiveInfinity; + CGRect rect = new CGRect(-INF, -INF, INF, INF); + objc_msgSend(nswindow.Window, selSetMouseConfinementRect, rect); + } + + switch (mode) + { + case CursorCaptureMode.Normal: + // Getting out of the other modes is handled above. + nswindow.CursorCaptureMode = CursorCaptureMode.Normal; + break; + case CursorCaptureMode.Confined: + // This undocumented property was found by the SDL developers + // See: https://github.com/libsdl-org/SDL/commit/35d90f17e1c7d3740c75641ef94b5e5c938c20c6 + // - Noggin_bops 2024-04-13 + CGRect rect = objc_msgSend_CGRect(nswindow.View, selFrame); + objc_msgSend(nswindow.Window, selSetMouseConfinementRect, rect); + nswindow.CursorCaptureMode = mode; + break; + case CursorCaptureMode.Locked: + // FIXME: Set the mode to locked + // CGAssociateMouseAndMouseCursorPosition() to allow the mouse to move separately from the cursor + // [NSCursor hide] can be used to hide the cursor + // Then we need to disable this when we loose key status and enable it again when we get key status again + // We also need to handle warping the cursor back to the center of the window every time the cursor moves. + // - Noggin_bops 2024-04-13 + break; + default: + break; + } } /// diff --git a/src/OpenTK.Platform.Native/macOS/ObjC.cs b/src/OpenTK.Platform.Native/macOS/ObjC.cs index 1f9af18743..62b389c12b 100644 --- a/src/OpenTK.Platform.Native/macOS/ObjC.cs +++ b/src/OpenTK.Platform.Native/macOS/ObjC.cs @@ -133,6 +133,9 @@ internal static ObjCClass objc_getClass(ReadOnlySpan name) [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] internal static extern void objc_msgSend(IntPtr receiver, SEL selector, CGPoint point); + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + internal static extern void objc_msgSend(IntPtr receiver, SEL selector, CGRect value1); + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] internal static extern void objc_msgSend(IntPtr receiver, SEL selector, CGRect value1, IntPtr value2); @@ -417,6 +420,21 @@ internal static bool class_addMethod(ObjCClass cls, SEL name, Delegate imp, Read static extern bool class_addMethod(ObjCClass cls, SEL name, IntPtr imp, byte* types); } + [DllImport(FoundationFramework)] + internal static extern IntPtr /* objc_property_t* */ class_copyPropertyList(ObjCClass cls, out uint outCount); + + internal static string property_getName(IntPtr property) { + + byte** name = property_getName(property); + return Marshal.PtrToStringUTF8((IntPtr)(*name))!; + + // FIXME: For some reason we are getting a char** when the documentation says + // we should be getting a char*??? + // - Noggin_bops 2024-04-13 + [DllImport(FoundationFramework, CallingConvention = CallingConvention.Cdecl)] + static extern byte** /* char* */ property_getName(IntPtr /* objc_property_t */ property); + } + internal static IntPtr /* Ivar */ object_getInstanceVariable(IntPtr /* id */ @object, ReadOnlySpan name, out IntPtr outValue) { fixed(byte* namePtr = name) diff --git a/tests/OpenTK.Backends.Tests/WindowComponentView.cs b/tests/OpenTK.Backends.Tests/WindowComponentView.cs index 1f6d0b50fc..aba15b3f8b 100644 --- a/tests/OpenTK.Backends.Tests/WindowComponentView.cs +++ b/tests/OpenTK.Backends.Tests/WindowComponentView.cs @@ -67,6 +67,7 @@ private HitType HitTest(WindowHandle window, Vector2 point) string titleString = ""; int modeIndex = 0; int borderStyleIndex = 0; + int captureModeIndex = 0; Vector2i windowPosition; Vector2i clientPosition; @@ -76,6 +77,9 @@ private HitType HitTest(WindowHandle window, Vector2 point) WindowBorderStyle[] WindowBorderStyles = Enum.GetValues(); string[] WindowBorderStyleNames = Enum.GetNames(); + CursorCaptureMode[] CaptureModes = Enum.GetValues(); + string[] CaptureModeNames = Enum.GetNames(); + public override void Paint(double deltaTime) { base.Paint(deltaTime); @@ -214,6 +218,14 @@ void PaintWindow(WindowHandle window, int i) } } + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Cursor capture mode"); ImGui.SameLine(); + ImGui.Combo("##captureMode", ref captureModeIndex, CaptureModeNames, CaptureModeNames.Length); ImGui.SameLine(); + if (ImGui.Button("Apply##captureMode")) + { + WindowComponent.SetCursorCaptureMode(window, CaptureModes[captureModeIndex]); + } + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted("Border style"); ImGui.SameLine(); ImGui.Combo("##border_style", ref borderStyleIndex, WindowBorderStyleNames, WindowBorderStyleNames.Length); ImGui.SameLine(); From 8b80aaf02d41e76890e25b7c4d4e5675ac0c19c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sun, 14 Apr 2024 00:03:22 +0200 Subject: [PATCH 05/62] Fixed small typo in SEL --- src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index c8e3ce95c6..8be6aa5f8a 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -139,7 +139,7 @@ public class MacOSWindowComponent : IWindowComponent // See: https://github.com/libsdl-org/SDL/commit/35d90f17e1c7d3740c75641ef94b5e5c938c20c6 // - Noggin_bops 2024-04-13 internal static readonly SEL selSetMouseConfinementRect = sel_registerName("setMouseConfinementRect:"u8); - internal static readonly SEL selMouseConfinementRect = sel_registerName("mouseConfinementRect:"u8); + internal static readonly SEL selMouseConfinementRect = sel_registerName("mouseConfinementRect"u8); internal static readonly IntPtr NSDefaultRunLoop = GetStringConstant(FoundationLibrary, "NSDefaultRunLoopMode"u8); From b85c53ab1b48027f4190875da266e2bb51c778f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sun, 14 Apr 2024 12:17:53 +0200 Subject: [PATCH 06/62] Fixed Y flipping in MacOSMouseComponent --- .../macOS/MacOSMouseComponent.cs | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs index 1f2bc438cc..d784332999 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs @@ -4,6 +4,7 @@ using OpenTK.Mathematics; using static OpenTK.Platform.Native.macOS.ObjC; using static OpenTK.Platform.Native.macOS.CG; +using System.Runtime.InteropServices; namespace OpenTK.Platform.Native.macOS { @@ -43,39 +44,28 @@ public void Initialize(PalComponents which) internal static void GetPosition(out double x, out double y) { CGPoint p = objc_msgSend_CGPoint((IntPtr)NSEventClass, selMouseLocation); + NFloat flippedY = CG.FlipYCoordinate(p.y); - IntPtr screensNSArray = objc_msgSend_IntPtr((IntPtr)NSScreenClass, selScreens); - IntPtr screen = objc_msgSend_IntPtr(screensNSArray, selObjectAtIndex, 0); - CGRect frame = objc_msgSend_CGRect(screen, selFrame); - - // FIXME: Coordinate system x = p.x; - y = frame.size.y - p.y; + y = flippedY; } /// public void GetPosition(out int x, out int y) { CGPoint p = objc_msgSend_CGPoint((IntPtr)NSEventClass, selMouseLocation); + NFloat flippedY = CG.FlipYCoordinate(p.y); - IntPtr screensNSArray = objc_msgSend_IntPtr((IntPtr)NSScreenClass, selScreens); - IntPtr screen = objc_msgSend_IntPtr(screensNSArray, selObjectAtIndex, 0); - CGRect frame = objc_msgSend_CGRect(screen, selFrame); - - // FIXME: Coordinate system x = (int)p.x; - y = (int)(frame.size.y - p.y); + y = (int)flippedY; } /// public void SetPosition(int x, int y) { - IntPtr screensNSArray = objc_msgSend_IntPtr((IntPtr)NSScreenClass, selScreens); - IntPtr screen = objc_msgSend_IntPtr(screensNSArray, selObjectAtIndex, 0); - CGRect frame = objc_msgSend_CGRect(screen, selFrame); + float flippedY = CG.FlipYCoordinate(y); - // FIXME: Coordinate system - CGWarpMouseCursorPosition(new CGPoint(x, frame.size.y - y)); + CGWarpMouseCursorPosition(new CGPoint(x, flippedY)); } // FIXME: This is only a 32-bit float and @@ -96,16 +86,12 @@ internal static void RegisterMouseWheelDelta(Vector2 delta) public void GetMouseState(out MouseState state) { CGPoint p = objc_msgSend_CGPoint((IntPtr)NSEventClass, selMouseLocation); + NFloat flippedY = CG.FlipYCoordinate(p.y); - IntPtr screensNSArray = objc_msgSend_IntPtr((IntPtr)NSScreenClass, selScreens); - IntPtr screen = objc_msgSend_IntPtr(screensNSArray, selObjectAtIndex, 0); - CGRect frame = objc_msgSend_CGRect(screen, selFrame); - - // NSUInteger + // FIXME: NSUInteger ulong buttons = objc_msgSend_ulong((IntPtr)NSEventClass, selPressedMouseButtons); - // FIXME: Coordinate system - state.Position = new Vector2i((int)p.x, (int)(frame.size.y - p.y)); + state.Position = new Vector2i((int)p.x, (int)flippedY); state.Scroll = ScrollPosition; From 5efae6bfd1fe6bc9fe428176617e927ea57096fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sun, 14 Apr 2024 13:11:34 +0200 Subject: [PATCH 07/62] Initial work on getting CursorCaptureMode.Locked to work on macOS. --- .../Windows/MouseComponent.cs | 4 -- src/OpenTK.Platform.Native/macOS/CG.cs | 6 ++ src/OpenTK.Platform.Native/macOS/Handles.cs | 4 ++ .../macOS/MacOSMouseComponent.cs | 2 + .../macOS/MacOSWindowComponent.cs | 57 ++++++++++++++++++- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/OpenTK.Platform.Native/Windows/MouseComponent.cs b/src/OpenTK.Platform.Native/Windows/MouseComponent.cs index cb58c97df1..27ccf65173 100644 --- a/src/OpenTK.Platform.Native/Windows/MouseComponent.cs +++ b/src/OpenTK.Platform.Native/Windows/MouseComponent.cs @@ -37,8 +37,6 @@ public void Initialize(PalComponents which) /// public void GetPosition(out int x, out int y) { - // FIXME: Check the handle! - // FIXME: When hibernating (or going out of hibernate) this function fails with 0x5 Access denied. bool success = Win32.GetCursorPos(out Win32.POINT lpPoint); if (success == false) @@ -53,8 +51,6 @@ public void GetPosition(out int x, out int y) /// public void SetPosition(int x, int y) { - // FIXME: Check the handle! - bool success = Win32.SetCursorPos(x, y); if (success == false) { diff --git a/src/OpenTK.Platform.Native/macOS/CG.cs b/src/OpenTK.Platform.Native/macOS/CG.cs index 17fc230074..697af9009f 100644 --- a/src/OpenTK.Platform.Native/macOS/CG.cs +++ b/src/OpenTK.Platform.Native/macOS/CG.cs @@ -60,6 +60,12 @@ internal static unsafe class CG [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] internal static extern uint /* CGDirectDisplayID */ CGMainDisplayID(); + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + internal static extern CGError CGAssociateMouseAndMouseCursorPosition([MarshalAs(UnmanagedType.Bool)] bool connected); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + internal static extern CGError CGDisplayMoveCursorToPoint(uint /* CGDirectDisplayID */ display, CGPoint point); + /// /// Flips the Y coordinate from a bottom to top space to a top to bottom space, and vice versa. /// diff --git a/src/OpenTK.Platform.Native/macOS/Handles.cs b/src/OpenTK.Platform.Native/macOS/Handles.cs index bbdda07239..132b53e3c1 100644 --- a/src/OpenTK.Platform.Native/macOS/Handles.cs +++ b/src/OpenTK.Platform.Native/macOS/Handles.cs @@ -1,5 +1,7 @@ using System; using OpenTK.Core.Platform; +using OpenTK.Mathematics; + namespace OpenTK.Platform.Native.macOS { internal class NSWindowHandle : WindowHandle @@ -32,6 +34,8 @@ internal class NSWindowHandle : WindowHandle public CGRect PreviousFrame { get; set; } public CursorCaptureMode CursorCaptureMode { get; set; } = CursorCaptureMode.Normal; + // FIXME: Should this be floats and not integers? + public CGPoint LastMousePosition { get; set; } public NSWindowHandle(IntPtr window, IntPtr view, GraphicsApiHints graphicsApiHints) : base(graphicsApiHints) { diff --git a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs index d784332999..56bc048c1f 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs @@ -66,6 +66,8 @@ public void SetPosition(int x, int y) float flippedY = CG.FlipYCoordinate(y); CGWarpMouseCursorPosition(new CGPoint(x, flippedY)); + + // FIXME: Should this generate an event? } // FIXME: This is only a 32-bit float and diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index 8be6aa5f8a..a36a3b137a 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -141,6 +141,9 @@ public class MacOSWindowComponent : IWindowComponent internal static readonly SEL selSetMouseConfinementRect = sel_registerName("setMouseConfinementRect:"u8); internal static readonly SEL selMouseConfinementRect = sel_registerName("mouseConfinementRect"u8); + internal static readonly SEL selHide = sel_registerName("hide"u8); + internal static readonly SEL selUnhide = sel_registerName("unhide"u8); + internal static readonly IntPtr NSDefaultRunLoop = GetStringConstant(FoundationLibrary, "NSDefaultRunLoopMode"u8); internal static readonly ObjCClass NSWindowClass = objc_getClass("NSWindow"u8); @@ -154,6 +157,7 @@ public class MacOSWindowComponent : IWindowComponent internal static readonly ObjCClass NSMutableAttributedStringClass = objc_getClass("NSMutableAttributedString"u8); internal static readonly ObjCClass NSImageViewClass = objc_getClass("NSImageView"u8); internal static readonly ObjCClass NSNotificationCenterClass = objc_getClass("NSNotificationCenter"u8); + internal static readonly ObjCClass NSCursorClass = objc_getClass("NSCursor"u8); internal static ObjCClass NSOpenTKWindowClass; internal static ObjCClass NSOpenTKViewClass; @@ -357,6 +361,13 @@ private static void NSOtkWindowDelegate_WindowDidBecomeKey(IntPtr @delegate, SEL IntPtr window = objc_msgSend_IntPtr(notification, selObject); if (NSWindowDict.TryGetValue(window, out NSWindowHandle? nswindow)) { + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked) + { + // FIXME: While we are resizing we don't want to center + // the mosue cursor... + CenterCursor(nswindow); + } + EventQueue.Raise(nswindow, PlatformEventType.Focus, new FocusEventArgs(nswindow, true)); } else @@ -394,6 +405,13 @@ private static void NSOtkWindowDelegate_WindowDidResize(IntPtr @delegate, SEL se objc_msgSend(nswindow.Context.Context, selUpdate); } + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked) + { + // FIXME: While we are resizing we don't want to center + // the mosue cursor... + //CenterCursor(nswindow); + } + CGRect bounds = objc_msgSend_CGRect(nswindow.View, selBounds); CGRect backing = objc_msgSend_CGRect(nswindow.View, selConvertRectToBacking, bounds); // FIXME: Framebuffer size event? @@ -421,6 +439,13 @@ private static void NSOtkWindowDelegate_WindowDidMove(IntPtr @delegate, SEL sele objc_msgSend(nswindow.Context.Context, selUpdate); } + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked) + { + // FIXME: While we are resizing we don't want to center + // the mosue cursor... + //CenterCursor(nswindow); + } + CGRect windowFrame = objc_msgSend_CGRect(nswindow.Window, selFrame); CGRect viewFrame = objc_msgSend_CGRect(nswindow.View, selFrame); @@ -804,7 +829,7 @@ private bool DoHitTest(NSWindowHandle window, CGPoint point) public bool CanSetCursor => true; /// - public bool CanCaptureCursor => throw new NotImplementedException(); + public bool CanCaptureCursor => true; /// public IReadOnlyList SupportedEvents => throw new NotImplementedException(); @@ -1923,7 +1948,6 @@ public void SetCursor(WindowHandle handle, CursorHandle? cursor) objc_msgSend(nswindow.Window, selInvalidateCursorRectsForView, nswindow.View); } - /// public CursorCaptureMode GetCursorCaptureMode(WindowHandle handle) { @@ -1945,6 +1969,12 @@ public void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode) objc_msgSend(nswindow.Window, selSetMouseConfinementRect, rect); } + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked && + mode != CursorCaptureMode.Locked) + { + CG.CGAssociateMouseAndMouseCursorPosition(true); + } + switch (mode) { case CursorCaptureMode.Normal: @@ -1966,12 +1996,35 @@ public void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode) // Then we need to disable this when we loose key status and enable it again when we get key status again // We also need to handle warping the cursor back to the center of the window every time the cursor moves. // - Noggin_bops 2024-04-13 + + // FIXME: Do we need to call CGAssociateMouseAndMouseCursorPosition? + // FIXME: We want to move the mouse display to the center of the screen. + // objc_msgSend((IntPtr)NSCursorClass, selHide); + + CG.CGAssociateMouseAndMouseCursorPosition(false); + + CenterCursor(nswindow); + + nswindow.CursorCaptureMode = CursorCaptureMode.Locked; break; default: break; } } + private static void CenterCursor(NSWindowHandle nswindow) + { + CGRect frame = objc_msgSend_CGRect(nswindow.View, selFrame); + CGRect screenFrame = objc_msgSend_CGRect(nswindow.Window, selConvertRectToScreen, frame); + + CGPoint center = new CGPoint(screenFrame.origin.x + (screenFrame.size.x / 2), + screenFrame.origin.y + (screenFrame.size.y / 2)); + + CG.CGDisplayMoveCursorToPoint(CG.CGMainDisplayID(), center); + CG.CGWarpMouseCursorPosition(center); + nswindow.LastMousePosition = center; + } + /// public void FocusWindow(WindowHandle handle) { From cbebca7ab151c1e0a38e7127cdba9822fbb9a135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sun, 14 Apr 2024 14:23:17 +0200 Subject: [PATCH 08/62] Switched to using UnmanagedCallersOnly and function pointers for macOS interop. --- .../macOS/MacOSWindowComponent.cs | 219 ++++++++---------- src/OpenTK.Platform.Native/macOS/ObjC.cs | 11 +- 2 files changed, 109 insertions(+), 121 deletions(-) diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index a36a3b137a..56d2671573 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -174,7 +174,7 @@ public class MacOSWindowComponent : IWindowComponent public ILogger? Logger { get; set; } /// - public void Initialize(PalComponents which) + public unsafe void Initialize(PalComponents which) { if (which != PalComponents.Window) { @@ -182,7 +182,7 @@ public void Initialize(PalComponents which) } // This method is called from the Quit menu option. - class_addMethod(NSApplicationClass, selQuit, Menu_QuitInst, "v@:"u8); + class_addMethod(NSApplicationClass, selQuit, (IntPtr)Menu_QuitInst, "v@:"u8); nsApplication = objc_msgSend_IntPtr((IntPtr)NSApplicationClass, selSharedApplication); @@ -242,20 +242,20 @@ public void Initialize(PalComponents which) class_addIvar(NSOpenTKWindowClass, "otkPALWindowComponent"u8, (nuint)nuint.Size, (nuint)int.Log2(nuint.Size), "^v"u8); // NSWindow methods. - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowShouldClose:"u8), NSOtkWindow_WindowShouldCloseInst, "b@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("zoom:"u8), NSOtkWindow_ZoomInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowShouldClose:"u8), (IntPtr)NSOtkWindow_WindowShouldCloseInst, "b@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("zoom:"u8), (IntPtr)NSOtkWindow_ZoomInst, "v@:@"u8); //class_addMethod(opentkWindowClass, sel_registerName("keyDown:"u8), (KeyDownDelegate)keyDown, "v@:@"u8); // NSWindowDelegate methods. We set the window as it's own delegate. - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidBecomeKey:"u8), NSOtkWindowDelegate_WindowDidBecomeKeyInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidResignKey:"u8), NSOtkWindowDelegate_WindowDidResignKeyInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidResize:"u8), NSOtkWindowDelegate_WindowDidResizeInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidMove:"u8), NSOtkWindowDelegate_WindowDidMoveInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidMiniaturize:"u8), NSOtkWindowDelegate_WindowDidMiniaturizeInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidDeminiaturize:"u8), NSOtkWindowDelegate_WindowDidDeminiaturizeInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidEnterFullScreen:"u8), NSOtkWindowDelegate_WindowDidEnterFullScreenInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidExitFullScreen:"u8), NSOtkWindowDelegate_WindowDidExitFullScreenInst, "v@:@"u8); - class_addMethod(NSOpenTKWindowClass, sel_registerName("didChangeScreenParameters:"u8), NSOtkWindowDelegate_DidChangeScreenParametersInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidBecomeKey:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidBecomeKeyInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidResignKey:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidResignKeyInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidResize:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidResizeInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidMove:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidMoveInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidMiniaturize:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidMiniaturizeInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidDeminiaturize:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidDeminiaturizeInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidEnterFullScreen:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidEnterFullScreenInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("windowDidExitFullScreen:"u8), (IntPtr)NSOtkWindowDelegate_WindowDidExitFullScreenInst, "v@:@"u8); + class_addMethod(NSOpenTKWindowClass, sel_registerName("didChangeScreenParameters:"u8), (IntPtr)NSOtkWindowDelegate_DidChangeScreenParametersInst, "v@:@"u8); objc_registerClassPair(NSOpenTKWindowClass); @@ -265,36 +265,36 @@ public void Initialize(PalComponents which) // Add an IVar for the markedText, so we can use it without finding the managed window object. class_addIvar(NSOpenTKViewClass, "markedText"u8, (nuint)nuint.Size, (nuint)int.Log2(nuint.Size), "@"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("resetCursorRects"u8), NSOtkView_ResetCursorRectsInst, "v@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("mouseEntered:"u8), NSOtkView_MouseEnteredInst, "v@:@"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("mouseExited:"u8), NSOtkView_MouseExitedInst, "v@:@"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("keyDown:"u8), NSOtkView_KeyDownInst, "v@:@"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("resetCursorRects"u8), (IntPtr)NSOtkView_ResetCursorRectsInst, "v@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("mouseEntered:"u8), (IntPtr)NSOtkView_MouseEnteredInst, "v@:@"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("mouseExited:"u8), (IntPtr)NSOtkView_MouseExitedInst, "v@:@"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("keyDown:"u8), (IntPtr)NSOtkView_KeyDownInst, "v@:@"u8); // TODO: canBecomeKeyView, - class_addMethod(NSOpenTKViewClass, sel_registerName("acceptsFirstResponder"u8), NSOtkView_AcceptsFirstResponderInst, "c@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("viewDidChangeEffectiveAppearance"u8), NSOtkView_ViewDidChangeEffectiveAppearanceInst, "v@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("acceptsFirstResponder"u8), (IntPtr)NSOtkView_AcceptsFirstResponderInst, "c@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("viewDidChangeEffectiveAppearance"u8), (IntPtr)NSOtkView_ViewDidChangeEffectiveAppearanceInst, "v@:"u8); // NSTextInputClientProtocol functions class_addProtocol(NSOpenTKViewClass, NSTextInputClientProtocol); // FIXME: The method type encoding strings only work for 64-bit, as NSRange is defined using NSUInteger so it changes size depending on arch. // So {_NSRange=QQ} and {_NSPoint=dd} would have to have something else other than QQ and dd, but I can't figure out what it's supposed to be atm.. // - Noggin_bops 2023-11-11 - class_addMethod(NSOpenTKViewClass, sel_registerName("hasMarkedText"u8), NSOtkView_NSTextInputClient_HasMarkedTextInst, "c@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("markedRange"u8), NSOtkView_NSTextInputClient_MarkedRangeInst, "{_NSRange=QQ}@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("selectedRange"u8), NSOtkView_NSTextInputClient_SelectedRangeInst, "{_NSRange=QQ}@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("setMarkedText:selectedRange:replacementRange:"u8), NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRangeInst, "v@:@{_NSRange=QQ}{_NSRange=QQ}"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("unmarkText"u8), NSOtkView_NSTextInputClient_UnmarkTextInst, "v@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("validAttributesForMarkedText"u8), NSOtkView_NSTextInputClient_ValidAttributesForMarkedTextInst, "@@:"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("attributedSubstringForProposedRange:actualRange:"u8), NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRangeInst, "@@:{_NSRange=QQ}^{_NSRange=QQ}"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("insertText:replacementRange:"u8), NSOtkView_NSTextInputClient_InsertText_ReplacementRangeInst, "v@:@{_NSRange=QQ}"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("characterIndexForPoint:"u8), NSOtkView_NSTextInputClient_CharacterIndexForPointInst, "Q@:{_NSPoint=dd}"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("firstRectForCharacterRange:actualRange:"u8), NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRangeInst, "{_NSRect={_NSPoint=dd}{_NSSize=dd}}@:{_NSRange=QQ}^{_NSRange=QQ}"u8); - class_addMethod(NSOpenTKViewClass, sel_registerName("doCommandBySelector:"u8), NSOtkView_NSTextInputClient_DoCommandBySelectorInst, "v@::"u8);; + class_addMethod(NSOpenTKViewClass, sel_registerName("hasMarkedText"u8), (IntPtr)NSOtkView_NSTextInputClient_HasMarkedTextInst, "c@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("markedRange"u8), (IntPtr)NSOtkView_NSTextInputClient_MarkedRangeInst, "{_NSRange=QQ}@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("selectedRange"u8), (IntPtr)NSOtkView_NSTextInputClient_SelectedRangeInst, "{_NSRange=QQ}@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("setMarkedText:selectedRange:replacementRange:"u8), (IntPtr)NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRangeInst, "v@:@{_NSRange=QQ}{_NSRange=QQ}"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("unmarkText"u8), (IntPtr)NSOtkView_NSTextInputClient_UnmarkTextInst, "v@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("validAttributesForMarkedText"u8), (IntPtr)NSOtkView_NSTextInputClient_ValidAttributesForMarkedTextInst, "@@:"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("attributedSubstringForProposedRange:actualRange:"u8), (IntPtr)NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRangeInst, "@@:{_NSRange=QQ}^{_NSRange=QQ}"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("insertText:replacementRange:"u8), (IntPtr)NSOtkView_NSTextInputClient_InsertText_ReplacementRangeInst, "v@:@{_NSRange=QQ}"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("characterIndexForPoint:"u8), (IntPtr)NSOtkView_NSTextInputClient_CharacterIndexForPointInst, "Q@:{_NSPoint=dd}"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("firstRectForCharacterRange:actualRange:"u8), (IntPtr)NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRangeInst, "{_NSRect={_NSPoint=dd}{_NSSize=dd}}@:{_NSRange=QQ}^{_NSRange=QQ}"u8); + class_addMethod(NSOpenTKViewClass, sel_registerName("doCommandBySelector:"u8), (IntPtr)NSOtkView_NSTextInputClient_DoCommandBySelectorInst, "v@::"u8);; objc_registerClassPair(NSOpenTKViewClass); } - private delegate void Menu_Quit_(IntPtr self, SEL cmd); - private static readonly Menu_Quit_ Menu_QuitInst = Menu_Quit; + private static unsafe readonly delegate* unmanaged[Cdecl] Menu_QuitInst = &Menu_Quit; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void Menu_Quit(IntPtr self, SEL cmd) { Console.WriteLine("On quit!"); @@ -306,27 +306,26 @@ private static void Menu_Quit(IntPtr self, SEL cmd) objc_msgSend(nsApplication, selTerminate, nsApplication); } - private delegate bool NSOtkWindow_WindowShouldClose_(IntPtr windowPtr, SEL selector, IntPtr sender); - static NSOtkWindow_WindowShouldClose_ NSOtkWindow_WindowShouldCloseInst = NSOtkWindow_WindowShouldClose; - private static bool NSOtkWindow_WindowShouldClose(IntPtr window, SEL selector, IntPtr sender) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindow_WindowShouldCloseInst = &NSOtkWindow_WindowShouldClose; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static sbyte NSOtkWindow_WindowShouldClose(IntPtr window, SEL selector, IntPtr sender) { if (NSWindowDict.TryGetValue(window, out NSWindowHandle? nswindow)) { EventQueue.Raise(nswindow, PlatformEventType.Close, new CloseEventArgs(nswindow)); // Let the user handle closing the window. - return false; + return (sbyte)NO; } else { // We don't know about this window, let it close. - return true; + return (sbyte)YES; } } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSOtkWindow_Zoom_(IntPtr window, SEL selector, IntPtr sender); - private static readonly NSOtkWindow_Zoom_ NSOtkWindow_ZoomInst = NSOtkWindow_Zoom; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindow_ZoomInst = &NSOtkWindow_Zoom; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindow_Zoom(IntPtr window, SEL selector, IntPtr sender) { objc_super super; @@ -349,13 +348,8 @@ private static void NSOtkWindow_Zoom(IntPtr window, SEL selector, IntPtr sender) } } - // We reuse this delegate definition for all window delegate notifications that return void - // and takes a single NSNotification as it's arguments. - // - Noggin_bops 2023-11-11 - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - delegate void NSOtkWindowDelegate_Notification(IntPtr @delegate, SEL selector, IntPtr notification); - - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidBecomeKeyInst = NSOtkWindowDelegate_WindowDidBecomeKey; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidBecomeKeyInst = &NSOtkWindowDelegate_WindowDidBecomeKey; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidBecomeKey(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -378,7 +372,8 @@ private static void NSOtkWindowDelegate_WindowDidBecomeKey(IntPtr @delegate, SEL } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidResignKeyInst = NSOtkWindowDelegate_WindowDidResignKey; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidResignKeyInst = &NSOtkWindowDelegate_WindowDidResignKey; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidResignKey(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -394,7 +389,8 @@ private static void NSOtkWindowDelegate_WindowDidResignKey(IntPtr @delegate, SEL } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidResizeInst = NSOtkWindowDelegate_WindowDidResize; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidResizeInst = &NSOtkWindowDelegate_WindowDidResize; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidResize(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -428,7 +424,8 @@ private static void NSOtkWindowDelegate_WindowDidResize(IntPtr @delegate, SEL se } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidMoveInst = NSOtkWindowDelegate_WindowDidMove; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidMoveInst = &NSOtkWindowDelegate_WindowDidMove; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidMove(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -465,7 +462,8 @@ private static void NSOtkWindowDelegate_WindowDidMove(IntPtr @delegate, SEL sele } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidMiniaturizeInst = NSOtkWindowDelegate_WindowDidMiniaturize; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidMiniaturizeInst = &NSOtkWindowDelegate_WindowDidMiniaturize; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidMiniaturize(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -481,7 +479,8 @@ private static void NSOtkWindowDelegate_WindowDidMiniaturize(IntPtr @delegate, S } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidDeminiaturizeInst = NSOtkWindowDelegate_WindowDidDeminiaturize; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidDeminiaturizeInst = &NSOtkWindowDelegate_WindowDidDeminiaturize; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidDeminiaturize(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -497,7 +496,8 @@ private static void NSOtkWindowDelegate_WindowDidDeminiaturize(IntPtr @delegate, } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidEnterFullScreenInst = NSOtkWindowDelegate_WindowDidEnterFullScreen; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidEnterFullScreenInst = &NSOtkWindowDelegate_WindowDidEnterFullScreen; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidEnterFullScreen(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -514,7 +514,8 @@ private static void NSOtkWindowDelegate_WindowDidEnterFullScreen(IntPtr @delegat } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_WindowDidExitFullScreenInst = NSOtkWindowDelegate_WindowDidExitFullScreen; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_WindowDidExitFullScreenInst = &NSOtkWindowDelegate_WindowDidExitFullScreen; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_WindowDidExitFullScreen(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { IntPtr window = objc_msgSend_IntPtr(notification, selObject); @@ -530,7 +531,8 @@ private static void NSOtkWindowDelegate_WindowDidExitFullScreen(IntPtr @delegate } } - private static readonly NSOtkWindowDelegate_Notification NSOtkWindowDelegate_DidChangeScreenParametersInst = NSOtkWindowDelegate_DidChangeScreenParameters; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkWindowDelegate_DidChangeScreenParametersInst = &NSOtkWindowDelegate_DidChangeScreenParameters; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkWindowDelegate_DidChangeScreenParameters(IntPtr @delegate, SEL selector, IntPtr /* NSNotification */ notification) { object_getInstanceVariable(@delegate, "otkPALWindowComponent"u8, out IntPtr windowCompPtr); @@ -542,10 +544,9 @@ private static void NSOtkWindowDelegate_DidChangeScreenParameters(IntPtr @delega MacOSDisplayComponent.UpdateDisplays(logger, true); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - delegate void NSOtkView_ResetCursorRects_(IntPtr view, SEL selector); - static NSOtkView_ResetCursorRects_ NSOtkView_ResetCursorRectsInst = NSOtkView_ResetCursorRects; - static void NSOtkView_ResetCursorRects(IntPtr view, SEL selector) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_ResetCursorRectsInst = &NSOtkView_ResetCursorRects; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static void NSOtkView_ResetCursorRects(IntPtr view, SEL selector) { objc_super super; super.receiver = view; @@ -584,10 +585,9 @@ static void NSOtkView_ResetCursorRects(IntPtr view, SEL selector) } } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - delegate void MouseEnteredDelegate(IntPtr view, SEL selector, IntPtr @event); - static MouseEnteredDelegate NSOtkView_MouseEnteredInst = NSOtkView_MouseEntered; - static void NSOtkView_MouseEntered(IntPtr view, SEL selector, IntPtr @event) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_MouseEnteredInst = &NSOtkView_MouseEntered; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static void NSOtkView_MouseEntered(IntPtr view, SEL selector, IntPtr @event) { IntPtr window = objc_msgSend_IntPtr(@event, selWindow); if (NSWindowDict.TryGetValue(window, out NSWindowHandle? nswindow) == false) @@ -601,10 +601,9 @@ static void NSOtkView_MouseEntered(IntPtr view, SEL selector, IntPtr @event) EventQueue.Raise(nswindow, PlatformEventType.MouseEnter, new MouseEnterEventArgs(nswindow, true)); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - delegate void MouseExitedDelegate(IntPtr view, SEL selector, IntPtr @event); - static MouseExitedDelegate NSOtkView_MouseExitedInst = NSOtkView_MouseExited; - static void NSOtkView_MouseExited(IntPtr view, SEL selector, IntPtr @event) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_MouseExitedInst = &NSOtkView_MouseExited; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static void NSOtkView_MouseExited(IntPtr view, SEL selector, IntPtr @event) { IntPtr window = objc_msgSend_IntPtr(@event, selWindow); if (NSWindowDict.TryGetValue(window, out NSWindowHandle? nswindow) == false) @@ -618,46 +617,40 @@ static void NSOtkView_MouseExited(IntPtr view, SEL selector, IntPtr @event) EventQueue.Raise(nswindow, PlatformEventType.MouseEnter, new MouseEnterEventArgs(nswindow, false)); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSOtkView_KeyDown_(IntPtr view, SEL selector, IntPtr @event); - private static NSOtkView_KeyDown_ NSOtkView_KeyDownInst = NSOtkView_KeyDown; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_KeyDownInst = &NSOtkView_KeyDown; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_KeyDown(IntPtr view, SEL selector, IntPtr @event) { IntPtr array = objc_msgSend_IntPtr((IntPtr)NSArrayClass, selArrayWithObject, @event); objc_msgSend_IntPtr(view, selInterpretKeyEvents, array); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.I1)] - private delegate bool NSOtkView_AcceptsFirstResponder_(IntPtr view, SEL selector); - private static NSOtkView_AcceptsFirstResponder_ NSOtkView_AcceptsFirstResponderInst = NSOtkView_AcceptsFirstResponder; - private static bool NSOtkView_AcceptsFirstResponder(IntPtr view, SEL selector) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_AcceptsFirstResponderInst = &NSOtkView_AcceptsFirstResponder; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static byte NSOtkView_AcceptsFirstResponder(IntPtr view, SEL selector) { - return true; + return YES; } - private delegate void NSOtkView_ViewDidChangeEffectiveAppearance_(IntPtr view, SEL selector); - private static NSOtkView_ViewDidChangeEffectiveAppearance_ NSOtkView_ViewDidChangeEffectiveAppearanceInst = NSOtkView_ViewDidChangeEffectiveAppearance; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_ViewDidChangeEffectiveAppearanceInst = &NSOtkView_ViewDidChangeEffectiveAppearance; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_ViewDidChangeEffectiveAppearance(IntPtr view, SEL selector) { MacOSShellComponent.CheckPreferredThemeChange(); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.I1)] - private delegate bool /* BOOL */ NSTextInputClient_HasMarkedText(IntPtr view, SEL selector); - private static readonly NSTextInputClient_HasMarkedText NSOtkView_NSTextInputClient_HasMarkedTextInst = NSOtkView_NSTextInputClient_HasMarkedText; - private static bool /* BOOL */ NSOtkView_NSTextInputClient_HasMarkedText(IntPtr view, SEL selector) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_HasMarkedTextInst = &NSOtkView_NSTextInputClient_HasMarkedText; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static sbyte /* BOOL */ NSOtkView_NSTextInputClient_HasMarkedText(IntPtr view, SEL selector) { // FIXME: Store the Ivar somewhere to be able to use object_getIvar? object_getInstanceVariable(view, "markedText"u8, out IntPtr markedText); ulong length = (ulong)objc_msgSend_IntPtr(markedText, selLength); - return length > 0; + return length > 0 ? (sbyte)YES : (sbyte)NO; } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate NSRange NSTextInputClient_MarkedRange(IntPtr view, SEL selector); - private static readonly NSTextInputClient_MarkedRange NSOtkView_NSTextInputClient_MarkedRangeInst = NSOtkView_NSTextInputClient_MarkedRange; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_MarkedRangeInst = &NSOtkView_NSTextInputClient_MarkedRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static NSRange NSOtkView_NSTextInputClient_MarkedRange(IntPtr view, SEL selector) { // FIXME: Store the Ivar somewhere to be able to use object_getIvar? @@ -673,17 +666,15 @@ private static NSRange NSOtkView_NSTextInputClient_MarkedRange(IntPtr view, SEL } } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate NSRange NSTextInputClient_SelectedRange(IntPtr view, SEL selector); - private static readonly NSTextInputClient_SelectedRange NSOtkView_NSTextInputClient_SelectedRangeInst = NSOtkView_NSTextInputClient_SelectedRange; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_SelectedRangeInst = &NSOtkView_NSTextInputClient_SelectedRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static NSRange NSOtkView_NSTextInputClient_SelectedRange(IntPtr view, SEL selector) { return NSRange.kEmptyRange; } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRange(IntPtr view, SEL selector, IntPtr /* id */ @string, NSRange selectedRange, NSRange replacementRange); - private static readonly NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRange NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRangeInst = NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRange; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRangeInst = &NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_ReplacementRange(IntPtr view, SEL selector, IntPtr /* id */ @string, NSRange selectedRange, NSRange replacementRange) { // FIXME: Store the Ivar somewhere to be able to use object_getIvar? @@ -703,9 +694,8 @@ private static void NSOtkView_NSTextInputClient_SetMarkedText_SelectedRange_Repl object_setInstanceVariable(view, "markedText"u8, markedText); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSTextInputClient_UnmarkText(IntPtr view, SEL selector); - private static readonly NSTextInputClient_UnmarkText NSOtkView_NSTextInputClient_UnmarkTextInst = NSOtkView_NSTextInputClient_UnmarkText; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_UnmarkTextInst = &NSOtkView_NSTextInputClient_UnmarkText; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_NSTextInputClient_UnmarkText(IntPtr view, SEL selector) { // FIXME: Store the Ivar somewhere to be able to use object_getIvar? @@ -714,25 +704,22 @@ private static void NSOtkView_NSTextInputClient_UnmarkText(IntPtr view, SEL sele objc_msgSend(objc_msgSend_IntPtr(markedText, selMutableString), selSetString, ToNSString("")); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr /* NSArray* */ NSTextInputClient_ValidAttributesForMarkedText(IntPtr view, SEL selector); - private static readonly NSTextInputClient_ValidAttributesForMarkedText NSOtkView_NSTextInputClient_ValidAttributesForMarkedTextInst = NSOtkView_NSTextInputClient_ValidAttributesForMarkedText; - private static IntPtr /* NSAttributedString* */ NSOtkView_NSTextInputClient_ValidAttributesForMarkedText(IntPtr view, SEL selector) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_ValidAttributesForMarkedTextInst = &NSOtkView_NSTextInputClient_ValidAttributesForMarkedText; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static IntPtr /* NSArray* */ NSOtkView_NSTextInputClient_ValidAttributesForMarkedText(IntPtr view, SEL selector) { return objc_msgSend_IntPtr((IntPtr)NSArrayClass, selArray); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr /* NSAttributedString* */ NSTextInputClient_AttributedSubstringForProposedRange_ActualRange(IntPtr view, SEL selector, NSRange range, ref NSRange actualRange); - private static readonly NSTextInputClient_AttributedSubstringForProposedRange_ActualRange NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRangeInst = NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRange; - private static IntPtr /* NSAttributedString* */ NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRange(IntPtr view, SEL selector, NSRange range, ref NSRange actualRange) + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRangeInst = &NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] + private static unsafe IntPtr /* NSAttributedString* */ NSOtkView_NSTextInputClient_AttributedSubstringForProposedRange_ActualRange(IntPtr view, SEL selector, NSRange range, NSRange* actualRange) { return IntPtr.Zero; } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSTextInputClient_InsertText_ReplacementRange(IntPtr view, SEL selector, IntPtr /* id */ @string, NSRange range); - private static readonly NSTextInputClient_InsertText_ReplacementRange NSOtkView_NSTextInputClient_InsertText_ReplacementRangeInst = NSOtkView_NSTextInputClient_InsertText_ReplacementRange; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_InsertText_ReplacementRangeInst = &NSOtkView_NSTextInputClient_InsertText_ReplacementRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_NSTextInputClient_InsertText_ReplacementRange(IntPtr view, SEL selector, IntPtr /* id */ @string, NSRange range) { IntPtr windowPtr = objc_msgSend_IntPtr(view, selWindow); @@ -755,17 +742,15 @@ private static void NSOtkView_NSTextInputClient_InsertText_ReplacementRange(IntP EventQueue.Raise(nswindow, PlatformEventType.TextInput, new TextInputEventArgs(nswindow, str)); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate nuint NSTextInputClient_CharacterIndexForPoint(IntPtr view, SEL selector, CGPoint point); - private static readonly NSTextInputClient_CharacterIndexForPoint NSOtkView_NSTextInputClient_CharacterIndexForPointInst = NSOtkView_NSTextInputClient_CharacterIndexForPoint; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_CharacterIndexForPointInst = &NSOtkView_NSTextInputClient_CharacterIndexForPoint; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static nuint NSOtkView_NSTextInputClient_CharacterIndexForPoint(IntPtr view, SEL selector, CGPoint point) { return 0; } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate CGRect NSTextInputClient_FirstRectForCharacterRange_ActualRange(IntPtr view, SEL selector, CGPoint point); - private static readonly NSTextInputClient_FirstRectForCharacterRange_ActualRange NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRangeInst = NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRange; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRangeInst = &NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRange; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static CGRect NSOtkView_NSTextInputClient_FirstRectForCharacterRange_ActualRange(IntPtr view, SEL selector, CGPoint point) { // FIXME: Why do we do this? @@ -773,9 +758,8 @@ private static CGRect NSOtkView_NSTextInputClient_FirstRectForCharacterRange_Act return new CGRect(frame.origin, CGPoint.Zero); } - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void NSTextInputClient_DoCommandBySelector(IntPtr view, SEL _selector, SEL selector); - private static readonly NSTextInputClient_DoCommandBySelector NSOtkView_NSTextInputClient_DoCommandBySelectorInst = NSOtkView_NSTextInputClient_DoCommandBySelector; + private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_NSTextInputClient_DoCommandBySelectorInst = &NSOtkView_NSTextInputClient_DoCommandBySelector; + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvCdecl) })] private static void NSOtkView_NSTextInputClient_DoCommandBySelector(IntPtr view, SEL _selector, SEL selector) { Console.WriteLine("DoCommandBySelector"); @@ -2012,6 +1996,9 @@ public void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode) } } + /// + /// Centers the cursor in the window. + /// private static void CenterCursor(NSWindowHandle nswindow) { CGRect frame = objc_msgSend_CGRect(nswindow.View, selFrame); diff --git a/src/OpenTK.Platform.Native/macOS/ObjC.cs b/src/OpenTK.Platform.Native/macOS/ObjC.cs index 62b389c12b..78bb60da37 100644 --- a/src/OpenTK.Platform.Native/macOS/ObjC.cs +++ b/src/OpenTK.Platform.Native/macOS/ObjC.cs @@ -26,6 +26,9 @@ internal static unsafe class ObjC internal static readonly IntPtr /* NSString */ NSApplicationDidChangeScreenParametersNotification = GetStringConstant(AppKitLibrary, "NSApplicationDidChangeScreenParametersNotification"u8); + internal const byte YES = 1; + internal const byte NO = 0; + // FIXME: Number type enum! internal const int kCFNumberIntType = 9; @@ -406,13 +409,11 @@ internal static bool class_addIvar(ObjCClass cls, ReadOnlySpan name, nuint [DllImport(FoundationFramework)] internal static extern bool class_addProtocol(ObjCClass cls, IntPtr /* Protocol */ protocol); - internal static bool class_addMethod(ObjCClass cls, SEL name, Delegate imp, ReadOnlySpan types) + internal static bool class_addMethod(ObjCClass cls, SEL name, IntPtr imp, ReadOnlySpan types) { - // FIXME: Maybe avoid marshalling the delegate? - IntPtr impPtr = Marshal.GetFunctionPointerForDelegate(imp); - fixed(byte* ptr = types) + fixed (byte* ptr = types) { - return class_addMethod(cls, name, impPtr, ptr); + return class_addMethod(cls, name, imp, ptr); } // FIXME: What framework? From 03aff9eea48b1d1830137f1321fa109c745085c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Sun, 14 Apr 2024 14:47:33 +0200 Subject: [PATCH 09/62] Small macOS backend cleanup. --- .../macOS/MacOSCursorComponent.cs | 8 ++++- .../macOS/MacOSDisplayComponent.cs | 29 +++++++------------ .../macOS/MacOSOpenGLComponent.cs | 4 +-- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs index 0808dfa633..887bc384e9 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs @@ -91,7 +91,6 @@ public void Initialize(PalComponents which) /// public bool CanInspectSystemCursors => true; - // FIXME: Make two separate functions, one for loading animated cursors. // FIXME: Maybe do the string manipulation in UTF-8 for efficiency? ""u8 etc. internal static IntPtr[] LoadAnimatedHiddenCursor(string cursorName, SEL fallback, out double delay) { @@ -159,6 +158,11 @@ internal IntPtr LoadHiddenCursor(string cursorName, SEL fallback) // FIXME: BOOL if (image == IntPtr.Zero || objc_msgSend_bool(image, selIsValid) == false) { + if (image != IntPtr.Zero) + { + // If we got an image, release our reference to it. + objc_msgSend(image, Release); + } // We could not load the image.. return the fallback. // FIXME: Can we avoid returning this array? return objc_msgSend_IntPtr((IntPtr)NSCursorClass, fallback); @@ -171,6 +175,8 @@ internal IntPtr LoadHiddenCursor(string cursorName, SEL fallback) double hotY = objc_msgSend_double(objc_msgSend_IntPtr(dict, selValueForKey, ToNSString("hoty"u8)), selDoubleValue); CGPoint hotspot = new CGPoint((NFloat)hotX, (NFloat)hotY); + // FIXME: Make sure to release the reference to the allocated image, and other allocated things. + if (frames > 1) { NSSize originalSize = objc_msgSend_NSSize(image, selSize); diff --git a/src/OpenTK.Platform.Native/macOS/MacOSDisplayComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSDisplayComponent.cs index d0e75d6bff..6c3b2688fa 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSDisplayComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSDisplayComponent.cs @@ -209,18 +209,6 @@ internal static DisplayHandle FindDisplay(IntPtr /* NSScreen */ nsscreen) throw new ArgumentException($"Cannot find display handle for NSScreen=0x{nsscreen}."); } - // FIXME: Remove this - internal Box2i ConvertCoordinates(CGRect rect) - { - Box2i area = new Box2i( - (int)rect.origin.x, - (int)FlipYCoordinate(rect.origin.y + rect.size.y), - (int)(rect.origin.x + rect.size.x), - (int)FlipYCoordinate(rect.origin.y)); - - return area; - } - /// public bool CanGetVirtualPosition => true; @@ -417,9 +405,6 @@ public void GetWorkArea(DisplayHandle handle, out Box2i area) CGRect visible = objc_msgSend_CGRect(nsscreen.Screen, selVisibleFrame); - // FIXME: In a multi-monitor setup this is likely wrong? - CGRect bounds = objc_msgSend_CGRect(nsscreen.Screen, selFrame); - area = new Box2i( (int)visible.origin.x, (int)FlipYCoordinate(visible.origin.y + visible.size.y), @@ -470,8 +455,11 @@ public bool GetSafeLeftAuxArea(DisplayHandle handle, out Box2i area) } else { - // FIXME: Remove ConvertCoordinates and use CG.FlipYCoordinate directly instead. - area = ConvertCoordinates(auxArea); + area = new Box2i( + (int)auxArea.origin.x, + (int)FlipYCoordinate(auxArea.origin.y + auxArea.size.y), + (int)(auxArea.origin.x + auxArea.size.x), + (int)FlipYCoordinate(auxArea.origin.y)); return true; } } @@ -498,8 +486,11 @@ public bool GetSafeRightAuxArea(DisplayHandle handle, out Box2i area) } else { - // FIXME: Remove ConvertCoordinates and use CG.FlipYCoordinate directly instead. - area = ConvertCoordinates(auxArea); + area = new Box2i( + (int)auxArea.origin.x, + (int)FlipYCoordinate(auxArea.origin.y + auxArea.size.y), + (int)(auxArea.origin.x + auxArea.size.x), + (int)FlipYCoordinate(auxArea.origin.y)); return true; } } diff --git a/src/OpenTK.Platform.Native/macOS/MacOSOpenGLComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSOpenGLComponent.cs index 3281bcc6a7..97cc82fecc 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSOpenGLComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSOpenGLComponent.cs @@ -50,12 +50,12 @@ public void Initialize(PalComponents which) public bool CanCreateFromWindow => true; /// - public bool CanCreateFromSurface => throw new NotImplementedException(); + public bool CanCreateFromSurface => false; /// public OpenGLContextHandle CreateFromSurface() { - throw new NotImplementedException(); + throw new NotSupportedException("We currently do not support surfaces."); } /// From 25bd0c9ae5f8f530fb76eb909bacd459b9346a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Fri, 10 May 2024 20:09:58 +0200 Subject: [PATCH 10/62] Implemented CaptureMode.Locked in macOS backend. Fixed so invisible cursors work in macOS backend. Fixed off-by-one error when inverting y coordinates. Added 'FpsCamera' test application to OpenTK.Backends.Tests. Made Update(float deltaTime) work in OpenTK.Backends.Tests test applications. --- .../Platform/Interfaces/IMouseComponent.cs | 4 + .../Platform/Interfaces/IWindowComponent.cs | 2 + src/OpenTK.Core/Platform/WindowEventArgs.cs | 4 + src/OpenTK.Platform.Native/macOS/CG.cs | 6 +- src/OpenTK.Platform.Native/macOS/Handles.cs | 1 + .../macOS/MacOSCursorComponent.cs | 35 +++ .../macOS/MacOSMouseComponent.cs | 7 +- .../macOS/MacOSWindowComponent.cs | 79 +++-- src/OpenTK.Platform.Native/macOS/ObjC.cs | 3 + src/OpenTK.Platform.Native/macOS/ObjCTypes.cs | 11 + .../CoordinateSpacesView.cs | 14 + tests/OpenTK.Backends.Tests/Program.cs | 62 ++-- .../TestApps/ColorTriangle.cs | 4 +- .../TestApps/ColorWheel.cs | 4 +- .../TestApps/FpsCamera.cs | 285 ++++++++++++++++++ .../TestApps/ITestApp.cs | 3 +- .../TestApps/ModelView.cs | 3 +- .../TestApps/VsyncTest.cs | 3 +- .../WindowComponentView.cs | 12 +- 19 files changed, 470 insertions(+), 72 deletions(-) create mode 100644 tests/OpenTK.Backends.Tests/TestApps/FpsCamera.cs diff --git a/src/OpenTK.Core/Platform/Interfaces/IMouseComponent.cs b/src/OpenTK.Core/Platform/Interfaces/IMouseComponent.cs index 5c76ffcac8..8188412184 100644 --- a/src/OpenTK.Core/Platform/Interfaces/IMouseComponent.cs +++ b/src/OpenTK.Core/Platform/Interfaces/IMouseComponent.cs @@ -36,6 +36,10 @@ public interface IMouseComponent : IPalComponent /// bool CanSetMousePosition { get; } + // FIXME: When using CaptureMode.Locked should these return the virtual position? + // If not, we need to have a clear distinction between which coordinates get + // virtualized and which ones do not. + /// /// Get the mouse cursor position. /// diff --git a/src/OpenTK.Core/Platform/Interfaces/IWindowComponent.cs b/src/OpenTK.Core/Platform/Interfaces/IWindowComponent.cs index 57829d83a9..4454652785 100644 --- a/src/OpenTK.Core/Platform/Interfaces/IWindowComponent.cs +++ b/src/OpenTK.Core/Platform/Interfaces/IWindowComponent.cs @@ -417,6 +417,8 @@ public interface IWindowComponent : IPalComponent /// The cursor capture mode. void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode); + // FIXME: Need a function to check if the window has input focus... + /// /// Gives the window input focus. /// diff --git a/src/OpenTK.Core/Platform/WindowEventArgs.cs b/src/OpenTK.Core/Platform/WindowEventArgs.cs index fc1c90ff45..390c18415f 100644 --- a/src/OpenTK.Core/Platform/WindowEventArgs.cs +++ b/src/OpenTK.Core/Platform/WindowEventArgs.cs @@ -35,6 +35,8 @@ public class FocusEventArgs : WindowEventArgs { // FIXME: A reference to the window that got or lost focus? + // FIXME: The mouse position + /// /// If the window got or lost focus. /// @@ -391,6 +393,8 @@ public class MouseMoveEventArgs : WindowEventArgs { // FIXME: In what coordinate space is the mouse coords? + // FIXME: Position delta + /// /// The new position of the mouse cursor. /// diff --git a/src/OpenTK.Platform.Native/macOS/CG.cs b/src/OpenTK.Platform.Native/macOS/CG.cs index 697af9009f..4b134a04fc 100644 --- a/src/OpenTK.Platform.Native/macOS/CG.cs +++ b/src/OpenTK.Platform.Native/macOS/CG.cs @@ -74,7 +74,7 @@ internal static unsafe class CG internal static float FlipYCoordinate(float y) { float height = (float)CGDisplayBounds(CGMainDisplayID()).size.y; - return (height - 1) - y; + return height - y; } /// @@ -85,7 +85,7 @@ internal static float FlipYCoordinate(float y) internal static NFloat FlipYCoordinate(NFloat y) { NFloat height = CGDisplayBounds(CGMainDisplayID()).size.y; - return (height - 1) - y; + return height - y; } /// @@ -96,7 +96,7 @@ internal static NFloat FlipYCoordinate(NFloat y) internal static CGPoint FlipYCoordinate(CGPoint p) { NFloat height = CGDisplayBounds(CGMainDisplayID()).size.y; - return new CGPoint(p.x, (height - 1) - p.y); + return new CGPoint(p.x, height - p.y); } /// diff --git a/src/OpenTK.Platform.Native/macOS/Handles.cs b/src/OpenTK.Platform.Native/macOS/Handles.cs index 132b53e3c1..be555846d5 100644 --- a/src/OpenTK.Platform.Native/macOS/Handles.cs +++ b/src/OpenTK.Platform.Native/macOS/Handles.cs @@ -36,6 +36,7 @@ internal class NSWindowHandle : WindowHandle public CursorCaptureMode CursorCaptureMode { get; set; } = CursorCaptureMode.Normal; // FIXME: Should this be floats and not integers? public CGPoint LastMousePosition { get; set; } + public CGPoint VirtualCursorPosition { get; set; } public NSWindowHandle(IntPtr window, IntPtr view, GraphicsApiHints graphicsApiHints) : base(graphicsApiHints) { diff --git a/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs index 887bc384e9..3bd730d2f7 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSCursorComponent.cs @@ -18,6 +18,8 @@ public class MacOSCursorComponent : ICursorComponent internal static readonly ObjCClass NSDictionaryClass = objc_getClass("NSDictionary"u8); internal static readonly ObjCClass NSUserDefaultsClass = objc_getClass("NSUserDefaults"u8); internal static readonly ObjCClass NSBitmapImageRep = objc_getClass("NSBitmapImageRep"u8); + internal static readonly ObjCClass NSDataClass = objc_getClass("NSData"u8); + internal static readonly IntPtr NSCalibratedRGBColorSpace = GetStringConstant(AppKitLibrary, "NSCalibratedRGBColorSpace"u8); @@ -60,9 +62,12 @@ public class MacOSCursorComponent : ICursorComponent internal static readonly SEL selPersistentDomainForName = sel_registerName("persistentDomainForName:"u8); internal static readonly SEL selInitWithBitmapDataPlanes_PixelsWide_PixelsHigh_BitsPerSample_SamplesPerPixel_HasAlpha_IsPlanar_ColorSpaceName_BitmapFormat_BytesPerRow_BitsPerPixel = sel_registerName("initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel:"u8); + internal static readonly SEL selInitWithData = sel_registerName("initWithData:"u8); internal static readonly SEL selBitmapData = sel_registerName("bitmapData"u8); internal static readonly SEL selSetSize = sel_registerName("setSize:"u8); + internal static readonly SEL selDataWithBytesNoCopy_Length_FreeWhenDone = sel_registerName("dataWithBytesNoCopy:length:freeWhenDone:"u8); + // Keep a list of animated cursors? private List AnimatedCursors = new List(); @@ -91,6 +96,36 @@ public void Initialize(PalComponents which) /// public bool CanInspectSystemCursors => true; + static IntPtr InvisibleCursor; + internal static IntPtr GetInvisibleCursor() + { + if (InvisibleCursor == 0) + { + // Create the invisible cursor + // We could do this with a deferred drawingHandler that + // just returns true. + // For now we are taking a book from SDL and just create a + // transparent GIF that we make a cursor out of. + // - Noggin_bops 2024-05-10 + unsafe + { + // 16x16 transparent GIF. + ReadOnlySpan cursorBytes = new byte[]{ + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04, + 0x01, 0x00, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED, + 0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B + }; + IntPtr cursorBytesPtr = (IntPtr)Unsafe.AsPointer(ref MemoryMarshal.GetReference(cursorBytes)); + IntPtr cursorData = objc_msgSend_IntPtr((IntPtr)NSDataClass, selDataWithBytesNoCopy_Length_FreeWhenDone, cursorBytesPtr, (nuint)cursorBytes.Length, false); + IntPtr cursorImage = objc_msgSend_IntPtr(objc_msgSend_IntPtr((IntPtr)NSImageClass, Alloc), selInitWithData, cursorData); + InvisibleCursor = objc_msgSend_IntPtr(objc_msgSend_IntPtr((IntPtr)NSCursorClass, Alloc), selInitWithImage_HotSpot, cursorImage, CGPoint.Zero); + } + } + return InvisibleCursor; + } + // FIXME: Maybe do the string manipulation in UTF-8 for efficiency? ""u8 etc. internal static IntPtr[] LoadAnimatedHiddenCursor(string cursorName, SEL fallback, out double delay) { diff --git a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs index 56bc048c1f..c993aab5fe 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSMouseComponent.cs @@ -63,11 +63,8 @@ public void GetPosition(out int x, out int y) /// public void SetPosition(int x, int y) { - float flippedY = CG.FlipYCoordinate(y); - - CGWarpMouseCursorPosition(new CGPoint(x, flippedY)); - - // FIXME: Should this generate an event? + // CGWarpMouseCursorPosition uses top left relative coordinates. + CGWarpMouseCursorPosition(new CGPoint(x, y)); } // FIXME: This is only a 32-bit float and diff --git a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs index 56d2671573..34844e05f4 100644 --- a/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs +++ b/src/OpenTK.Platform.Native/macOS/MacOSWindowComponent.cs @@ -36,6 +36,8 @@ public class MacOSWindowComponent : IWindowComponent internal static readonly SEL selWindow = sel_registerName("window"u8); internal static readonly SEL selButtonNumber = sel_registerName("buttonNumber"u8); internal static readonly SEL selLocationInWindow = sel_registerName("locationInWindow"u8); + internal static readonly SEL selDeltaX = sel_registerName("deltaX"u8); + internal static readonly SEL selDeltaY = sel_registerName("deltaY"u8); internal static readonly SEL selMouseLocation = sel_registerName("mouseLocation"u8); internal static readonly SEL selScrollingDeltaX = sel_registerName("scrollingDeltaX"u8); internal static readonly SEL selScrollingDeltaY = sel_registerName("scrollingDeltaY"u8); @@ -359,8 +361,14 @@ private static void NSOtkWindowDelegate_WindowDidBecomeKey(IntPtr @delegate, SEL { // FIXME: While we are resizing we don't want to center // the mosue cursor... + CG.CGAssociateMouseAndMouseCursorPosition(false); CenterCursor(nswindow); } + else + { + // We don't have a locked cursor anymore. + CG.CGAssociateMouseAndMouseCursorPosition(true); + } EventQueue.Raise(nswindow, PlatformEventType.Focus, new FocusEventArgs(nswindow, true)); } @@ -564,25 +572,27 @@ private static void NSOtkView_ResetCursorRects(IntPtr view, SEL selector) return; } + IntPtr cursor; if (nswindow.Cursor != null) { - CGRect bounds = objc_msgSend_CGRect(view, selBounds); switch (nswindow.Cursor.Mode) { // If the cursor is animated, pick the current frame. case NSCursorHandle.CursorMode.SystemAnimatedCursor: - objc_msgSend(view, selAddCursorRect_Cursor, bounds, nswindow.Cursor.CursorFrames![nswindow.Cursor.Frame]); + cursor = nswindow.Cursor.CursorFrames![nswindow.Cursor.Frame]; break; default: - objc_msgSend(view, selAddCursorRect_Cursor, bounds, nswindow.Cursor.Cursor); + cursor = nswindow.Cursor.Cursor; break; } } else { - // Null cursor means it is hidden. - // FIXME: Hide the cursor? Should we do it here? + cursor = MacOSCursorComponent.GetInvisibleCursor(); } + + CGRect bounds = objc_msgSend_CGRect(view, selBounds); + objc_msgSend(view, selAddCursorRect_Cursor, bounds, cursor); } private static unsafe readonly delegate* unmanaged[Cdecl] NSOtkView_MouseEnteredInst = &NSOtkView_MouseEntered; @@ -933,6 +943,9 @@ public void ProcessEvents(bool waitForEvents = false) break; } case NSEventType.MouseMoved: + case NSEventType.LeftMouseDragged: + case NSEventType.RightMouseDragged: + case NSEventType.OtherMouseDragged: { if (ProcessHitTest(@event)) { @@ -950,32 +963,27 @@ public void ProcessEvents(bool waitForEvents = false) objc_msgSend_CGRect(nswindow.View, selBounds)); // FIXME: Coordinate space - Vector2 pos = new Vector2((float)pointRect.origin.x, (float)(backing.size.y - pointRect.origin.y)); - - EventQueue.Raise(nswindow, PlatformEventType.MouseMove, new MouseMoveEventArgs(nswindow, pos)); + CGPoint pos = new CGPoint(pointRect.origin.x, backing.size.y - pointRect.origin.y); - objc_msgSend(nsApplication, selSendEvent, @event); - break; - } - case NSEventType.LeftMouseDragged: - case NSEventType.RightMouseDragged: - case NSEventType.OtherMouseDragged: - { - CGPoint point = objc_msgSend_CGPoint(@event, selLocationInWindow); + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked) + { + // Handle virtual cursor position + NFloat dx = objc_msgSend_nfloat(@event, selDeltaX); + NFloat dy = objc_msgSend_nfloat(@event, selDeltaY); - CGRect pointRect = objc_msgSend_CGRect(nswindow.Window, selConvertRectToBacking, new CGRect(point, CGPoint.Zero)); + nswindow.VirtualCursorPosition += new CGPoint(dx, dy); - CGRect backing = objc_msgSend_CGRect( - nswindow.Window, - selConvertRectToBacking, - objc_msgSend_CGRect(nswindow.View, selBounds)); - - // FIXME: Coordinate space - Vector2 pos = new Vector2((float)pointRect.origin.x, (float)(backing.size.y - pointRect.origin.y)); + EventQueue.Raise(nswindow, PlatformEventType.MouseMove, new MouseMoveEventArgs(nswindow, (Vector2)nswindow.VirtualCursorPosition)); + } + else + { + // Do normal mouse events + EventQueue.Raise(nswindow, PlatformEventType.MouseMove, new MouseMoveEventArgs(nswindow, (Vector2)pos)); - EventQueue.Raise(nswindow, PlatformEventType.MouseMove, new MouseMoveEventArgs(nswindow, pos)); + objc_msgSend(nsApplication, selSendEvent, @event); + } - objc_msgSend(nsApplication, selSendEvent, @event); + nswindow.LastMousePosition = pos; break; } case NSEventType.ScrollWheel: @@ -1204,6 +1212,12 @@ public void Destroy(WindowHandle handle) { NSWindowHandle nswindow = handle.As(this); + if (nswindow.CursorCaptureMode == CursorCaptureMode.Locked) + { + // Restore the cursor. + CG.CGAssociateMouseAndMouseCursorPosition(true); + } + NSWindowDict.Remove(nswindow.Window); // This also releases the window if the releaseWhenClosed property is true @@ -1932,6 +1946,7 @@ public void SetCursor(WindowHandle handle, CursorHandle? cursor) objc_msgSend(nswindow.Window, selInvalidateCursorRectsForView, nswindow.View); } + /// public CursorCaptureMode GetCursorCaptureMode(WindowHandle handle) { @@ -1985,9 +2000,14 @@ public void SetCursorCaptureMode(WindowHandle handle, CursorCaptureMode mode) // FIXME: We want to move the mouse display to the center of the screen. // objc_msgSend((IntPtr)NSCursorClass, selHide); - CG.CGAssociateMouseAndMouseCursorPosition(false); + // FIXME: With multiple windows we only want to apply this + // if this window is key... - CenterCursor(nswindow); + //CG.CGAssociateMouseAndMouseCursorPosition(false); + + //CenterCursor(nswindow); + + nswindow.VirtualCursorPosition = nswindow.LastMousePosition; nswindow.CursorCaptureMode = CursorCaptureMode.Locked; break; @@ -2007,6 +2027,9 @@ private static void CenterCursor(NSWindowHandle nswindow) CGPoint center = new CGPoint(screenFrame.origin.x + (screenFrame.size.x / 2), screenFrame.origin.y + (screenFrame.size.y / 2)); + // CGWarpMouseCursorPosition uses top-left coordinates. + center = CG.FlipYCoordinate(center); + CG.CGDisplayMoveCursorToPoint(CG.CGMainDisplayID(), center); CG.CGWarpMouseCursorPosition(center); nswindow.LastMousePosition = center; diff --git a/src/OpenTK.Platform.Native/macOS/ObjC.cs b/src/OpenTK.Platform.Native/macOS/ObjC.cs index 78bb60da37..7c946e26cf 100644 --- a/src/OpenTK.Platform.Native/macOS/ObjC.cs +++ b/src/OpenTK.Platform.Native/macOS/ObjC.cs @@ -185,6 +185,9 @@ internal static ObjCClass objc_getClass(ReadOnlySpan name) [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] internal static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, SEL selector, CGRect rect); + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + internal static extern IntPtr objc_msgSend_IntPtr(IntPtr receiver, SEL selector, IntPtr value1, nuint value2, bool value3); + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] internal static extern void objc_msgSend(IntPtr receiver, SEL selector, bool value); diff --git a/src/OpenTK.Platform.Native/macOS/ObjCTypes.cs b/src/OpenTK.Platform.Native/macOS/ObjCTypes.cs index c870aadad5..53e2585085 100644 --- a/src/OpenTK.Platform.Native/macOS/ObjCTypes.cs +++ b/src/OpenTK.Platform.Native/macOS/ObjCTypes.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using OpenTK.Mathematics; namespace OpenTK.Platform.Native.macOS { @@ -82,6 +83,16 @@ public override string ToString() { return !(left == right); } + + public static CGPoint operator +(CGPoint left, CGPoint right) + { + return new CGPoint(left.x + right.x, left.y + right.y); + } + + public static explicit operator Vector2(CGPoint p) + { + return new Vector2((float)p.x, (float)p.y); + } } internal struct CGRect diff --git a/tests/OpenTK.Backends.Tests/CoordinateSpacesView.cs b/tests/OpenTK.Backends.Tests/CoordinateSpacesView.cs index b5cd2d11dc..abbbf24c12 100644 --- a/tests/OpenTK.Backends.Tests/CoordinateSpacesView.cs +++ b/tests/OpenTK.Backends.Tests/CoordinateSpacesView.cs @@ -17,6 +17,11 @@ internal class CoordinateSpacesView : View public override bool IsVisible => Program.WindowComp != null; + readonly static CursorCaptureMode[] CaptureModes = Enum.GetValues(); + readonly static string[] CaptureModeNames = Enum.GetNames(); + + int captureModeIndex = 0; + public override void Initialize() { base.Initialize(); @@ -53,6 +58,15 @@ public override void Paint(double deltaTime) ImGui.Text($"Framebuffer size: {fbw}x{fbh}"); } + + // FIXME: Make some way to get out of the locked cursor mode. + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Cursor capture mode"); ImGui.SameLine(); + ImGui.Combo("##captureMode", ref captureModeIndex, CaptureModeNames, CaptureModeNames.Length); ImGui.SameLine(); + if (ImGui.Button("Apply##captureMode")) + { + Program.WindowComp.SetCursorCaptureMode(Program.Window, CaptureModes[captureModeIndex]); + } } catch { } } diff --git a/tests/OpenTK.Backends.Tests/Program.cs b/tests/OpenTK.Backends.Tests/Program.cs index 51f45575ea..abbae8669d 100644 --- a/tests/OpenTK.Backends.Tests/Program.cs +++ b/tests/OpenTK.Backends.Tests/Program.cs @@ -348,6 +348,8 @@ static bool IsExtensionSupported(string name) OpenGLComp.SetCurrentContext(WindowContext); Render(); + // FIXME: Avoid allocating this list every frame. + List shouldCloseWindows = new List(); foreach (var applicationWindow in ApplicationWindows) { if (applicationWindow.Context == null) @@ -357,11 +359,22 @@ static bool IsExtensionSupported(string name) OpenGLComp.SetCurrentContext(applicationWindow.Context); + bool shouldClose = applicationWindow.Application.Update(dt); + if (shouldClose) + { + shouldCloseWindows.Add(applicationWindow.Window); + } + // FIXME: Send delta time? applicationWindow.Application.Render(); OpenGLComp.SetCurrentContext(WindowContext); } + + foreach (var window in shouldCloseWindows) + { + CloseApplicationWindow(window); + } } } @@ -473,6 +486,31 @@ static ImGuiKey ToImgui(Key key) } } + private static void CloseApplicationWindow(WindowHandle window) + { + int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == window); + ApplicationWindow appWindow = ApplicationWindows[index]; + if (appWindow != null) + { + if (appWindow.Context != null) + { + if (appWindow.Application != null) + { + // FIXME: Make there only be one place where we actually deinit applications. + OpenGLComp.SetCurrentContext(appWindow.Context); + appWindow.Application?.Deinitialize(); + OpenGLComp.SetCurrentContext(WindowContext); + } + + OpenGLComp.DestroyContext(appWindow.Context); + } + + ApplicationWindows.RemoveAt(index); + } + + WindowComp.Destroy(window); + } + private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType type, EventArgs args) { if (args is WindowEventArgs windowEvent) @@ -482,29 +520,7 @@ private static void EventQueue_EventRaised(PalHandle? handle, PlatformEventType if (args is CloseEventArgs close2) { Console.WriteLine($"Closing window: '{WindowComp.GetTitle(close2.Window)}'"); - - // If this is one of our other windows we want to gracefully close it before we delete the window. - int index = ApplicationWindows.FindIndex(appWindow => appWindow.Window == close2.Window); - ApplicationWindow appWindow = ApplicationWindows[index]; - if (appWindow != null) - { - if (appWindow.Context != null) - { - if (appWindow.Application != null) - { - // FIXME: Make there only be one place where we actually deinit applications. - OpenGLComp.SetCurrentContext(appWindow.Context); - appWindow.Application?.Deinitialize(); - OpenGLComp.SetCurrentContext(WindowContext); - } - - OpenGLComp.DestroyContext(appWindow.Context); - } - - ApplicationWindows.RemoveAt(index); - } - - WindowComp.Destroy(close2.Window); + CloseApplicationWindow(close2.Window); return; } else diff --git a/tests/OpenTK.Backends.Tests/TestApps/ColorTriangle.cs b/tests/OpenTK.Backends.Tests/TestApps/ColorTriangle.cs index 6b2e235406..041e493b0d 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/ColorTriangle.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/ColorTriangle.cs @@ -215,9 +215,9 @@ public void HandleEvent(EventArgs args) } } - public void Update(float deltaTime) + public bool Update(float deltaTime) { - + return false; } public void Render() diff --git a/tests/OpenTK.Backends.Tests/TestApps/ColorWheel.cs b/tests/OpenTK.Backends.Tests/TestApps/ColorWheel.cs index c6d373b0c4..f1d612fc7a 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/ColorWheel.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/ColorWheel.cs @@ -27,9 +27,9 @@ public void HandleEvent(EventArgs args) // FIXME: Handle resize? } - public void Update(float deltaTime) + public bool Update(float deltaTime) { - + return false; } public void Render() diff --git a/tests/OpenTK.Backends.Tests/TestApps/FpsCamera.cs b/tests/OpenTK.Backends.Tests/TestApps/FpsCamera.cs new file mode 100644 index 0000000000..81aa3efe85 --- /dev/null +++ b/tests/OpenTK.Backends.Tests/TestApps/FpsCamera.cs @@ -0,0 +1,285 @@ +using System; +using System.Diagnostics; +using OpenTK.Core.Platform; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using OpenTK.Platform.Native.macOS; + +namespace OpenTK.Backends.Tests +{ + [TestApp] + public class FpsCamera : ITestApp + { + public string Name => "Fps Camera"; + + struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + public Vector3 Color; + + public Vertex(Vector3 position, Vector3 normal, Vector3 color) + { + Position = position; + Normal = normal; + Color = color; + } + } + + WindowHandle window; + OpenGLContextHandle context; + + const float D2R = MathF.PI / 180.0f; + + const float RoomWidth = 10; + static readonly Vector3 FloorColor = (0.5f, 0.5f, 0.5f); + static readonly Vertex[] env_data = { + // Floor + new Vertex((-RoomWidth, -2, RoomWidth), (0, 1, 0), FloorColor), + new Vertex(( RoomWidth, -2, RoomWidth), (0, 1, 0), FloorColor), + new Vertex(( RoomWidth, -2, -RoomWidth), (0, 1, 0), FloorColor), + new Vertex((-RoomWidth, -2, -RoomWidth), (0, 1, 0), FloorColor), + }; + static readonly int[] env_idx = { + // Floor + 0, 1, 2, 0, 2, 3 + }; + + int env_vao; + int env_vbo; + int env_ebo; + + // FIXME: Make the version dynamically changeable to support gles... + const string vertex_shader = +@"#version 330 core + +layout(location = 0) in vec3 in_position; +layout(location = 1) in vec3 in_normal; +layout(location = 2) in vec3 in_color; + +out vec3 f_color; + +uniform mat4 mvp; + +void main() { + gl_Position = vec4(in_position, 1.0) * mvp; + f_color = in_color; +} +"; + + // FIXME: Make the version dynamically changeable to support gles... + const string fragment_shader = +@"#version 330 core +in vec3 f_color; + +out vec3 out_color; + +void main() { + out_color = f_color; +} +"; + + int env_program; + int env_program_mvp_location; + + float cameraFov = 90; + float cameraNear = 0.01f; + float cameraFar = 1000; + + Vector3 cameraPosition = (0, 0, 0); + Quaternion cameraRotation = Quaternion.Identity; + float cameraRotationX = 0; + float cameraRotationY = 0; + const float cameraMovementSpeed = 10; + + readonly Vector2 cameraRotationSpeed = (0.3f, 0.3f); + + public unsafe void Initialize(WindowHandle window, OpenGLContextHandle context, bool useGLES) + { + Debug.Assert(useGLES == false, "We don't support gles here yet."); + + this.window = window; + this.context = context; + + env_vao = GL.GenVertexArray(); + GL.BindVertexArray(env_vao); + + env_vbo = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, env_vbo); + GL.BufferData(BufferTarget.ArrayBuffer, env_data, BufferUsage.StaticDraw); + + GL.EnableVertexAttribArray(0); + GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, sizeof(Vertex), 0*sizeof(Vector3)); + GL.EnableVertexAttribArray(1); + GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, sizeof(Vertex), 1*sizeof(Vector3)); + GL.EnableVertexAttribArray(2); + GL.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, sizeof(Vertex), 2*sizeof(Vector3)); + + env_ebo = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ElementArrayBuffer, env_ebo); + GL.BufferData(BufferTarget.ElementArrayBuffer, env_idx, BufferUsage.StaticDraw); + + int vert = GL.CreateShader(ShaderType.VertexShader); + GL.ShaderSource(vert, vertex_shader); + GL.CompileShader(vert); + if (GL.GetShaderi(vert, ShaderParameterName.CompileStatus) == 0) + { + GL.GetShaderInfoLog(vert, out string info); + Debug.Assert(false, $"Vertex shader compilation error: {info}"); + } + + int frag = GL.CreateShader(ShaderType.FragmentShader); + GL.ShaderSource(frag, fragment_shader); + GL.CompileShader(frag); + if (GL.GetShaderi(frag, ShaderParameterName.CompileStatus) == 0) + { + GL.GetShaderInfoLog(frag, out string info); + Debug.Assert(false, $"Fragment shader compilation error: {info}"); + } + + env_program = GL.CreateProgram(); + GL.AttachShader(env_program, vert); + GL.AttachShader(env_program, frag); + GL.LinkProgram(env_program); + if (GL.GetProgrami(env_program, ProgramProperty.LinkStatus) == 0) + { + GL.GetProgramInfoLog(env_program, out string info); + Debug.Assert(false, $"Shader link error: {info}"); + } + GL.DetachShader(env_program, vert); + GL.DetachShader(env_program, frag); + + GL.DeleteShader(vert); + GL.DeleteShader(frag); + + env_program_mvp_location = GL.GetUniformLocation(env_program, "mvp"); + + Program.WindowComp.SetCursorCaptureMode(window, CursorCaptureMode.Locked); + Program.WindowComp.SetCursor(window, null); + } + + public void Deinitialize() + { + GL.DeleteVertexArray(env_vao); + GL.DeleteBuffer(env_vbo); + GL.DeleteBuffer(env_ebo); + + GL.DeleteProgram(env_program); + } + + public void Render() + { + GL.ClearColor(Color4.Darkslategray); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + GL.BindVertexArray(env_vao); + + GL.UseProgram(env_program); + + // FIXME: Framebuffer size... + Program.WindowComp.GetClientSize(window, out int width, out int height); + if (Program.WindowComp is MacOSWindowComponent macOSWindowComp) + { + macOSWindowComp.GetFramebufferSize(window, out width, out height); + } + float aspect = width / (float)height; + + Matrix4 model = Matrix4.Identity; + Matrix4 camera = Matrix4.CreateFromQuaternion(cameraRotation) * Matrix4.CreateTranslation(cameraPosition); + Matrix4 view = camera.Inverted(); + Matrix4 proj = Matrix4.CreatePerspectiveFieldOfView(cameraFov * MathHelper.DegreesToRadians(1), aspect, cameraNear, cameraFar); + + Matrix4 mvp = model * view * proj; + + GL.UniformMatrix4f(env_program_mvp_location, 1, true, mvp); + + GL.DrawElements(PrimitiveType.Triangles, env_idx.Length, DrawElementsType.UnsignedInt, 0); + + Program.OpenGLComp.SwapBuffers(context); + } + + Vector2 delta; + readonly bool[] keyboardState = new bool[256]; + public bool Update(float deltaTime) + { + bool shouldClose = false; + + if (Program.MouseComponent != null) + { + // FIXME: Because we can't check input focus atm we accumulate a + // mouse delta from events instead of doing this. + //Program.MouseComponent.GetMouseState(out MouseState state); + //Vector2 delta = prevPos - state.Position; + //prevPos = state.Position; + + cameraRotationY += delta.X * cameraRotationSpeed.X * deltaTime; + cameraRotationX += delta.Y * cameraRotationSpeed.Y * deltaTime; + cameraRotationX = MathHelper.Clamp(cameraRotationX, -80 * D2R, 80 * D2R); + + cameraRotation = Quaternion.FromAxisAngle(Vector3.UnitY, cameraRotationY) * + Quaternion.FromAxisAngle(Vector3.UnitX, cameraRotationX); + + delta = (0, 0); + } + + if (Program.KeyboardComponent != null) + { + // FIXME: Check input focus... + Program.KeyboardComponent.GetKeyboardState(keyboardState); + if (keyboardState[(int)Scancode.A]) + { + cameraPosition += (cameraRotation * -Vector3.UnitX) * cameraMovementSpeed * deltaTime; + } + + if (keyboardState[(int)Scancode.D]) + { + cameraPosition += (cameraRotation * Vector3.UnitX) * cameraMovementSpeed * deltaTime; + } + + if (keyboardState[(int)Scancode.W]) + { + cameraPosition += (cameraRotation * -Vector3.UnitZ) * cameraMovementSpeed * deltaTime; + } + + if (keyboardState[(int)Scancode.S]) + { + cameraPosition += (cameraRotation * Vector3.UnitZ) * cameraMovementSpeed * deltaTime; + } + + if (keyboardState[(int)Scancode.Escape]) + { + // FIXME: If we destroy a window while a key is pressed we + // never get the KeyUp event this leads to escape + // to stay pressed after closing the window. + shouldClose = true; + } + } + + return shouldClose; + } + + Vector2 prevPos; + public void HandleEvent(EventArgs args) + { + if (args is MouseMoveEventArgs mouseMove) + { + delta += prevPos - mouseMove.Position; + prevPos = mouseMove.Position; + + Console.WriteLine($"Mouse move: {mouseMove.Position}"); + } + else if (args is FocusEventArgs focus) + { + if (focus.GotFocus) + { + if (Program.MouseComponent != null) + { + Program.MouseComponent.GetPosition(out int x, out int y); + prevPos = (x, y); + } + } + } + } + } +} + diff --git a/tests/OpenTK.Backends.Tests/TestApps/ITestApp.cs b/tests/OpenTK.Backends.Tests/TestApps/ITestApp.cs index dfc5838ccc..a680b2eb2b 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/ITestApp.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/ITestApp.cs @@ -48,7 +48,8 @@ public interface ITestApp /// Update the application logic. /// /// The time since the last call to Update. - void Update(float deltaTime); + /// If the application should quit. + bool Update(float deltaTime); /// /// Render the application view. diff --git a/tests/OpenTK.Backends.Tests/TestApps/ModelView.cs b/tests/OpenTK.Backends.Tests/TestApps/ModelView.cs index a804c9074d..6721c7c423 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/ModelView.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/ModelView.cs @@ -28,8 +28,9 @@ public void HandleEvent(EventArgs args) { } - public void Update(float deltaTime) + public bool Update(float deltaTime) { + return false; } public void Render() diff --git a/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs index 0ce442797d..94fad72b76 100644 --- a/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs +++ b/tests/OpenTK.Backends.Tests/TestApps/VsyncTest.cs @@ -56,8 +56,9 @@ public void HandleEvent(EventArgs args) } } - public void Update(float deltaTime) + public bool Update(float deltaTime) { + return false; } static readonly Color4 Red = new Color4(1.0f, 0.0f, 0.0f, 1.0f); diff --git a/tests/OpenTK.Backends.Tests/WindowComponentView.cs b/tests/OpenTK.Backends.Tests/WindowComponentView.cs index aba15b3f8b..ea82c58d8d 100644 --- a/tests/OpenTK.Backends.Tests/WindowComponentView.cs +++ b/tests/OpenTK.Backends.Tests/WindowComponentView.cs @@ -71,14 +71,14 @@ private HitType HitTest(WindowHandle window, Vector2 point) Vector2i windowPosition; Vector2i clientPosition; - WindowMode[] WindowModes = Enum.GetValues(); - string[] WindowModeNames = Enum.GetNames(); + readonly static WindowMode[] WindowModes = Enum.GetValues(); + readonly static string[] WindowModeNames = Enum.GetNames(); - WindowBorderStyle[] WindowBorderStyles = Enum.GetValues(); - string[] WindowBorderStyleNames = Enum.GetNames(); + readonly static WindowBorderStyle[] WindowBorderStyles = Enum.GetValues(); + readonly static string[] WindowBorderStyleNames = Enum.GetNames(); - CursorCaptureMode[] CaptureModes = Enum.GetValues(); - string[] CaptureModeNames = Enum.GetNames(); + readonly static CursorCaptureMode[] CaptureModes = Enum.GetValues(); + readonly static string[] CaptureModeNames = Enum.GetNames(); public override void Paint(double deltaTime) { From 7b0bd92a7595341030f5bcfbda1a1747efc48f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Fri, 17 May 2024 13:59:06 +0200 Subject: [PATCH 11/62] Small fixes --- src/OpenTK.Platform.Native/Windows/ShellComponent.cs | 2 ++ tests/OpenTK.Backends.Tests/ShellComponentView.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/OpenTK.Platform.Native/Windows/ShellComponent.cs b/src/OpenTK.Platform.Native/Windows/ShellComponent.cs index e838b8b96e..fc66bbd162 100644 --- a/src/OpenTK.Platform.Native/Windows/ShellComponent.cs +++ b/src/OpenTK.Platform.Native/Windows/ShellComponent.cs @@ -135,6 +135,8 @@ public ThemeInfo GetPreferredTheme() // FIXME: Move this to the window component itself? // FIXME: DWMWA_BORDER_COLOR? DWMWA_WINDOW_CORNER_PREFERENCE? + // FIXME: It seems like the window titlebar doesn't get changed + // immediately, you need to minimize and restore the window for this to work... /// /// Sets the DWMWA_USE_IMMERSIVE_DARK_MODE flag on the window causing the titlebar be rendered in dark mode colors. /// diff --git a/tests/OpenTK.Backends.Tests/ShellComponentView.cs b/tests/OpenTK.Backends.Tests/ShellComponentView.cs index 63c38ff220..e75a4c7eba 100644 --- a/tests/OpenTK.Backends.Tests/ShellComponentView.cs +++ b/tests/OpenTK.Backends.Tests/ShellComponentView.cs @@ -1,5 +1,6 @@ using ImGuiNET; using OpenTK.Core.Platform; +using OpenTK.Mathematics; using System; using System.Collections.Generic; using System.Linq; @@ -16,6 +17,9 @@ internal class ShellComponentView : View static readonly AppTheme[] Themes = Enum.GetValues(); + System.Numerics.Vector3 textColor = new System.Numerics.Vector3(1, 1, 1); + System.Numerics.Vector3 captionColor = new System.Numerics.Vector3(1, 1, 1); + public override void Initialize() { base.Initialize(); @@ -114,21 +118,17 @@ public override void Paint(double deltaTime) { if (ImGui.Checkbox("Use Immeresive Dark Mode", ref useImmersiveDarkMode)) { - // FIXME: It seems like the window titlebar doesn't get changed - // immediately, you need to minimize and restore the window for this to work... winShell.SetImmersiveDarkMode(Program.Window, useImmersiveDarkMode); } - System.Numerics.Vector3 textColor = new System.Numerics.Vector3(1, 1, 1); if (ImGui.ColorEdit3("Caption Text Color", ref textColor)) { - //winShell.SetCaptionTextColor(Program.Window, new Color3(textColor.X, textColor.Y, textColor.Z)); + winShell.SetCaptionTextColor(Program.Window, new Color3(textColor.X, textColor.Y, textColor.Z)); } - System.Numerics.Vector3 captionColor = new System.Numerics.Vector3(1, 1, 1); if (ImGui.ColorEdit3("Caption Color", ref captionColor)) { - //winShell.SetCaptionColor(Program.Window, new Color3(captionColor.X, captionColor.Y, captionColor.Z)); + winShell.SetCaptionColor(Program.Window, new Color3(captionColor.X, captionColor.Y, captionColor.Z)); } ImGui.EndTabItem(); From ef26c5a230166e1fd72933a575bb94ca8f74bd44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4ger?= Date: Fri, 17 May 2024 17:00:14 +0200 Subject: [PATCH 12/62] Added basic IDialogComponent and win32 implementation. --- src/OpenTK.Core/OpenTK.Core.csproj | 2 +- .../Platform/Enums/OpenDialogOptions.cs | 25 + .../Platform/Enums/PalComponents.cs | 5 + .../Platform/Enums/SaveDialogOptions.cs | 20 + .../Platform/Interfaces/IDialogComponent.cs | 26 + .../PlatformComponents.cs | 32 +- src/OpenTK.Platform.Native/Toolkit.cs | 7 + .../Windows/DialogComponent.cs | 497 ++++++++++++++++ src/OpenTK.Platform.Native/Windows/Enums.cs | 543 +++++++++++++++++- src/OpenTK.Platform.Native/Windows/Win32.cs | 45 ++ tests/OpenTK.Backends.Tests/OverviewView.cs | 59 +- tests/OpenTK.Backends.Tests/Program.cs | 11 +- 12 files changed, 1241 insertions(+), 31 deletions(-) create mode 100644 src/OpenTK.Core/Platform/Enums/OpenDialogOptions.cs create mode 100644 src/OpenTK.Core/Platform/Enums/SaveDialogOptions.cs create mode 100644 src/OpenTK.Core/Platform/Interfaces/IDialogComponent.cs create mode 100644 src/OpenTK.Platform.Native/Windows/DialogComponent.cs diff --git a/src/OpenTK.Core/OpenTK.Core.csproj b/src/OpenTK.Core/OpenTK.Core.csproj index 841f4faba5..589bed91ce 100644 --- a/src/OpenTK.Core/OpenTK.Core.csproj +++ b/src/OpenTK.Core/OpenTK.Core.csproj @@ -18,6 +18,6 @@ - + diff --git a/src/OpenTK.Core/Platform/Enums/OpenDialogOptions.cs b/src/OpenTK.Core/Platform/Enums/OpenDialogOptions.cs new file mode 100644 index 0000000000..863fdbf3b8 --- /dev/null +++ b/src/OpenTK.Core/Platform/Enums/OpenDialogOptions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTK.Core.Platform +{ + /// + /// Options for open dialogs. + /// + [Flags] + public enum OpenDialogOptions + { + /// + /// Allows multiple items to be selected. + /// + AllowMultiSelect = 1 << 0, + + /// + /// Select directories instead of files. + /// + SelectDirectory = 1 << 1, + } +} diff --git a/src/OpenTK.Core/Platform/Enums/PalComponents.cs b/src/OpenTK.Core/Platform/Enums/PalComponents.cs index 8d527d8d0e..d628fc59f4 100644 --- a/src/OpenTK.Core/Platform/Enums/PalComponents.cs +++ b/src/OpenTK.Core/Platform/Enums/PalComponents.cs @@ -97,5 +97,10 @@ public enum PalComponents /// Abstraction layer provides the joystick component. /// Joystick = 1 << 12, + + /// + /// Abstraction layer provides the dialog component. + /// + Dialog = 1 << 13, } } diff --git a/src/OpenTK.Core/Platform/Enums/SaveDialogOptions.cs b/src/OpenTK.Core/Platform/Enums/SaveDialogOptions.cs new file mode 100644 index 0000000000..e242c2b7b2 --- /dev/null +++ b/src/OpenTK.Core/Platform/Enums/SaveDialogOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTK.Core.Platform +{ + /// + /// Options for save dialogs. + /// + [Flags] + public enum SaveDialogOptions + { + /// + /// Select directories instead of files. + /// + SelectDirectory = 1 << 2, + } +} diff --git a/src/OpenTK.Core/Platform/Interfaces/IDialogComponent.cs b/src/OpenTK.Core/Platform/Interfaces/IDialogComponent.cs new file mode 100644 index 0000000000..77374d6ca8 --- /dev/null +++ b/src/OpenTK.Core/Platform/Interfaces/IDialogComponent.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#nullable enable + +namespace OpenTK.Core.Platform +{ + public interface IDialogComponent : IPalComponent + { + /// + /// If the value of this property is true and will work. + /// Otherwise these flags will be ignored. + /// + public bool CanTargetFolders { get; } + + // FIXME: Formalize the format for allowedExtensions so that we avoid being platform dependent. + + public unsafe List? ShowOpenDialog(WindowHandle parent, string title, string directory, (string Name, string Ext)[]? allowedExtensions, OpenDialogOptions options); + + // FIXME: Does a save dialog return multiple items? + public unsafe string? ShowSaveDialog(WindowHandle parent, string title, string directory, (string Name, string Ext)[]? allowedExtensions, SaveDialogOptions options); + } +} diff --git a/src/OpenTK.Platform.Native/PlatformComponents.cs b/src/OpenTK.Platform.Native/PlatformComponents.cs index 5ecf4b0bb3..c61d8c179d 100644 --- a/src/OpenTK.Platform.Native/PlatformComponents.cs +++ b/src/OpenTK.Platform.Native/PlatformComponents.cs @@ -48,6 +48,7 @@ public static class PlatformComponents [PalComponents.WindowIcon] = () => new SDL.SDLIconComponent(), [PalComponents.Clipboard] = () => new SDL.SDLClipboardComponent(), [PalComponents.Joystick] = () => new SDL.SDLJoystickComponent(), + //[PalComponents.Dialog] = () => new SDL.SDLDialogComponent(), }; private static Dictionary win32Components = @@ -63,6 +64,7 @@ public static class PlatformComponents [PalComponents.WindowIcon] = () => new Windows.IconComponent(), [PalComponents.Clipboard] = () => new Windows.ClipboardComponent(), [PalComponents.Joystick] = () => new Windows.JoystickComponent(), + [PalComponents.Dialog] = () => new Windows.DialogComponent(), }; private static Dictionary x11Components = @@ -78,6 +80,7 @@ public static class PlatformComponents [PalComponents.WindowIcon] = () => new X11.X11IconComponent(), [PalComponents.Clipboard] = () => new X11.X11ClipboardComponent(), //[PalComponents.Joystick] = () => new X11.X11JoystickComponent(), + //[PalComponents.Dialog] = () => new X11.X11DialogComponent(), }; private static Dictionary macosComponents = @@ -93,6 +96,7 @@ public static class PlatformComponents [PalComponents.WindowIcon] = () => new macOS.MacOSIconComponent(), //[PalComponents.Clipboard] = () => new macOS.MacOSClipboardComponent(), //[PalComponents.Joystick] = () => new macOS.MacOSJoystickComponent(), + //[PalComponents.Dialog] = () => new macOS.MacOSDialogComponent(), }; /// @@ -182,13 +186,13 @@ public static Backend GetBackend() } } - /// + /// public static IWindowComponent CreateWindowComponent() { return GetPlatformComponent(PalComponents.Window); } - /// + /// public static IOpenGLComponent CreateOpenGLComponent() { // FIXME: Should we do this here? @@ -203,58 +207,64 @@ public static IOpenGLComponent CreateOpenGLComponent() } } - /// + /// public static IDisplayComponent CreateDisplayComponent() { return GetPlatformComponent(PalComponents.Display); } - /// + /// public static IShellComponent CreateShellComponent() { return GetPlatformComponent(PalComponents.Shell); } - /// + /// public static IMouseComponent CreateMouseComponent() { return GetPlatformComponent(PalComponents.MiceInput); } - /// + /// public static IKeyboardComponent CreateKeyboardComponent() { return GetPlatformComponent(PalComponents.KeyboardInput); } - /// + /// public static ICursorComponent CreateCursorComponent() { return GetPlatformComponent(PalComponents.MouseCursor); } - /// + /// public static IIconComponent CreateIconComponent() { return GetPlatformComponent(PalComponents.WindowIcon); } - /// + /// public static IClipboardComponent CreateClipboardComponent() { return GetPlatformComponent(PalComponents.Clipboard); } - /// + /// public static ISurfaceComponent CreateSurfaceComponent() { return GetPlatformComponent(PalComponents.Surface); } - /// + /// public static IJoystickComponent CreateJoystickComponent() { return GetPlatformComponent(PalComponents.Joystick); } + + /// + public static IDialogComponent CreateDialogComponent() + { + return GetPlatformComponent(PalComponents.Dialog); + } } } diff --git a/src/OpenTK.Platform.Native/Toolkit.cs b/src/OpenTK.Platform.Native/Toolkit.cs index 3feebcbe52..a3af374d42 100644 --- a/src/OpenTK.Platform.Native/Toolkit.cs +++ b/src/OpenTK.Platform.Native/Toolkit.cs @@ -30,6 +30,7 @@ public static class Toolkit private static IWindowComponent? _windowComponent; private static IShellComponent? _shellComponent; private static IJoystickComponent? _joystickComponent; + private static IDialogComponent? _dialogComponent; public static IWindowComponent Window => _windowComponent; @@ -53,6 +54,8 @@ public static class Toolkit public static IJoystickComponent Joystick => _joystickComponent; + public static IDialogComponent Dialog => _dialogComponent; + public static void Init(ToolkitOptions options) { // FIXME: Figure out options... @@ -68,6 +71,7 @@ public static void Init(ToolkitOptions options) try { _iconComponent = PlatformComponents.CreateIconComponent(); } catch (NotSupportedException) { } try { _clipboardComponent = PlatformComponents.CreateClipboardComponent(); } catch (NotSupportedException) { } try { _joystickComponent = PlatformComponents.CreateJoystickComponent(); } catch (NotSupportedException) { } + try { _dialogComponent = PlatformComponents.CreateDialogComponent(); } catch (NotSupportedException) { } if (_windowComponent != null) _windowComponent.Logger = options.Logger; @@ -91,6 +95,8 @@ public static void Init(ToolkitOptions options) _clipboardComponent.Logger = options.Logger; if (_joystickComponent != null) _joystickComponent.Logger = options.Logger; + if (_dialogComponent != null) + _dialogComponent.Logger = options.Logger; // FIXME: Change initialize to take toolkit options // This will also allow us to potentially remove the need @@ -108,6 +114,7 @@ public static void Init(ToolkitOptions options) _iconComponent?.Initialize(PalComponents.WindowIcon); _clipboardComponent?.Initialize(PalComponents.Clipboard); _joystickComponent?.Initialize(PalComponents.Joystick); + _dialogComponent?.Initialize(PalComponents.Dialog); } } } diff --git a/src/OpenTK.Platform.Native/Windows/DialogComponent.cs b/src/OpenTK.Platform.Native/Windows/DialogComponent.cs new file mode 100644 index 0000000000..adb35e2ffe --- /dev/null +++ b/src/OpenTK.Platform.Native/Windows/DialogComponent.cs @@ -0,0 +1,497 @@ +using Microsoft.Win32.SafeHandles; +using OpenTK.Core.Platform; +using OpenTK.Core.Utility; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using static OpenTK.Platform.Native.Windows.XInput; + +namespace OpenTK.Platform.Native.Windows +{ + internal class DialogHandle + { + + }; + + internal struct Button + { + public string Text; + } + + internal struct DLGTEMPLATEEX + { + public ushort dlgVer; + public ushort signature; + public uint helpID; + public WindowStylesEx exStyle; + public DialogStyles style; + public ushort cDlgItems; + public short x; + public short y; + public short cx; + public short cy; + /* + sz_Or_Ord menu; + sz_Or_Ord windowClass; + char title[titleLen]; + ushort pointsize; + ushort weight; + byte italic; + byte charset; + char typeface[stringLen]; + */ + } + + internal struct DLGITEMTEMPLATEEX + { + public uint helpID; + public WindowStylesEx exStyle; + // FIXME: + public uint style; + public short x; + public short y; + public short cx; + public short cy; + public uint id; + /* + sz_Or_Ord windowClass; + sz_Or_Ord title; + WORD extraCount; + */ + } + + // Make struct? + internal unsafe struct DialogData + { + public const nuint BLOCK_SIZE = 4096; + + public enum ControlType : ushort + { + Button = 0x0080, + Edit = 0x0081, + Static = 0x0082, + Listbox = 0x0083, + Scrollbar = 0x0084, + Combobox = 0x0085, + } + + public byte* Data; + public nuint Size; + public nuint Used; + + public static DialogData Alloc() + { + DialogData data; + data.Size = 4096; + data.Data = (byte*)NativeMemory.AlignedAlloc(data.Size, sizeof(int)); + data.Used = 0; + return data; + } + + public void EnsureSize(nuint size) + { + if (Used + size > Size) + { + Data = (byte*)NativeMemory.AlignedRealloc(Data, Size + BLOCK_SIZE, sizeof(int)); + Size += BLOCK_SIZE; + } + } + + public void AlignData(nuint alignment) + { + nuint padding = Used % alignment; + EnsureSize(Used + padding); + Used += padding; + } + + public void AddDialogData(T data) where T : unmanaged + { + EnsureSize(Size + (uint)sizeof(T)); + NativeMemory.Copy(&data, &Data[Used], (uint)sizeof(T)); + Used += (uint)sizeof(T); + } + + public void AddDialogString(string str) + { + uint dataSize = (uint)(str.Length * sizeof(char)); + EnsureSize(Size + dataSize); + fixed (char* c = str) + { + NativeMemory.Copy(c, &Data[Used], (uint)dataSize); + } + Used += dataSize; + } + + // FIXME: Strong type for type + public void AddDialogControl(ControlType type, uint style, WindowStylesEx exStyle, uint id, string caption) + { + DLGITEMTEMPLATEEX itemTemplate; + itemTemplate.helpID = 0; + itemTemplate.exStyle = exStyle; + itemTemplate.style = (uint)style; + // FIXME: position! + itemTemplate.x = 0; + itemTemplate.y = 0; + itemTemplate.cx = 10; + itemTemplate.cy = 10; + itemTemplate.id = id; + + AlignData(sizeof(int)); + + AddDialogData(itemTemplate); + + // windowClass + AddDialogData(0xFFFF); + AddDialogData(type); + + AddDialogString(caption); + + // extraData + AddDialogData(0); + + ((DLGTEMPLATEEX*)Data)->cDlgItems++; + } + + public void Init(int w, int h, string caption) + { + DLGTEMPLATEEX template; + template.dlgVer = 1; + template.signature = 0xFFFF; + template.helpID = 0; + template.exStyle = 0; + template.style = DialogStyles.Caption | DialogStyles.Center | DialogStyles.ShellFont; + template.cDlgItems = 0; + template.x = 0; + template.y = 0; + // FIXME: These are in DLU units not in pixels? + template.cx = (short)w; + template.cy = (short)h; + + AddDialogData(template); + + // No menu + AddDialogData(0); + + // No custom class + AddDialogData(0); + + AddDialogString(caption); + + // FIXME: Font stuff! + // pointsize + AddDialogData(12); + // weight + AddDialogData(400); + // italic + AddDialogData(0); + // charset + AddDialogData(0); + // FIXME: Font name! + AddDialogString("Arial"); + + AddDialogControl(ControlType.Button, (uint)WindowStyles.Visible | (uint)WindowStyles.Child | (uint)WindowStyles.TabStop | (uint)ButtonStyles.BS_DEFPUSHBUTTON | (uint)WindowStyles.Group, 0, 1, "Hello!"); + } + } + + public class DialogComponent : IDialogComponent + { + /// + public string Name => nameof(DialogComponent); + + /// + public PalComponents Provides => PalComponents.Dialog; + + /// + public ILogger? Logger { get; set; } + + /// + public void Initialize(PalComponents which) + { + if (which != PalComponents.Dialog) + { + throw new Exception("DialogComponent can only initialize the Dialog component."); + } + } + + /// + public bool CanTargetFolders => false; + + static IntPtr /* INT_PTR */ MessageBoxDialogProc(IntPtr /* HWND */ hDlg, WM iMessage, UIntPtr /* WPARAM */ wParam, IntPtr /* LPARAM */ lParam) + { + Console.WriteLine($"Message: {iMessage}, wParam: {wParam}, lParam: {lParam}"); + + switch (iMessage) + { + case WM.INITDIALOG: + return 1; + default: + break; + } + + return 0; + } + + internal unsafe int ShowMessageBox(WindowHandle parent, string text, IconHandle icon, ReadOnlySpan