commit fc0b04a4bc169f1327f66e8581d9f65d02df0b7a Author: NepuShiro Date: Mon May 18 01:12:52 2026 -0500 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.SDL3 TestingSuite/.idea/.gitignore b/.idea/.idea.SDL3 TestingSuite/.idea/.gitignore new file mode 100644 index 0000000..ac7df40 --- /dev/null +++ b/.idea/.idea.SDL3 TestingSuite/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/.idea.SDL3 TestingSuite.iml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.SDL3 TestingSuite/.idea/encodings.xml b/.idea/.idea.SDL3 TestingSuite/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.SDL3 TestingSuite/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.SDL3 TestingSuite/.idea/indexLayout.xml b/.idea/.idea.SDL3 TestingSuite/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.SDL3 TestingSuite/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.SDL3 TestingSuite/.idea/vcs.xml b/.idea/.idea.SDL3 TestingSuite/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/.idea.SDL3 TestingSuite/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..00692fc --- /dev/null +++ b/Program.cs @@ -0,0 +1,189 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using Hexa.NET.ImGui; +using Hexa.NET.ImPlot; +using Hexa.NET.ImPlot3D; +using SDL3_TestingSuite.SDL3; +using SDL3; + +namespace SDL3_TestingSuite; + +public class Program +{ + private static bool _demoWindowVisible = true; + private static bool _fontStuff; + private static readonly Dictionary?> GlyphsByName = new Dictionary?>(); + private static bool _initialized; + private static ImFontPtr _font; + private static float size = 1f; + + public static void Main() + { + const SDL.WindowFlags flags = SDL.WindowFlags.Resizable | SDL.WindowFlags.HighPixelDensity | SDL.WindowFlags.Transparent; + SDL3Window window = new SDL3Window("SDL3 Testing Suite", 100, 100, 1280, 720, flags); + window.ClearColor.W = 0f; + window.RenderCallback = () => + { + ImGuiViewportPtr viewport = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(viewport.WorkPos); + ImGui.SetNextWindowSize(viewport.WorkSize); + + ImGui.Begin("MainWindow", ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.MenuBar | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoBringToFrontOnFocus); + + if (ImGui.BeginMenuBar()) + { + if (ImGui.BeginMenu("Stuff")) + { + if (ImGui.MenuItem("Quit")) + { + window.ShouldClose = true; + } + + ImGui.EndMenu(); + } + ImGui.Spacing(); + ImGui.MenuItem("Demo Window", "", ref _demoWindowVisible); + ImGui.Spacing(); + ImGui.MenuItem("Font Stuff", "", ref _fontStuff); + + ImGui.EndMenuBar(); + } + + ImGui.End(); + + if (_fontStuff) + { + ImGui.SetNextWindowSize(new Vector2(250, 500), ImGuiCond.FirstUseEver); + if (ImGui.Begin("FontStuff", ref _fontStuff)) + { + ImGui.ShowFontSelector("Font"); + + bool change = false; + if (ImGui.GetFont() != _font) + { + _font = ImGui.GetFont(); + change = true; + } + + string fontName = _font.GetDebugNameS(); + fontName = string.IsNullOrEmpty(fontName) ? _font.FontId.ToString() : fontName; + if (!_initialized || change) + { + if (!GlyphsByName.TryGetValue(fontName, out List glyphs)) + { + glyphs = new List(); + + ImFontPtr font = _font; + + for (uint codepoint = 0; codepoint <= 0x10FFFF; codepoint++) + { + if (codepoint >= 0xD800 && codepoint <= 0xDFFF) + { + continue; + } + + if (!font.IsGlyphInFont(codepoint)) continue; + + glyphs.Add(codepoint); + } + + GlyphsByName[fontName] = glyphs; + Logger.Log($"Initialized font {fontName} with {glyphs.Count} glyphs"); + } + + _initialized = true; + } + + ImGui.PushFont(null, 24); + + if (GlyphsByName.TryGetValue(fontName, out List glyphs2)) + { + float cursorXStart = ImGui.GetCursorPosX(); + float maxWidth = ImGui.GetContentRegionAvail().X; + + foreach (uint codepoint in glyphs2) + { + string text = char.ConvertFromUtf32((int)codepoint); + float glyphWidth = ImGui.CalcTextSize(text).X; + + float cursorX = ImGui.GetCursorPosX(); + if (cursorX > cursorXStart && (cursorX + glyphWidth) > (cursorXStart + maxWidth)) + { + ImGui.NewLine(); + } + + ImGui.TextUnformatted(text); + + ImGui.SameLine(); + } + } + + ImGui.PopFont(); + + ImGui.End(); + } + } + + float time = (float)ImGui.GetTime() * 5; + if (ImPlot.BeginPlot("Moving Rainbow Sine Wave")) + { + int count = 200; + + float[] xs = new float[2]; + float[] ys = new float[2]; + + for (int i = 0; i < count - 1; i++) + { + float x0 = i * 0.1f; + float x1 = (i + 1) * 0.1f; + + float y0 = MathF.Sin(x0 * size + time); + float y1 = MathF.Sin(x1 * size + time); + + xs[0] = x0; + xs[1] = x1; + + ys[0] = y0; + ys[1] = y1; + + float t = i / (float)count; + + float r = 0.5f + 1f * MathF.Sin(6.2831f * (t)); + float g = 0.5f + 1f * MathF.Sin(6.2831f * (t + 0.33f)); + float b = 0.5f + 1f * MathF.Sin(6.2831f * (t + 0.66f)); + + + ImPlot.PushStyleColor(ImPlotCol.Line, new Vector4(r, g, b, 1f)); + ImPlot.PlotLine("##seg", ref xs[0], ref ys[0], 2); + ImPlot.PopStyleColor(); + + } + + ImPlot.EndPlot(); + } + + ImGui.SliderFloat("Sine", ref size, 1f, 120f); + + if (_demoWindowVisible) + { + ImGui.ShowDemoWindow(ref _demoWindowVisible); + ImPlot.ShowDemoWindow(ref _demoWindowVisible); + // ImPlot3D.ShowDemoWindow(ref _demoWindowVisible); + } + }; + + window.Run(); + } + + public static class Logger + { + public static void Log(string? str, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0) + { + Console.WriteLine($"{caller}({line}): {str}"); + } + public static void Log(object? str, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0) + { + Log(str?.ToString(), caller, line); + } + } +} \ No newline at end of file diff --git a/SDL3 TestingSuite.csproj b/SDL3 TestingSuite.csproj new file mode 100644 index 0000000..6821cfd --- /dev/null +++ b/SDL3 TestingSuite.csproj @@ -0,0 +1,24 @@ + + + + Exe + net10.0 + SDL3_TestingSuite + enable + enable + true + true + + + + + + + + + + + + + + diff --git a/SDL3 TestingSuite.sln.DotSettings.user b/SDL3 TestingSuite.sln.DotSettings.user new file mode 100644 index 0000000..5335a82 --- /dev/null +++ b/SDL3 TestingSuite.sln.DotSettings.user @@ -0,0 +1,2 @@ + + ForceIncluded \ No newline at end of file diff --git a/SDL3 TestingSuite.slnx b/SDL3 TestingSuite.slnx new file mode 100644 index 0000000..647e9ac --- /dev/null +++ b/SDL3 TestingSuite.slnx @@ -0,0 +1,3 @@ + + + diff --git a/SDL3/FontFind.cs b/SDL3/FontFind.cs new file mode 100644 index 0000000..d3c862c --- /dev/null +++ b/SDL3/FontFind.cs @@ -0,0 +1,162 @@ +using Hexa.NET.ImGui; + +namespace SDL3_TestingSuite.SDL3; + +public struct FontMetrics +{ + public ushort UnitsPerEm; + + public short TypoAscender; + public short TypoDescender; + public short TypoLineGap; + + public short WinAscent; + public short WinDescent; + + public int TypoLineHeight; + public int WinLineHeight; +} + +public static class FontFind +{ + extension(ImGuiIOPtr io) + { + public unsafe ImFontPtr AddFont(string fontPath) + { + try + { + if (fontPath == null || io.Handle == null) return null; + FileInfo info = new FileInfo(fontPath); + if (!info.Exists || info.Length <= 0) return null; + + FontMetrics metrics = FontFind.ReadFontMetrics(fontPath); + float size = FontFind.GetRecommendedPixelSize(metrics); + + ImFontConfigPtr config = ImGui.ImFontConfig(); + config.FontLoaderFlags |= (uint)ImGuiFreeTypeLoaderFlags.LoadColor; + ImFontPtr font = io.Fonts.AddFontFromFileTTF(fontPath, size, config); + Program.Logger.Log("Added font: " + Path.GetFileName(fontPath)); + return font; + } + catch (Exception e) + { + Console.WriteLine(e); + } + return null; + } + public unsafe bool RemoveFont(string? fontName) + { + if (fontName == null || io.Handle == null) return false; + + bool flag = false; + ImVector fonts = io.Fonts.Fonts; + List toRemove = new List(); + + for (int i = 0; i < fonts.Size; i++) + { + ImFontPtr font = fonts[i]; + if (font.GetDebugNameS() == fontName) + { + toRemove.Add(font); + flag = true; + } + } + + foreach (ImFontPtr font in toRemove) + { + io.Fonts.RemoveFont(font); + Program.Logger.Log("Removed font: " + fontName); + } + + return flag; + } + } + + public static FontMetrics ReadFontMetrics(string fontPath) + { + byte[] data = File.ReadAllBytes(fontPath); + + ushort numTables = ReadU16Be(4); + const int tableDir = 12; + + int headOffset = 0; + int os2Offset = 0; + + for (int i = 0; i < numTables; i++) + { + int entry = tableDir + i * 16; + string tag = ReadTag(entry); + + uint offset = ReadU32Be(entry + 8); + + if (tag == "head") + headOffset = (int)offset; + + if (tag == "OS/2") + os2Offset = (int)offset; + } + + FontMetrics metrics = new FontMetrics(); + + metrics.UnitsPerEm = ReadU16Be(headOffset + 18); + + if (os2Offset != 0) + { + metrics.TypoAscender = ReadS16Be(os2Offset + 68); + metrics.TypoDescender = ReadS16Be(os2Offset + 70); + metrics.TypoLineGap = ReadS16Be(os2Offset + 72); + + metrics.WinAscent = ReadS16Be(os2Offset + 74); + metrics.WinDescent = ReadS16Be(os2Offset + 76); + } + + metrics.TypoLineHeight = + metrics.TypoAscender - metrics.TypoDescender + metrics.TypoLineGap; + + metrics.WinLineHeight = + metrics.WinAscent + metrics.WinDescent; + + return metrics; + + ushort ReadU16Be(int o) + { + return (ushort)((data[o] << 8) | data[o + 1]); + } + + short ReadS16Be(int o) + { + return (short)ReadU16Be(o); + } + + uint ReadU32Be(int o) + { + return (uint)((data[o] << 24) | (data[o + 1] << 16) | (data[o + 2] << 8) | data[o + 3]); + } + + string ReadTag(int o) + { + char c1 = (char)data[o]; + char c2 = (char)data[o + 1]; + char c3 = (char)data[o + 2]; + char c4 = (char)data[o + 3]; + return new string(new char[] { c1, c2, c3, c4 }); + } + } + + public static int GetRecommendedPixelSize(FontMetrics metrics) + { + if (metrics.UnitsPerEm == 0 || metrics.TypoLineHeight == 0) + return 16; + + const float targetLineHeightPx = 18.0f; + + float scale = targetLineHeightPx * metrics.UnitsPerEm / metrics.TypoLineHeight; + + int size = (int)MathF.Round(scale); + + if (size < 10) size = 10; + if (size > 72) size = 72; + + return size; + } +} \ No newline at end of file diff --git a/SDL3/ImGuiSDL3Platform.cs b/SDL3/ImGuiSDL3Platform.cs new file mode 100644 index 0000000..5123369 --- /dev/null +++ b/SDL3/ImGuiSDL3Platform.cs @@ -0,0 +1,1152 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Hexa.NET.ImGui; +using SDL3; + +namespace SDL3_TestingSuite.SDL3; + +/// +/// Implementation of SDL3 platform backend for ImGui. +/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdl3.h +/// +public unsafe static class ImGuiSDL3Platform +{ + public class PlatformData + { + public nint Window; + public uint WindowID; + public nint Renderer; + public ulong Time; + public nint ClipboardTextData; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 48)] + public byte[] BackendPlatformName = new byte[48]; + public bool UseVulkan; + public bool WantUpdateMonitors; + + // IME Handling + public nint ImeWindow; + public ImGuiPlatformImeData ImeData; + public bool ImeDirty; + + // Mouse Handling + public uint MouseWindowID; + public int MouseButtonsDown; + public readonly nint[] MouseCursors = new nint[(int)ImGuiMouseCursor.Count]; + public nint MouseLastCursor; + public int MouseLastLeaveFrame; + public bool MouseCanUseGlobalState; + public bool MouseCanReportHoveredViewport; + public MouseCaptureMode MouseCaptureMode; + + // Gamepad Handling + public readonly nint[] Gamepads = new nint[16]; + public int GamepadCount; + public GamepadMode GamepadMode; + public bool WantUpdateGamepadsList; + + public PlatformData() + { + MouseLastCursor = nint.Zero; + + for (int i = 0; i < MouseCursors.Length; i++) + MouseCursors[i] = nint.Zero; + + for (int i = 0; i < Gamepads.Length; i++) + Gamepads[i] = nint.Zero; + } + } + + public class ViewPortData + { + public nint Window; + public nint Renderer; + public nint ParentWindow; + public uint WindowID; // Stored in ImGuiViewport.PlatformHandle. Use SDL.GetWindowFromID() to get SDL.Window* from Uint32 WindowID. + public bool WindowOwned; + public bool RendererOwned; + } + + // ReSharper disable NotAccessedField.Local + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate nint PlatformGetClipboardTextFn(nint ctx); + private static readonly PlatformGetClipboardTextFn GetClipboardDelegate = GetClipboardText; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void PlatformSetClipboardTextFn(nint ctx, nint text); + private static readonly PlatformSetClipboardTextFn SetClipboardDelegate = SetClipboardText; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void PlatformSetImeDataFn(nint ctx, nint userData, nint imeData); + private static readonly PlatformSetImeDataFn SetImeDataDelegate = SetImeData; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate bool PlatformOpenInShellFn(nint ctx, nint url); + private static readonly PlatformOpenInShellFn OpenInShellDelegate = OpenInShell; + // ReSharper restore NotAccessedField.Local + + public static PlatformData Data => ImGui.GetCurrentContext().Handle != null ? ImGuiUserData.Get(ImGui.GetIO().BackendPlatformUserData)! : null!; + + public static bool Init(nint window, nint renderer) + { + ImGuiIOPtr io = ImGui.GetIO(); + PlatformData data = new PlatformData + { + Window = window, + Renderer = renderer, + WindowID = SDL.GetWindowID(window) + }; + + io.BackendPlatformUserData = ImGuiUserData.Store(data); + io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; + io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; + + data.MouseCanUseGlobalState = false; + data.MouseCaptureMode = MouseCaptureMode.Disabled; + + string? backend = SDL.GetCurrentVideoDriver(); + string[] captureAndGlobalStateWhitelist = new string[] { "windows", "cocoa", "x11", "DIVE", "VMAN", "wayland" }; + foreach (var item in captureAndGlobalStateWhitelist) + { + if (item != backend) continue; + data.MouseCanUseGlobalState = true; + data.MouseCaptureMode = item == "x11" ? MouseCaptureMode.EnabledAfterDrag : MouseCaptureMode.Enabled; + } + if (data.MouseCanUseGlobalState) + { + io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports; // We can create multi-viewports on the Platform side (optional) + } + +#if __APPLE__ + data.MouseCanReportHoveredViewport = false; +#else + data.MouseCanReportHoveredViewport = data.MouseCanUseGlobalState; +#endif + + ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); + platformIo.PlatformGetClipboardTextFn = (void*)Marshal.GetFunctionPointerForDelegate(GetClipboardDelegate); + platformIo.PlatformSetClipboardTextFn = (void*)Marshal.GetFunctionPointerForDelegate(SetClipboardDelegate); + platformIo.PlatformSetImeDataFn = (void*)Marshal.GetFunctionPointerForDelegate(SetImeDataDelegate); + platformIo.PlatformOpenInShellFn = (void*)Marshal.GetFunctionPointerForDelegate(OpenInShellDelegate); + + UpdateMonitors(); + + data.GamepadMode = GamepadMode.AutoFirst; + data.WantUpdateGamepadsList = true; + + data.MouseCursors[(int)ImGuiMouseCursor.Arrow] = SDL.CreateSystemCursor(SDL.SystemCursor.Default); + data.MouseCursors[(int)ImGuiMouseCursor.TextInput] = SDL.CreateSystemCursor(SDL.SystemCursor.Text); + data.MouseCursors[(int)ImGuiMouseCursor.ResizeAll] = SDL.CreateSystemCursor(SDL.SystemCursor.Move); + data.MouseCursors[(int)ImGuiMouseCursor.ResizeNs] = SDL.CreateSystemCursor(SDL.SystemCursor.NSResize); + data.MouseCursors[(int)ImGuiMouseCursor.ResizeEw] = SDL.CreateSystemCursor(SDL.SystemCursor.EWResize); + data.MouseCursors[(int)ImGuiMouseCursor.ResizeNesw] = SDL.CreateSystemCursor(SDL.SystemCursor.NESWResize); + data.MouseCursors[(int)ImGuiMouseCursor.ResizeNwse] = SDL.CreateSystemCursor(SDL.SystemCursor.NWSEResize); + data.MouseCursors[(int)ImGuiMouseCursor.Hand] = SDL.CreateSystemCursor(SDL.SystemCursor.Pointer); + data.MouseCursors[(int)ImGuiMouseCursor.Wait] = SDL.CreateSystemCursor(SDL.SystemCursor.Wait); + data.MouseCursors[(int)ImGuiMouseCursor.Progress] = SDL.CreateSystemCursor(SDL.SystemCursor.Progress); + data.MouseCursors[(int)ImGuiMouseCursor.NotAllowed] = SDL.CreateSystemCursor(SDL.SystemCursor.NotAllowed); + + SetupPlatformHandles(ImGui.GetMainViewport(), window); + + SDL.SetHint(SDL.Hints.MouseFocusClickthrough, "1"); + SDL.SetHint(SDL.Hints.MouseAutoCapture, "0"); + SDL.SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "0"); + + if ((io.BackendFlags & ImGuiBackendFlags.PlatformHasViewports) != 0) + InitMultiViewportSupport(window); + + return true; + } + + private static void GetWindowSizeAndFramebufferScale(nint window, out Vector2 out_size, out Vector2 out_framebuffer_scale) + { + SDL.GetWindowSize(window, out int w, out int h); + if ((SDL.GetWindowFlags(window) & SDL.WindowFlags.Minimized) != 0) + w = h = 0; + + SDL.GetWindowSizeInPixels(window, out int displayW, out int displayH); + float fbScaleX = w > 0 ? displayW / (float)w : 1.0f; + float fbScaleY = h > 0 ? displayH / (float)h : 1.0f; + + out_size = new Vector2(w, h); + out_framebuffer_scale = new Vector2(fbScaleX, fbScaleY); + } + + + public static void NewFrame() + { + ImGuiIOPtr io = ImGui.GetIO(); + PlatformData data = Data; + + GetWindowSizeAndFramebufferScale(data.Window, out Vector2 size, out Vector2 framebufferScale); + + if (data.WantUpdateMonitors) + { + UpdateMonitors(); + } + + ulong frequency = SDL.GetPerformanceFrequency(); + ulong currentTime = SDL.GetPerformanceCounter(); + if (currentTime <= data.Time) + currentTime = data.Time + 1; + io.DeltaTime = data.Time > 0 ? (float)((currentTime - data.Time) / (double)frequency) : 1.0f / 60.0f; + data.Time = currentTime; + + io.DisplaySize = size; + io.DisplayFramebufferScale = framebufferScale; + + if (data.MouseLastLeaveFrame > 0 && data.MouseLastLeaveFrame >= ImGui.GetFrameCount() && data.MouseButtonsDown == 0) + { + data.MouseWindowID = 0; + data.MouseLastLeaveFrame = 0; + io.AddMousePosEvent(-float.MaxValue, -float.MaxValue); + } + + if (data.MouseCanReportHoveredViewport && ImGui.GetDragDropPayload().Equals(nint.Zero)) + { + io.BackendFlags |= ImGuiBackendFlags.HasMouseHoveredViewport; + } + else + { + io.BackendFlags &= ~ImGuiBackendFlags.HasMouseHoveredViewport; + } + + UpdateMouseData(data); + UpdateMouseCursor(data); + UpdateIme(data); + + UpdateGamepads(data); + } + + public static bool ProcessEvent(SDL.Event e) + { + ImGuiIOPtr io = ImGui.GetIO(); + PlatformData data = Data; + + switch ((SDL.EventType)e.Type) + { + case SDL.EventType.MouseMotion: + if (GetViewportForWindowId(e.Motion.WindowID) == null) + return false; + + io.AddMouseSourceEvent(e.Motion.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.AddMousePosEvent(e.Motion.X, e.Motion.Y); + return true; + case SDL.EventType.MouseWheel: + if (GetViewportForWindowId(e.Wheel.WindowID) == null) + return false; + + float wheelX = -e.Wheel.X; + float wheelY = e.Wheel.Y; + + io.AddMouseSourceEvent(e.Wheel.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.AddMouseWheelEvent(wheelX, wheelY); + return true; + case SDL.EventType.MouseButtonDown: + case SDL.EventType.MouseButtonUp: + if (GetViewportForWindowId(e.Button.WindowID) == null) + return false; + + int mouseButton = e.Button.Button switch + { + SDL.ButtonLeft => 0, + SDL.ButtonRight => 1, + SDL.ButtonMiddle => 2, + SDL.ButtonX1 => 3, + SDL.ButtonX2 => 4, + _ => -1 + }; + if (mouseButton == -1) break; + + io.AddMouseSourceEvent(e.Button.Which == SDL.TouchMouseID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.AddMouseButtonEvent(mouseButton, (SDL.EventType)e.Type == SDL.EventType.MouseButtonDown); + data.MouseButtonsDown = (SDL.EventType)e.Type == SDL.EventType.MouseButtonDown ? data.MouseButtonsDown | 1 << mouseButton : data.MouseButtonsDown & ~(1 << mouseButton); + return true; + case SDL.EventType.TextInput: + if (GetViewportForWindowId(e.Text.WindowID) == null) + return false; + + ImGui.GetIO().AddInputCharactersUTF8((byte*)e.Text.Text); + return true; + case SDL.EventType.KeyDown: + case SDL.EventType.KeyUp: + if (GetViewportForWindowId(e.Key.WindowID) == null) + return false; + + UpdateKeyModifiers(e.Key.Mod); + ImGuiKey imguiKey = KeyEventToImGui(e.Key.Key, e.Key.Scancode); + bool pressed = (SDL.EventType)e.Type == SDL.EventType.KeyDown; + + io.AddKeyEvent(imguiKey, pressed); + io.SetKeyEventNativeData(imguiKey, (int)e.Key.Key, (int)e.Key.Scancode, (int)e.Key.Scancode); + + return true; + case SDL.EventType.WindowMouseEnter: + if (GetViewportForWindowId(e.Window.WindowID) == null) + return false; + + data.MouseWindowID = e.Window.WindowID; + data.MouseLastLeaveFrame = 0; + return true; + case SDL.EventType.WindowMouseLeave: + if (GetViewportForWindowId(e.Window.WindowID) == null) + return false; + + data.MouseLastLeaveFrame = ImGui.GetFrameCount() + 1; + return true; + case SDL.EventType.WindowFocusGained: + case SDL.EventType.WindowFocusLost: + if (GetViewportForWindowId(e.Window.WindowID) == null) + return false; + + io.AddFocusEvent((SDL.EventType)e.Type == SDL.EventType.WindowFocusGained); + return true; + } + + return false; + } + + private static void UpdateMouseData(PlatformData data) + { + if (SDL.GetKeyboardFocus() == data.Window && ImGui.GetIO().WantSetMousePos) + SDL.WarpMouseInWindow(data.Window, (int)ImGui.GetIO().MousePos.X, (int)ImGui.GetIO().MousePos.Y); + } + + private static void UpdateMouseCursor(PlatformData data) + { + ImGuiIOPtr io = ImGui.GetIO(); + + if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) != 0) + return; + + ImGuiMouseCursor cursor = ImGui.GetMouseCursor(); + + if (io.MouseDrawCursor || cursor == ImGuiMouseCursor.None) + { + SDL.HideCursor(); + return; + } + + nint expected = data.MouseCursors[(int)cursor]; + + if (data.MouseLastCursor != expected) + { + SDL.SetCursor(expected); + data.MouseLastCursor = expected; + } + + SDL.ShowCursor(); + } + + private static void CloseGamepads() + { + PlatformData bd = Data; + + if (bd.GamepadMode != GamepadMode.Manual) + { + for (int i = 0; i < bd.GamepadCount; i++) + { + SDL.CloseGamepad(bd.Gamepads[i]); + bd.Gamepads[i] = nint.Zero; + } + } + + bd.GamepadCount = 0; + } + + private static void SetGamepadMode(GamepadMode mode, nint[] manual_gamepads_array, int manual_gamepads_count) + { + PlatformData bd = Data; + + CloseGamepads(); + + if (mode == GamepadMode.Manual) + { + int count = manual_gamepads_count; + + if (count > bd.Gamepads.Length) + count = bd.Gamepads.Length; + + for (int n = 0; n < count; n++) + bd.Gamepads[n] = manual_gamepads_array[n]; + + bd.GamepadCount = count; + } + else + { + bd.WantUpdateGamepadsList = true; + } + + bd.GamepadMode = mode; + } + + private static void UpdateGamepadButton(PlatformData bd, ImGuiIOPtr io, ImGuiKey key, SDL.GamepadButton button_no) + { + bool mergedValue = false; + + for (int i = 0; i < bd.GamepadCount; i++) + { + nint gamepad = bd.Gamepads[i]; + mergedValue |= SDL.GetGamepadButton(gamepad, button_no); + } + + io.AddKeyEvent(key, mergedValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Saturate(float v) + { + return v < 0.0f ? 0.0f : v > 1.0f ? 1.0f : v; + } + private static void UpdateGamepadAnalog(PlatformData bd, ImGuiIOPtr io, ImGuiKey key, SDL.GamepadAxis axis_no, float v0, float v1) + { + float mergedValue = 0.0f; + + for (int i = 0; i < bd.GamepadCount; i++) + { + nint gamepad = bd.Gamepads[i]; + + float raw = SDL.GetGamepadAxis(gamepad, axis_no); + float vn = Saturate((raw - v0) / (v1 - v0)); + + if (mergedValue < vn) + mergedValue = vn; + } + + io.AddKeyAnalogEvent(key, mergedValue > 0.1f, mergedValue); + } + + private static void UpdateGamepads(PlatformData data) + { + ImGuiIOPtr io = ImGui.GetIO(); + + if (data.WantUpdateGamepadsList && data.GamepadMode != GamepadMode.Manual) + { + CloseGamepads(); + + uint[] sdlGamepads = SDL.GetGamepads(out int count); + + for (int n = 0; n < count; n++) + { + if (data.GamepadCount >= data.Gamepads.Length) + break; + + data.Gamepads[data.GamepadCount++] = SDL.OpenGamepad(sdlGamepads[n]); + + if (data.GamepadMode == GamepadMode.AutoFirst) + break; + } + + data.WantUpdateGamepadsList = false; + } + + io.BackendFlags &= ~ImGuiBackendFlags.HasGamepad; + + if (data.GamepadCount == 0) + return; + + io.BackendFlags |= ImGuiBackendFlags.HasGamepad; + + const int thumbDeadZone = 8000; + + UpdateGamepadButton(data, io, ImGuiKey.GamepadStart, SDL.GamepadButton.Start); + UpdateGamepadButton(data, io, ImGuiKey.GamepadBack, SDL.GamepadButton.Back); + UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceLeft, SDL.GamepadButton.West); + UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceRight, SDL.GamepadButton.East); + UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceUp, SDL.GamepadButton.North); + UpdateGamepadButton(data, io, ImGuiKey.GamepadFaceDown, SDL.GamepadButton.South); + UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadLeft, SDL.GamepadButton.DPadLeft); + UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadRight, SDL.GamepadButton.DPadRight); + UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadUp, SDL.GamepadButton.DPadUp); + UpdateGamepadButton(data, io, ImGuiKey.GamepadDpadDown, SDL.GamepadButton.DPadDown); + UpdateGamepadButton(data, io, ImGuiKey.GamepadL1, SDL.GamepadButton.LeftShoulder); + UpdateGamepadButton(data, io, ImGuiKey.GamepadR1, SDL.GamepadButton.RightShoulder); + + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadL2, SDL.GamepadAxis.LeftTrigger, 0.0f, 32767); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadR2, SDL.GamepadAxis.RightTrigger, 0.0f, 32767); + + UpdateGamepadButton(data, io, ImGuiKey.GamepadL3, SDL.GamepadButton.LeftStick); + UpdateGamepadButton(data, io, ImGuiKey.GamepadR3, SDL.GamepadButton.RightStick); + + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickLeft, SDL.GamepadAxis.LeftX, -thumbDeadZone, -32768); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickRight, SDL.GamepadAxis.LeftX, thumbDeadZone, 32767); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickUp, SDL.GamepadAxis.LeftY, -thumbDeadZone, -32768); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadLStickDown, SDL.GamepadAxis.LeftY, thumbDeadZone, 32767); + + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickLeft, SDL.GamepadAxis.RightX, -thumbDeadZone, -32768); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickRight, SDL.GamepadAxis.RightX, thumbDeadZone, 32767); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickUp, SDL.GamepadAxis.RightY, -thumbDeadZone, -32768); + UpdateGamepadAnalog(data, io, ImGuiKey.GamepadRStickDown, SDL.GamepadAxis.RightY, thumbDeadZone, 32767); + } + + public static void UpdateMonitors() + { + PlatformData bd = Data; + ImGuiPlatformIOPtr platformio = ImGui.GetPlatformIO(); + platformio.Monitors.Resize(0); + bd.WantUpdateMonitors = false; + + var displays = SDL.GetDisplays(out int displayCount); + for (int n = 0; n < displayCount; n++) + { + // Warning: the validity of monitor DPI information on Windows depends on the application DPI awareness settings, which generally needs to be set in the manifest or at runtime. + var displayID = displays[n]; + ImGuiPlatformMonitor monitor = new ImGuiPlatformMonitor(); + SDL.GetDisplayBounds(displayID, out SDL.Rect r); + monitor.MainPos = monitor.WorkPos = new Vector2(r.X, r.Y); + monitor.MainSize = monitor.WorkSize = new Vector2(r.W, r.H); + if (SDL.GetDisplayUsableBounds(displayID, out SDL.Rect r2) && r2.W > 0 && r2.H > 0) + { + monitor.WorkPos = new Vector2(r2.X, r2.Y); + monitor.WorkSize = new Vector2(r2.W, r2.H); + } + monitor.DpiScale = SDL.GetDisplayContentScale(displayID); // See https://wiki.libsdl.org/SDL3/README-highdpi for details. + monitor.PlatformHandle = (void*)n; + if (monitor.DpiScale <= 0.0f) + continue; // Some accessibility applications are declaring virtual monitors with a DPI of 0, see #7902. + platformio.Monitors.PushBack(monitor); + } + } + + private static ImGuiKey KeyEventToImGui(SDL.Keycode keycode, SDL.Scancode scancode) + { + return scancode switch + { + SDL.Scancode.Kp0 => ImGuiKey.Keypad0, + SDL.Scancode.Kp1 => ImGuiKey.Keypad1, + SDL.Scancode.Kp2 => ImGuiKey.Keypad2, + SDL.Scancode.Kp3 => ImGuiKey.Keypad3, + SDL.Scancode.Kp4 => ImGuiKey.Keypad4, + SDL.Scancode.Kp5 => ImGuiKey.Keypad5, + SDL.Scancode.Kp6 => ImGuiKey.Keypad6, + SDL.Scancode.Kp7 => ImGuiKey.Keypad7, + SDL.Scancode.Kp8 => ImGuiKey.Keypad8, + SDL.Scancode.Kp9 => ImGuiKey.Keypad9, + SDL.Scancode.KpPeriod => ImGuiKey.KeypadDecimal, + SDL.Scancode.KpDivide => ImGuiKey.KeypadDivide, + SDL.Scancode.KpMultiply => ImGuiKey.KeypadMultiply, + SDL.Scancode.KpMinus => ImGuiKey.KeypadSubtract, + SDL.Scancode.KpPlus => ImGuiKey.KeypadAdd, + SDL.Scancode.KpEnter => ImGuiKey.KeypadEnter, + SDL.Scancode.KpEquals => ImGuiKey.KeypadEqual, + _ => keycode switch + { + SDL.Keycode.Tab => ImGuiKey.Tab, + SDL.Keycode.Left => ImGuiKey.LeftArrow, + SDL.Keycode.Right => ImGuiKey.RightArrow, + SDL.Keycode.Up => ImGuiKey.UpArrow, + SDL.Keycode.Down => ImGuiKey.DownArrow, + SDL.Keycode.Pageup => ImGuiKey.PageUp, + SDL.Keycode.Pagedown => ImGuiKey.PageDown, + SDL.Keycode.Home => ImGuiKey.Home, + SDL.Keycode.End => ImGuiKey.End, + SDL.Keycode.Insert => ImGuiKey.Insert, + SDL.Keycode.Delete => ImGuiKey.Delete, + SDL.Keycode.Backspace => ImGuiKey.Backspace, + SDL.Keycode.Space => ImGuiKey.Space, + SDL.Keycode.Return => ImGuiKey.Enter, + SDL.Keycode.Escape => ImGuiKey.Escape, + SDL.Keycode.Apostrophe => ImGuiKey.Apostrophe, + SDL.Keycode.Comma => ImGuiKey.Comma, + SDL.Keycode.Minus => ImGuiKey.Minus, + SDL.Keycode.Period => ImGuiKey.Period, + SDL.Keycode.Slash => ImGuiKey.Slash, + SDL.Keycode.Semicolon => ImGuiKey.Semicolon, + SDL.Keycode.Equals => ImGuiKey.Equal, + SDL.Keycode.LeftBracket => ImGuiKey.LeftBracket, + SDL.Keycode.Backslash => ImGuiKey.Backslash, + SDL.Keycode.RightBracket => ImGuiKey.RightBracket, + SDL.Keycode.Grave => ImGuiKey.GraveAccent, + SDL.Keycode.Capslock => ImGuiKey.CapsLock, + SDL.Keycode.ScrollLock => ImGuiKey.ScrollLock, + SDL.Keycode.NumLockClear => ImGuiKey.NumLock, + SDL.Keycode.PrintScreen => ImGuiKey.PrintScreen, + SDL.Keycode.Pause => ImGuiKey.Pause, + SDL.Keycode.LCtrl => ImGuiKey.LeftCtrl, + SDL.Keycode.LShift => ImGuiKey.LeftShift, + SDL.Keycode.LAlt => ImGuiKey.LeftAlt, + SDL.Keycode.LGui => ImGuiKey.LeftSuper, + SDL.Keycode.RCtrl => ImGuiKey.RightCtrl, + SDL.Keycode.RShift => ImGuiKey.RightShift, + SDL.Keycode.RAlt => ImGuiKey.RightAlt, + SDL.Keycode.RGUI => ImGuiKey.RightSuper, + SDL.Keycode.Application => ImGuiKey.Menu, + SDL.Keycode.Alpha0 => ImGuiKey.Key0, + SDL.Keycode.Alpha1 => ImGuiKey.Key1, + SDL.Keycode.Alpha2 => ImGuiKey.Key2, + SDL.Keycode.Alpha3 => ImGuiKey.Key3, + SDL.Keycode.Alpha4 => ImGuiKey.Key4, + SDL.Keycode.Alpha5 => ImGuiKey.Key5, + SDL.Keycode.Alpha6 => ImGuiKey.Key6, + SDL.Keycode.Alpha7 => ImGuiKey.Key7, + SDL.Keycode.Alpha8 => ImGuiKey.Key8, + SDL.Keycode.Alpha9 => ImGuiKey.Key9, + SDL.Keycode.A => ImGuiKey.A, + SDL.Keycode.B => ImGuiKey.B, + SDL.Keycode.C => ImGuiKey.C, + SDL.Keycode.D => ImGuiKey.D, + SDL.Keycode.E => ImGuiKey.E, + SDL.Keycode.F => ImGuiKey.F, + SDL.Keycode.G => ImGuiKey.G, + SDL.Keycode.H => ImGuiKey.H, + SDL.Keycode.I => ImGuiKey.I, + SDL.Keycode.J => ImGuiKey.J, + SDL.Keycode.K => ImGuiKey.K, + SDL.Keycode.L => ImGuiKey.L, + SDL.Keycode.M => ImGuiKey.M, + SDL.Keycode.N => ImGuiKey.N, + SDL.Keycode.O => ImGuiKey.O, + SDL.Keycode.P => ImGuiKey.P, + SDL.Keycode.Q => ImGuiKey.Q, + SDL.Keycode.R => ImGuiKey.R, + SDL.Keycode.S => ImGuiKey.S, + SDL.Keycode.T => ImGuiKey.T, + SDL.Keycode.U => ImGuiKey.U, + SDL.Keycode.V => ImGuiKey.V, + SDL.Keycode.W => ImGuiKey.W, + SDL.Keycode.X => ImGuiKey.X, + SDL.Keycode.Y => ImGuiKey.Y, + SDL.Keycode.Z => ImGuiKey.Z, + SDL.Keycode.F1 => ImGuiKey.F1, + SDL.Keycode.F2 => ImGuiKey.F2, + SDL.Keycode.F3 => ImGuiKey.F3, + SDL.Keycode.F4 => ImGuiKey.F4, + SDL.Keycode.F5 => ImGuiKey.F5, + SDL.Keycode.F6 => ImGuiKey.F6, + SDL.Keycode.F7 => ImGuiKey.F7, + SDL.Keycode.F8 => ImGuiKey.F8, + SDL.Keycode.F9 => ImGuiKey.F9, + SDL.Keycode.F10 => ImGuiKey.F10, + SDL.Keycode.F11 => ImGuiKey.F11, + SDL.Keycode.F12 => ImGuiKey.F12, + SDL.Keycode.F13 => ImGuiKey.F13, + SDL.Keycode.F14 => ImGuiKey.F14, + SDL.Keycode.F15 => ImGuiKey.F15, + SDL.Keycode.F16 => ImGuiKey.F16, + SDL.Keycode.F17 => ImGuiKey.F17, + SDL.Keycode.F18 => ImGuiKey.F18, + SDL.Keycode.F19 => ImGuiKey.F19, + SDL.Keycode.F20 => ImGuiKey.F20, + SDL.Keycode.F21 => ImGuiKey.F21, + SDL.Keycode.F22 => ImGuiKey.F22, + SDL.Keycode.F23 => ImGuiKey.F23, + SDL.Keycode.F24 => ImGuiKey.F24, + SDL.Keycode.AcBack => ImGuiKey.AppBack, + SDL.Keycode.AcForward => ImGuiKey.AppForward, + _ => ImGuiKey.None + } + }; + + } + + public static nint GetClipboardText(nint ctx) + { + PlatformData data = Data; + + string text = SDL.GetClipboardText(); + data.ClipboardTextData = Marshal.StringToHGlobalAnsi(text ?? string.Empty); + return data.ClipboardTextData; + } + + public static void SetClipboardText(nint ctx, nint text) + { + string? managedText = Marshal.PtrToStringAnsi(text); + if (managedText != null) + { + SDL.SetClipboardText(managedText); + } + } + + private static void SetImeData(nint ctx, nint userData, nint imeData) + { + PlatformData data = Data; + data.ImeData = Marshal.PtrToStructure(imeData); + data.ImeDirty = true; + } + + public static void UpdateIme(PlatformData data) + { + ImGuiPlatformImeData imeData = data.ImeData; + nint window = SDL.GetKeyboardFocus(); + + // Stop previous input + if ((imeData.WantVisible != 1 || data.ImeWindow != window) && data.ImeWindow != nint.Zero) + { + SDL.StopTextInput(data.ImeWindow); + data.ImeWindow = nint.Zero; + } + if (!data.ImeDirty && data.ImeWindow == window || window == nint.Zero) + return; + + // Start/update current input + data.ImeDirty = false; + if (imeData.WantVisible == 1) + { + SDL.Rect r = new SDL.Rect + { + X = (int)imeData.InputPos.X, + Y = (int)imeData.InputPos.Y, + W = 1, + H = (int)imeData.InputLineHeight + }; + SDL.SetTextInputArea(window, r, 0); + data.ImeWindow = window; + } + if (!SDL.TextInputActive(window) && imeData.WantVisible == 1) + SDL.StartTextInput(window); + } + + private static bool OpenInShell(nint ctx, nint url) + { + string managedUrl = Marshal.PtrToStringUTF8(url) ?? string.Empty; + return SDL.OpenURL(managedUrl); + } + + // !! This is sorta fucked + // public static void UpdateKeyModifiers(SDL.Keymod keymods) + // { + // ImGuiIOPtr io = ImGui.GetIO(); + // io.KeyCtrl = keymods.HasFlag(SDL.Keymod.LCtrl) || keymods.HasFlag(SDL.Keymod.RCtrl); + // io.KeyShift = keymods.HasFlag(SDL.Keymod.LShift) || keymods.HasFlag(SDL.Keymod.RShift); + // io.KeyAlt = keymods.HasFlag(SDL.Keymod.LAlt) || keymods.HasFlag(SDL.Keymod.RAlt); + // io.KeySuper = keymods.HasFlag(SDL.Keymod.LGUI) || keymods.HasFlag(SDL.Keymod.RGUI); + // } + + private static void UpdateKeyModifiers(SDL.Keymod mod) + { + ImGuiIOPtr io = ImGui.GetIO(); + io.AddKeyEvent(ImGuiKey.ModCtrl, (mod & SDL.Keymod.Ctrl) != 0); + io.AddKeyEvent(ImGuiKey.ModShift, (mod & SDL.Keymod.Shift) != 0); + io.AddKeyEvent(ImGuiKey.ModAlt, (mod & SDL.Keymod.Alt) != 0); + io.AddKeyEvent(ImGuiKey.ModSuper, (mod & SDL.Keymod.GUI) != 0); + } + + private static void CreateWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + PlatformData bd = Data; + + ViewPortData vd = new ViewPortData(); + viewport.PlatformUserData = ImGuiUserData.Store(vd); + + SDL.WindowFlags flags = SDL.WindowFlags.Hidden; + + flags |= SDL.GetWindowFlags(bd.Window) & SDL.WindowFlags.HighPixelDensity; + + string? videoDriver = SDL.GetCurrentVideoDriver(); + bool isWayland = string.Equals(videoDriver, "wayland", StringComparison.OrdinalIgnoreCase); + + if ((viewport.Flags & ImGuiViewportFlags.NoDecoration) != 0 && !isWayland) + { + flags |= SDL.WindowFlags.Borderless; + } + else + { + flags |= SDL.WindowFlags.Resizable; + } + + if ((viewport.Flags & ImGuiViewportFlags.NoTaskBarIcon) != 0) + { + flags |= SDL.WindowFlags.Utility; + } + + if ((viewport.Flags & ImGuiViewportFlags.TopMost) != 0) + { + flags |= SDL.WindowFlags.AlwaysOnTop; + } + + vd.Window = SDL.CreateWindow("Untitled", (int)viewport.Size.X, (int)viewport.Size.Y, flags); + + ImGuiViewportPtr? parentViewport = viewport.ParentViewportId != 0 ? ImGui.FindViewportByID(viewport.ParentViewportId) : null; + if (parentViewport != null && parentViewport.Value.Handle != null) + { + vd.ParentWindow = GetSDLWindowFromViewport(parentViewport.Value); + if (vd.ParentWindow != nint.Zero) + SDL.SetWindowParent(vd.Window, vd.ParentWindow); + } + + SDL.SetWindowPosition(vd.Window, (int)viewport.Pos.X, (int)viewport.Pos.Y); + vd.WindowOwned = true; + + SetupPlatformHandles(viewport, vd.Window); + + Program.Logger.Log($"SDL Window: {vd.Window} {vd.WindowID} {viewport.Pos.X} {viewport.Pos.Y} {viewport.Size.X} {viewport.Size.Y}"); + +#if WINDOWS + viewport.PlatformHandleRaw = SDL.GetPointerProperty( + SDL.GetWindowProperties(vd.Window), + SDL.Properties.WindowWin32HWNDPointer, + nint.Zero + ).ToPointer(); +#endif + } + + private static void DestroyWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); + + if (vd != null) + { + if (vd.Window != nint.Zero && vd.WindowOwned) + { + SDL.DestroyWindow(vd.Window); + } + } + + ImGuiUserData.Free(viewport.PlatformUserData); + + viewport.PlatformUserData = null; + viewport.PlatformHandle = null; + viewport.PlatformHandleRaw = null; + + Program.Logger.Log(null); + } + + private static void ShowWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + string? oldHint = SDL.GetHint(SDL.Hints.WindowActivateWhenShown); + + SDL.SetHint(SDL.Hints.WindowActivateWhenShown, ((viewport.Flags & ImGuiViewportFlags.NoFocusOnAppearing) != 0) ? "0" : "1"); + + SDL.ShowWindow(vd.Window); + + SDL.SetHint(SDL.Hints.WindowActivateWhenShown, oldHint); + Program.Logger.Log(null); + } + + private static void UpdateWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + if ((viewport.Flags & ImGuiViewportFlags.TopMost) != 0) + { + SDL.SetWindowAlwaysOnTop(vd.Window, true); + } + else + { + SDL.SetWindowAlwaysOnTop(vd.Window, false); + } + Program.Logger.Log(null); + } + + private static Vector2 GetWindowPos(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.GetWindowPosition(vd.Window, out int x, out int y); + + // Program.Logger.Log(null); + + return new Vector2(x, y); + } + + private static void SetWindowPos(ImGuiViewportPtr viewport, Vector2 pos) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.SetWindowPosition(vd.Window, (int)pos.X, (int)pos.Y); + Program.Logger.Log(null); + } + + private static Vector2 GetWindowSize(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.GetWindowSize(vd.Window, out int w, out int h); + + // Program.Logger.Log(null); + return new Vector2(w, h); + } + + private static void SetWindowSize(ImGuiViewportPtr viewport, Vector2 size) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.SetWindowSize(vd.Window, (int)size.X, (int)size.Y); + Program.Logger.Log(null); + } + + private static Vector2 GetWindowFramebufferScale(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + GetWindowSizeAndFramebufferScale(vd.Window, out Vector2 framebufferSize, out Vector2 framebufferScale); + // Program.Logger.Log(null); + + return framebufferScale; + } + + private static void SetWindowTitle(ImGuiViewportPtr viewport, string title) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.SetWindowTitle(vd.Window, title); + Program.Logger.Log(null); + } + + private static void SetWindowAlpha(ImGuiViewportPtr viewport, float alpha) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.SetWindowOpacity(vd.Window, alpha); + Program.Logger.Log(null); + } + + private static void SetWindowFocus(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + + SDL.RaiseWindow(vd.Window); + Program.Logger.Log(null); + } + + private static bool GetWindowFocus(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + // Program.Logger.Log(null); + + return (SDL.GetWindowFlags(vd.Window) & SDL.WindowFlags.InputFocus) != 0; + } + + private static bool GetWindowMinimized(ImGuiViewportPtr viewport) + { + ViewPortData vd = ImGuiUserData.Get(viewport.PlatformUserData)!; + // Program.Logger.Log(null); + + return (SDL.GetWindowFlags(vd.Window) & SDL.WindowFlags.Minimized) != 0; + } + + private static void RenderWindow(ImGuiViewportPtr viewport) + { + // Renderer-backed viewport rendering is handled by ImGuiSDL3Renderer. + Program.Logger.Log(null); + } + + private static void SwapBuffers(ImGuiViewportPtr viewport) + { + // Renderer-backed viewport presentation is handled by ImGuiSDL3Renderer. + Program.Logger.Log(null); + } + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void CreateWindowDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly CreateWindowDelegatelFn CreateWindowDelegate = CreateWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void DestroyWindowDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly DestroyWindowDelegatelFn DestroyWindowDelegate = DestroyWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void ShowWindowDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly ShowWindowDelegatelFn ShowWindowDelegate = ShowWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void UpdateWindowDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly UpdateWindowDelegatelFn UpdateWindowDelegate = UpdateWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetWindowPosDelegatelFn(ImGuiViewportPtr ctx, Vector2 pos); + + private static readonly SetWindowPosDelegatelFn SetWindowPosDelegate = SetWindowPos; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate Vector2 GetWindowPosDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly GetWindowPosDelegatelFn GetWindowPosDelegate = GetWindowPos; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetWindowSizeDelegatelFn(ImGuiViewportPtr ctx, Vector2 size); + + private static readonly SetWindowSizeDelegatelFn SetWindowSizeDelegate = SetWindowSize; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate Vector2 GetWindowSizeDelegateFn(ImGuiViewportPtr ctx); + + private static readonly GetWindowSizeDelegateFn GetWindowSizeDelegate = GetWindowSize; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate Vector2 GetWindowFramebufferScaleDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly GetWindowFramebufferScaleDelegatelFn GetWindowFramebufferScaleDelegate = GetWindowFramebufferScale; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetWindowFocusDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly SetWindowFocusDelegatelFn SetWindowFocusDelegate = SetWindowFocus; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate bool GetWindowFocusDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly GetWindowFocusDelegatelFn GetWindowFocusDelegate = GetWindowFocus; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate bool GetWindowMinimizedDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly GetWindowMinimizedDelegatelFn GetWindowMinimizedDelegate = GetWindowMinimized; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetWindowTitleDelegatelFn(ImGuiViewportPtr ctx, string title); + + private static readonly SetWindowTitleDelegatelFn SetWindowTitleDelegate = SetWindowTitle; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RenderWindowDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly RenderWindowDelegatelFn RenderWindowDelegate = RenderWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SwapBuffersDelegatelFn(ImGuiViewportPtr ctx); + + private static readonly SwapBuffersDelegatelFn SwapBuffersDelegate = SwapBuffers; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SetWindowAlphaDelegatelFn(ImGuiViewportPtr ctx, float alpha); + + private static readonly SetWindowAlphaDelegatelFn SetWindowAlphaDelegate = SetWindowAlpha; + + private static void InitMultiViewportSupport(nint window) + { + Program.Logger.Log(null); + + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + + platformIO.PlatformCreateWindow = (void*)Marshal.GetFunctionPointerForDelegate(CreateWindowDelegate); + platformIO.PlatformDestroyWindow = (void*)Marshal.GetFunctionPointerForDelegate(DestroyWindowDelegate); + platformIO.PlatformShowWindow = (void*)Marshal.GetFunctionPointerForDelegate(ShowWindowDelegate); + platformIO.PlatformSetWindowPos = (void*)Marshal.GetFunctionPointerForDelegate(SetWindowPosDelegate); + platformIO.PlatformGetWindowPos = (void*)Marshal.GetFunctionPointerForDelegate(GetWindowPosDelegate); + platformIO.PlatformSetWindowSize = (void*)Marshal.GetFunctionPointerForDelegate(SetWindowSizeDelegate); + platformIO.PlatformGetWindowSize = (void*)Marshal.GetFunctionPointerForDelegate(GetWindowSizeDelegate); + platformIO.PlatformSetWindowFocus = (void*)Marshal.GetFunctionPointerForDelegate(SetWindowFocusDelegate); + platformIO.PlatformGetWindowFocus = (void*)Marshal.GetFunctionPointerForDelegate(GetWindowFocusDelegate); + platformIO.PlatformGetWindowMinimized = (void*)Marshal.GetFunctionPointerForDelegate(GetWindowMinimizedDelegate); + platformIO.PlatformSetWindowTitle = (void*)Marshal.GetFunctionPointerForDelegate(SetWindowTitleDelegate); + platformIO.PlatformSetWindowAlpha = (void*)Marshal.GetFunctionPointerForDelegate(SetWindowAlphaDelegate); + platformIO.PlatformUpdateWindow = (void*)Marshal.GetFunctionPointerForDelegate(UpdateWindowDelegate); + platformIO.PlatformGetWindowFramebufferScale = (void*)Marshal.GetFunctionPointerForDelegate(GetWindowFramebufferScaleDelegate); + + ImGuiViewportPtr mainViewport = ImGui.GetMainViewport(); + + ViewPortData vd = new ViewPortData + { + Window = window, + Renderer = Data.Renderer, + WindowID = SDL.GetWindowID(window), + WindowOwned = false, + RendererOwned = false + }; + + mainViewport.PlatformUserData = ImGuiUserData.Store(vd); + mainViewport.PlatformHandle = (void*)(nint)SDL.GetWindowID(window); + + Program.Logger.Log(null); + +#if WINDOWS + mainViewport.PlatformHandleRaw = SDL.GetPointerProperty( + SDL.GetWindowProperties(window), + SDL.Properties.WindowWin32HWNDPointer, + nint.Zero + ).ToPointer(); +#endif + } + + public static ImGuiViewportPtr? GetViewportForWindowId(uint id) + { + return ImGui.FindViewportByPlatformHandle((void*)(nint)id); + } + + private static nint GetSDLWindowFromViewport(ImGuiViewportPtr viewport) + { + return SDL.GetWindowFromID((uint)(nint)viewport.PlatformHandle); + } + + private static void SetupPlatformHandles(ImGuiViewportPtr viewport, nint window) + { + viewport.PlatformHandle = (void*)(nint)SDL.GetWindowID(window); + viewport.PlatformHandleRaw = (void*)nint.Zero; +#if _WIN32 && !__WINTR__ + SDL.GetPointerProperty(SDL.GetWindowProperties(window), SDL.Props.WindowWin32HWNDPointer, 0); +#elif __APPLE__ && SDL_VIDEO_DRIVER_COCOA + SDL.GetPointerProperty(SDL.GetWindowProperties(window), SDL.Props.WindowCocoaWindow, 0); +#endif + } + + public static void Dispose() + { + PlatformData? data = Data; + + if (data != null) + { + foreach (nint cursor in data.MouseCursors) + { + if (cursor != nint.Zero) + SDL.DestroyCursor(cursor); + } + } + + ImGuiIOPtr io = ImGui.GetIO(); + ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); + platformIo.PlatformGetClipboardTextFn = null; + platformIo.PlatformSetClipboardTextFn = null; + platformIo.PlatformSetImeDataFn = null; + platformIo.PlatformOpenInShellFn = null; + ImGuiUserData.Free(io.BackendPlatformUserData); + io.BackendPlatformUserData = null; + } +} + +public enum GamepadMode +{ + AutoFirst, + AutoAll, + Manual +} + +public enum MouseCaptureMode +{ + Enabled, + EnabledAfterDrag, + Disabled +} + +public unsafe static class ImGuiUserData where T : class +{ + public static void* Store(T value) + { + if (value == null) + return null; + + GCHandle handle = GCHandle.Alloc(value, GCHandleType.Normal); + return (void*)GCHandle.ToIntPtr(handle); + } + + public static T Get(void* ptr) + { + if (ptr == null) + return null; + + GCHandle handle = GCHandle.FromIntPtr((nint)ptr); + return (T)handle.Target; + } + + public static void Free(void* ptr) + { + if (ptr == null) + return; + + GCHandle handle = GCHandle.FromIntPtr((nint)ptr); + + if (handle.IsAllocated) + handle.Free(); + } +} diff --git a/SDL3/ImGuiSDL3Renderer.cs b/SDL3/ImGuiSDL3Renderer.cs new file mode 100644 index 0000000..b03dd0d --- /dev/null +++ b/SDL3/ImGuiSDL3Renderer.cs @@ -0,0 +1,433 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using Hexa.NET.ImGui; +using SDL3; + +namespace SDL3_TestingSuite.SDL3; + +/// +/// Implementation of ImGui for SDL3 Renderer backend. +/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdlrenderer3.h +/// +public unsafe static class ImGuiSDL3Renderer +{ + private sealed class TextureState + { + public ImTextureDataPtr Source; + public readonly Dictionary RendererTextures = new(); + } + + public class RendererData + { + public nint Renderer; // Main viewport's renderer + public ImVector ColorBuffer; + + // Render State + public SDL.ScaleMode CurrentScaleMode; + } + + private struct BackupSDLRendererState + { + public SDL.Rect Viewport; + public bool ViewportEnabled; + public bool ClipEnabled; + public SDL.Rect ClipRect; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RendererCreateWindowFn(ImGuiViewportPtr viewport); + + private static readonly RendererCreateWindowFn RendererCreateWindowDelegate = RendererCreateWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RendererDestroyWindowFn(ImGuiViewportPtr viewport); + + private static readonly RendererDestroyWindowFn RendererDestroyWindowDelegate = RendererDestroyWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RendererSetWindowSizeFn(ImGuiViewportPtr viewport, Vector2 size); + + private static readonly RendererSetWindowSizeFn RendererSetWindowSizeDelegate = RendererSetWindowSize; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RendererRenderWindowFn(ImGuiViewportPtr viewport); + + private static readonly RendererRenderWindowFn RendererRenderWindowDelegate = RendererRenderWindow; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void RendererSwapBuffersFn(ImGuiViewportPtr viewport); + + private static readonly RendererSwapBuffersFn RendererSwapBuffersDelegate = RendererSwapBuffers; + + public static RendererData Data => ImGui.GetCurrentContext().Handle != null ? ImGuiUserData.Get(ImGui.GetIO().BackendRendererUserData)! : null!; + + public static bool Init(nint renderer) + { + ImGuiIOPtr io = ImGui.GetIO(); + + RendererData bd = new RendererData(); + io.BackendRendererUserData = ImGuiUserData.Store(bd); + io.BackendRendererName = (byte*)Marshal.StringToHGlobalAnsi("NepImGuiSDL3Renderer"); + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; // We can honor the ImDrawCmd.VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags.RendererHasTextures; // We can honor ImGuiPlatformIO.Textures[] requests during render. + io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports; + + bd.Renderer = renderer; + + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + platformIO.RendererCreateWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererCreateWindowDelegate); + platformIO.RendererDestroyWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererDestroyWindowDelegate); + platformIO.RendererSetWindowSize = (void*)Marshal.GetFunctionPointerForDelegate(RendererSetWindowSizeDelegate); + platformIO.RendererRenderWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererRenderWindowDelegate); + platformIO.RendererSwapBuffers = (void*)Marshal.GetFunctionPointerForDelegate(RendererSwapBuffersDelegate); + + return true; + } + + public static void Dispose() + { + var io = ImGui.GetIO(); + var platformIO = ImGui.GetPlatformIO(); + + DestroyDeviceObjects(); + + io.BackendRendererName = null; + io.BackendRendererUserData = null; + io.BackendFlags &= ~(ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasTextures); + platformIO.RendererTextureMaxWidth = 0; + platformIO.RendererTextureMaxHeight = 0; + platformIO.RendererRenderState = null; + platformIO.RendererCreateWindow = null; + platformIO.RendererDestroyWindow = null; + platformIO.RendererSetWindowSize = null; + platformIO.RendererRenderWindow = null; + platformIO.RendererSwapBuffers = null; + } + + public static void NewFrame() { } + + private static void RendererCreateWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); + if (vd == null || vd.Window == nint.Zero) + return; + + if (vd.Renderer == nint.Zero) + { + vd.Renderer = SDL.CreateRenderer(vd.Window, (string?)null); + vd.RendererOwned = true; + } + Program.Logger.Log(null); + } + + private static void RendererDestroyWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); + if (vd == null) + return; + + if (vd.RendererOwned && vd.Renderer != nint.Zero) + { + SDL.DestroyRenderer(vd.Renderer); + } + + vd.Renderer = nint.Zero; + vd.RendererOwned = false; + Program.Logger.Log(null); + } + + private static void RendererSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) + { + // SDL renderer windows track size through the platform window callback. + Program.Logger.Log(null); + } + + private static void RendererRenderWindow(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); + if (vd == null || vd.Renderer == nint.Zero) + { + return; + } + + RenderDrawData(viewport.DrawData, vd.Renderer); + Program.Logger.Log(null); + } + + private static void RendererSwapBuffers(ImGuiViewportPtr viewport) + { + Program.Logger.Log(null); + ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); + if (vd == null || vd.Renderer == nint.Zero) + return; + + SDL.RenderPresent(vd.Renderer); + Program.Logger.Log(null); + } + + public static void RenderDrawData(ImDrawDataPtr drawData, nint renderer) + { + // Skip if no data to render + if (drawData.Handle == null || drawData.CmdListsCount == 0) + return; + + SDL.GetRenderScale(renderer, out float renderScaleX, out float renderScaleY); + Vector2 renderScale = new Vector2( + renderScaleX == 1.0f ? drawData.FramebufferScale.X : 1.0f, + renderScaleY == 1.0f ? drawData.FramebufferScale.Y : 1.0f); + + int fbWidth = (int)(drawData.DisplaySize.X * renderScale.X); + int fbHeight = (int)(drawData.DisplaySize.Y * renderScale.Y); + if (fbWidth <= 0 || fbHeight <= 0) + return; + + for (int i = 0; i < drawData.Textures.Size; i++) + { + var texture = drawData.Textures[i]; + UpdateTexture(texture, renderer); + } + + // Backup SDL renderer state + BackupSDLRendererState old = new BackupSDLRendererState + { + ViewportEnabled = SDL.RenderViewportSet(renderer), + ClipEnabled = SDL.RenderClipEnabled(renderer) + }; + SDL.GetRenderViewport(renderer, out var oldViewport); + old.Viewport = oldViewport; + SDL.GetRenderClipRect(renderer, out var oldClipRect); + old.ClipRect = oldClipRect; + + // Set up render state + SDL.SetRenderViewport(renderer, 0); + SDL.SetRenderClipRect(renderer, nint.Zero); + + // Set render state in platform IO + ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); + platformIo.RendererRenderState = (void*)renderer; + + Vector2 clipOffset = drawData.DisplayPos; + + // Render command lists + for (int n = 0; n < drawData.CmdListsCount; n++) + { + ImDrawListPtr cmdList = drawData.CmdLists[n]; + + for (int cmdIndex = 0; cmdIndex < cmdList.CmdBuffer.Size; cmdIndex++) + { + ImDrawCmd cmd = cmdList.CmdBuffer[cmdIndex]; + + if (cmd.UserCallback != null) + { + continue; // User callback not implemented + } + else + { + // Apply clipping rectangle + Vector4 clipRect = cmd.ClipRect; + Vector2 clipMin = new Vector2((clipRect.X - clipOffset.X) * renderScale.X, (clipRect.Y - clipOffset.Y) * renderScale.Y); + Vector2 clipMax = new Vector2((clipRect.Z - clipOffset.X) * renderScale.X, (clipRect.W - clipOffset.Y) * renderScale.Y); + + clipMin.X = Math.Max(0, clipMin.X); + clipMin.Y = Math.Max(0, clipMin.Y); + clipMax.X = Math.Min(fbWidth, clipMax.X); + clipMax.Y = Math.Min(fbHeight, clipMax.Y); + if (clipMax.X <= clipMin.X || clipMax.Y <= clipMin.Y) + continue; + + SDL.Rect r = new SDL.Rect + { + X = (int)clipMin.X, + Y = (int)clipMin.Y, + W = (int)(clipMax.X - clipMin.X), + H = (int)(clipMax.Y - clipMin.Y) + }; + SDL.SetRenderClipRect(renderer, r); + + // Get texture + nint texId = ResolveTextureId(cmd.GetTexID(), renderer); + + // Convert ImGui vertices to SDL vertices + if (!RenderDrawCommand(cmdList, cmd, renderer, texId, renderScale)) + { + Console.WriteLine($"Failed to render ImGui draw command: {SDL.GetError()}"); + } + } + } + } + + // Reset render state + platformIo.RendererRenderState = null; + + // Restore renderer state + SDL.SetRenderViewport(renderer, old.ViewportEnabled ? old.Viewport : new SDL.Rect()); + SDL.SetRenderClipRect(renderer, old.ClipEnabled ? old.ClipRect : new SDL.Rect()); + } + + private static nint ResolveTextureId(ImTextureID texId, nint renderer) + { + if (texId == ImTextureID.Null) + return nint.Zero; + + TextureState? state = ImGuiUserData.Get((void*)(nint)texId); + if (state == null) + return (nint)texId; + + if (!state.RendererTextures.TryGetValue(renderer, out nint rendererTexture) || rendererTexture == nint.Zero) + { + rendererTexture = CreateRendererTexture(state, renderer); + state.RendererTextures[renderer] = rendererTexture; + } + + return rendererTexture; + } + + private static nint CreateRendererTexture(TextureState state, nint renderer) + { + ImTextureDataPtr tex = state.Source; + // We keep ARGB8888 here because this project already relies on that upload path. + nint sdlTexture = SDL.CreateTexture(renderer, SDL.PixelFormat.ARGB8888, SDL.TextureAccess.Static, tex.Width, tex.Height); + if (sdlTexture == nint.Zero) + return nint.Zero; + + SDL.UpdateTexture(sdlTexture, nint.Zero, (nint)tex.GetPixels(), tex.GetPitch()); + SDL.SetTextureBlendMode(sdlTexture, SDL.BlendMode.Blend); + SDL.SetTextureScaleMode(sdlTexture, SDL.ScaleMode.Linear); + return sdlTexture; + } + + private static void UploadRendererTexture(ImTextureDataPtr tex, nint renderer, nint sdlTexture) + { + if (tex.Status == ImTextureStatus.WantUpdates) + { + for (int i = 0; i < tex.Updates.Size; i++) + { + var r = tex.Updates[i]; + SDL.Rect rect = new SDL.Rect + { + X = r.X, + Y = r.Y, + W = r.W, + H = r.H + }; + SDL.UpdateTexture(sdlTexture, rect, (nint)tex.GetPixelsAt(r.X, r.Y), tex.GetPitch()); + } + } + else + { + SDL.UpdateTexture(sdlTexture, nint.Zero, (nint)tex.GetPixels(), tex.GetPitch()); + } + } + + private static void UpdateTexture(ImTextureDataPtr tex, nint renderer) + { + TextureState? state = null; + if ((nint)tex.BackendUserData != nint.Zero) + state = ImGuiUserData.Get(tex.BackendUserData); + + if (state == null) + { + state = new TextureState(); + tex.BackendUserData = ImGuiUserData.Store(state); + } + + state.Source = tex; + tex.SetTexID((nint)tex.BackendUserData); + + if (tex.Status == ImTextureStatus.WantDestroy) + { + foreach (nint rendererTexture in state.RendererTextures.Values) + { + if (rendererTexture != nint.Zero) + SDL.DestroyTexture(rendererTexture); + } + + state.RendererTextures.Clear(); + ImGuiUserData.Free(tex.BackendUserData); + tex.BackendUserData = (void*)nint.Zero; + tex.SetTexID(ImTextureID.Null); + tex.SetStatus(ImTextureStatus.Destroyed); + return; + } + + bool hasRendererTexture = state.RendererTextures.TryGetValue(renderer, out nint sdlTexture) && sdlTexture != nint.Zero; + if (!hasRendererTexture) + { + sdlTexture = CreateRendererTexture(state, renderer); + state.RendererTextures[renderer] = sdlTexture; + } + + if (sdlTexture != nint.Zero && (tex.Status == ImTextureStatus.WantCreate || tex.Status == ImTextureStatus.WantUpdates || !hasRendererTexture)) + { + UploadRendererTexture(tex, renderer, sdlTexture); + tex.SetStatus(ImTextureStatus.Ok); + } + + } + + private static bool RenderDrawCommand(ImDrawListPtr drawList, ImDrawCmd cmd, nint renderer, nint texId, Vector2 scale) + { + uint indexOffset = cmd.IdxOffset; + uint vertexOffset = cmd.VtxOffset; + uint elemCount = cmd.ElemCount; + + SDL.Vertex[] vertices = new SDL.Vertex[elemCount]; + int[] indices = new int[elemCount]; + + for (int i = 0; i < elemCount; i++) + { + ushort idx = drawList.IdxBuffer[(int)indexOffset + i]; + int vertIdx = (int)(vertexOffset + idx); + + ImDrawVert srcVert = drawList.VtxBuffer[vertIdx]; + + uint col = srcVert.Col; + + byte r = (byte)((col >> 0) & 0xFF); + byte g = (byte)((col >> 8) & 0xFF); + byte b = (byte)((col >> 16) & 0xFF); + byte a = (byte)((col >> 24) & 0xFF); + + vertices[i] = new SDL.Vertex() + { + Position = new SDL.FPoint() + { + X = srcVert.Pos.X * scale.X, + Y = srcVert.Pos.Y * scale.Y + }, + Color = new SDL.FColor() + { + R = r / 255f, + G = g / 255f, + B = b / 255f, + A = a / 255f + }, + TexCoord = new SDL.FPoint() + { + X = srcVert.Uv.X, + Y = srcVert.Uv.Y + } + }; + + indices[i] = i; + } + + return SDL.RenderGeometry(renderer, texId, vertices, vertices.Length, indices, indices.Length); + } + + public static void CreateDeviceObjects() { } + + public static void DestroyDeviceObjects() + { + var texures = ImGui.GetPlatformIO().Textures; + for (int i = 0; i < texures.Size; i++) + { + var texture = texures[i]; + texture.Status = ImTextureStatus.WantDestroy; + UpdateTexture(texture, nint.Zero); + } + } +} diff --git a/SDL3/SDL3Window.cs b/SDL3/SDL3Window.cs new file mode 100644 index 0000000..57dc562 --- /dev/null +++ b/SDL3/SDL3Window.cs @@ -0,0 +1,221 @@ +using System; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using Hexa.NET.ImGui; +using Hexa.NET.ImGui.Utilities; +using Hexa.NET.ImGuizmo; +using Hexa.NET.ImPlot; +using Hexa.NET.ImPlot3D; +using SDL3; + +namespace SDL3_TestingSuite.SDL3; + +public sealed unsafe class SDL3Window : IDisposable +{ + public readonly nint Window; + public readonly nint Renderer; + + internal Action? RenderCallback { get; set; } + + public Vector4 ClearColor = new Vector4(0.06f, 0.05882353f, 0.05882353f, 1f); + + private readonly Stopwatch _timer = Stopwatch.StartNew(); + private TimeSpan _time = TimeSpan.Zero; + + private SDL.Rect _screenClipRect; + private ImGuiContextPtr _imGuiContext; + + public bool Disposed => _disposed; + private bool _disposed; + + private FileSystemWatcher? _watcher; + + public SDL3Window(string name, int posX, int posY, int width, int height, SDL.WindowFlags flags = SDL.WindowFlags.Resizable | SDL.WindowFlags.HighPixelDensity) + { + if (!SDL.Init(SDL.InitFlags.Events | SDL.InitFlags.Video | SDL.InitFlags.Gamepad)) + throw new Exception($"SDL_Init failed: {SDL.GetError()}"); + + // Create window & renderer + if (!SDL.CreateWindowAndRenderer(name, width, height, flags, out Window, out Renderer)) + throw new Exception($"SDL_CreateWindowAndRenderer failed: {SDL.GetError()}"); + + SDL.SetWindowPosition(Window, posX, posY); + SDL.SetRenderVSync(Renderer, 1); + SDL.ShowWindow(Window); + + // Create ImGui context + var context = ImGui.CreateContext(); + ImPlot.CreateContext(); + ImPlot.SetImGuiContext(context); + ImGuizmo.SetImGuiContext(context); + // ImPlot3D.SetImGuiContext(context); + // ImPlot3D.CreateContext(); + + ImGuiIOPtr io = ImGui.GetIO(); + io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.NavEnableGamepad | ImGuiConfigFlags.DockingEnable; + // io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines; + + io.Fonts.AddFontDefault(); + + try + { + string fontsPath = Path.Combine(AppContext.BaseDirectory, "Fonts"); + if (!Path.Exists(fontsPath)) Directory.CreateDirectory(fontsPath); + + _watcher = new FileSystemWatcher(fontsPath); + _watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime; + _watcher.Created += (a, b) => + { + if (!File.Exists(b.FullPath)) return; + + if (Path.GetExtension(b.FullPath) != ".ttf") return; + + ImGui.GetIO().AddFont(b.FullPath); + }; + _watcher.Deleted += (a, b) => + { + if (Path.GetExtension(b.FullPath) != ".ttf") return; + + ImGui.GetIO().RemoveFont(b.Name); + }; + + _watcher.IncludeSubdirectories = false; + _watcher.EnableRaisingEvents = true; + + string[] fonts = Directory.GetFiles(fontsPath, "*.ttf", SearchOption.AllDirectories); + foreach (string font in fonts) + { + io.AddFont(font); + } + } + catch (Exception e) + { + Console.WriteLine(e); + } + + // Init platform and renderer + ImGuiSDL3Platform.Init(Window, Renderer); + ImGuiSDL3Renderer.Init(Renderer); + } + + public void Run() + { + while (!_disposed) + { + ImGui.GetIO().DeltaTime = (float)(_timer.Elapsed - _time).TotalSeconds; + _time = _timer.Elapsed; + + PollEvents(); + + Update(); + + RenderCallback?.Invoke(); + + Render(); + + if (ShouldClose) + { + Dispose(); + break; + } + } + + if (!_disposed) + Dispose(); + } + + public bool ShouldClose; + + private void PollEvents() + { + if (ImGui.GetIO().WantTextInput && !SDL.TextInputActive(Window)) + SDL.StartTextInput(Window); + else if (!ImGui.GetIO().WantTextInput && SDL.TextInputActive(Window)) + SDL.StopTextInput(Window); + + while (SDL.PollEvent(out SDL.Event ev)) + { + ImGuiSDL3Platform.ProcessEvent(ev); + + switch ((SDL.EventType)ev.Type) + { + case SDL.EventType.Terminating: + case SDL.EventType.WindowCloseRequested: + case SDL.EventType.Quit: + ShouldClose = true; + break; + } + } + } + + private void Update() + { + ImGuiSDL3Platform.NewFrame(); + ImGuiSDL3Renderer.NewFrame(); + ImGui.NewFrame(); + } + + private void Render() + { + ImGuiIOPtr io = ImGui.GetIO(); + + ImGui.Render(); + SDL.SetRenderScale(Renderer, io.DisplayFramebufferScale.X, io.DisplayFramebufferScale.Y); + SDL.SetRenderDrawColorFloat(Renderer, ClearColor.X, ClearColor.Y, ClearColor.Z, ClearColor.W); + SDL.RenderClear(Renderer); + ImGuiSDL3Renderer.RenderDrawData(ImGui.GetDrawData(), Renderer); + + if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) + { + ImGui.UpdatePlatformWindows(); + ImGui.RenderPlatformWindowsDefault(); + } + + SDL.RenderPresent(Renderer); + } + + ~SDL3Window() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (_disposed) return; + _disposed = true; + + if (disposing) + { + ImGuiSDL3Renderer.Dispose(); + ImGuiSDL3Platform.Dispose(); + + RenderCallback = null; + } + + if (_imGuiContext.Handle != null) + { + ImGui.SetCurrentContext(null); + ImGui.DestroyContext(_imGuiContext); + _imGuiContext = null; + } + + if (Renderer != nint.Zero) + { + SDL.DestroyRenderer(Renderer); + } + + if (Window != nint.Zero) + { + SDL.DestroyWindow(Window); + } + } + +} \ No newline at end of file