diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..13e684c
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "tcli": {
+ "version": "0.2.4",
+ "commands": [
+ "tcli"
+ ],
+ "rollForward": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 1ff0c42..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,63 +0,0 @@
-###############################################################################
-# Set default behavior to automatically normalize line endings.
-###############################################################################
-* text=auto
-
-###############################################################################
-# Set default behavior for command prompt diff.
-#
-# This is need for earlier builds of msysgit that does not have it on by
-# default for csharp files.
-# Note: This is only used by command line
-###############################################################################
-#*.cs diff=csharp
-
-###############################################################################
-# Set the merge driver for project and solution files
-#
-# Merging from the command prompt will add diff markers to the files if there
-# are conflicts (Merging from VS is not affected by the settings below, in VS
-# the diff markers are never inserted). Diff markers may cause the following
-# file extensions to fail to load in VS. An alternative would be to treat
-# these files as binary and thus will always conflict and require user
-# intervention with every merge. To do so, just uncomment the entries below
-###############################################################################
-#*.sln merge=binary
-#*.csproj merge=binary
-#*.vbproj merge=binary
-#*.vcxproj merge=binary
-#*.vcproj merge=binary
-#*.dbproj merge=binary
-#*.fsproj merge=binary
-#*.lsproj merge=binary
-#*.wixproj merge=binary
-#*.modelproj merge=binary
-#*.sqlproj merge=binary
-#*.wwaproj merge=binary
-
-###############################################################################
-# behavior for image files
-#
-# image files are treated as binary by default.
-###############################################################################
-#*.jpg binary
-#*.png binary
-#*.gif binary
-
-###############################################################################
-# diff behavior for common document formats
-#
-# Convert binary document formats to text before diffing them. This feature
-# is only available from the command line. Turn it on by uncommenting the
-# entries below.
-###############################################################################
-#*.doc diff=astextplain
-#*.DOC diff=astextplain
-#*.docx diff=astextplain
-#*.DOCX diff=astextplain
-#*.dot diff=astextplain
-#*.DOT diff=astextplain
-#*.pdf diff=astextplain
-#*.PDF diff=astextplain
-#*.rtf diff=astextplain
-#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
index 8dd4607..f303b72 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,10 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
-## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
# User-specific files
*.rsuser
@@ -57,11 +60,14 @@ dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
-# .NET Core
+# .NET
project.lock.json
project.fragment.lock.json
artifacts/
+# Tye
+.tye/
+
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
@@ -82,6 +88,8 @@ StyleCopReport.xml
*.pgc
*.pgd
*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
*.sbr
*.tlb
*.tli
@@ -395,4 +403,84 @@ FodyWeavers.xsd
*.msp
# JetBrains Rider
-*.sln.iml
\ No newline at end of file
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
+
+# Thunderstore packaging
+dist/
+build/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..736d0fc
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 2.0.0
+
+Initial BepisLoader Release
\ No newline at end of file
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..a16d670
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+ build
+ publish
+
+
+
+
+
+
diff --git a/LICENSE.txt b/LICENSE
similarity index 96%
rename from LICENSE.txt
rename to LICENSE
index ed25c81..553e5a7 100644
--- a/LICENSE.txt
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT No Attribution
-Copyright 2023 art0007i
+Copyright (c) 2025 art0007i
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Plugin/ImGuiInstance.cs b/Plugin/ImGuiInstance.cs
new file mode 100644
index 0000000..dc5d272
--- /dev/null
+++ b/Plugin/ImGuiInstance.cs
@@ -0,0 +1,147 @@
+using BepInEx.Configuration;
+using Elements.Core;
+using ResoniteImGuiLib.SDL3;
+using SDL3;
+using System.Collections.Concurrent;
+
+namespace ResoniteImGuiLib;
+
+public class ImGuiInstance
+{
+ private ImGuiInstance(string name)
+ {
+ _name = name;
+ _entry = Plugin.Config.Bind("Windows", "Rect_" + Name, new int4(200, 200, 400, 300));
+ }
+
+ public string Name => _name;
+ public event Action? Layout;
+
+ private string _name;
+ private ConfigEntry _entry;
+
+ private static bool IsSDL3Running => _sdl3Thread is { IsAlive: true };
+ private static Thread? _sdl3Thread;
+ private static Dictionary GuiInstances = new();
+ internal static ConcurrentQueue newInstances = new();
+ internal static ConcurrentQueue<(string, Action)> windowRequests = new();
+
+ ///
+ /// Gets a reference to the SDL3Window inside of a callback.
+ /// The callback will be called inside the SDL3 Thread.
+ ///
+ ///
+ public void GetImGui(Action callback)
+ {
+ windowRequests.Enqueue((Name, callback));
+ }
+
+ ///
+ /// Creates a new ImGuiInstance with the "global" name or returns it if it already exists.
+ ///
+ /// Callback for when the ImGui instance is ready. You can put initialization logic here.
+ ///
+ public static ImGuiInstance GetOrCreate(ImGuiReady onReady) => GetOrCreate("global", onReady);
+
+
+ ///
+ /// Creates a new ImGuiInstance or returns one if it already exists.
+ ///
+ /// Callback for when the ImGui instance is ready. You can put initialization logic here.
+ ///
+ public static ImGuiInstance GetOrCreate(string name = "global", ImGuiReady? onReady = null)
+ {
+ if (GuiInstances.TryGetValue(name, out var instance))
+ {
+ if (onReady != null)
+ instance.GetImGui((gui) => onReady(gui, false));
+ return instance;
+ }
+
+ instance = new ImGuiInstance(name);
+ GuiInstances.Add(name, instance);
+ if (onReady != null)
+ instance.GetImGui((gui) => onReady(gui, true));
+
+ newInstances.Enqueue(instance);
+ TryStartSDL3Thread();
+
+ return instance;
+ }
+
+ private static void TryStartSDL3Thread()
+ {
+ if (IsSDL3Running)
+ return;
+
+ _sdl3Thread = new Thread(() => RunSDL3())
+ {
+ Name = $"Resonite ImGui SDL3",
+ Priority = ThreadPriority.Highest,
+ IsBackground = false
+ };
+
+ _sdl3Thread.Start();
+ }
+ static Dictionary windows = new();
+ static Dictionary windowsById = new();
+ private static void RunSDL3()
+ {
+ try
+ {
+ while (true)
+ {
+ while (newInstances.TryDequeue(out var request))
+ {
+ SDL3Window app = new SDL3Window("ImGuiContext: " + request.Name, request._entry.Value.x, request._entry.Value.y, request._entry.Value.z, request._entry.Value.w);
+ app.WindowRectModified += (rect) => request._entry.Value = rect;
+ // TODO: maybe listen to config changes to resize window
+ // request._entry.SettingChanged += (_, _) => { };
+ app.RenderCallback = request.Layout!;
+ windows[request.Name] = app;
+ var winId = SDL.GetWindowID(app.Window);
+ if (!windowsById.TryAdd(winId, app))
+ {
+ Plugin.Log.LogError($"Failed to add window with id {(uint) app.Window}, name {request.Name}!");
+ }
+ }
+ for (int i = 0; i < windowRequests.Count; i++)
+ {
+ if (windowRequests.TryDequeue(out var callback))
+ {
+ if (windows.TryGetValue(callback.Item1, out var window))
+ {
+ callback.Item2?.Invoke(window);
+ }
+ else
+ {
+ windowRequests.Enqueue(callback);
+ }
+ }
+ }
+
+ while (SDL.PollEvent(out SDL.Event ev))
+ {
+ if (windowsById.TryGetValue(ev.Window.WindowID, out var window))
+ {
+ window.EventQueue.Enqueue(ev);
+ }
+ }
+
+ foreach (var (key, window) in windows)
+ {
+ window.RunOneFrame();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Plugin.Log.LogError($"SDL3 Thread crashed: {e}");
+ }
+
+ _sdl3Thread?.Interrupt();
+ _sdl3Thread = null;
+ }
+}
+
+public delegate void ImGuiReady(SDL3Window imGui, bool isNewInstance);
\ No newline at end of file
diff --git a/Plugin/Plugin.cs b/Plugin/Plugin.cs
new file mode 100644
index 0000000..a4f22c9
--- /dev/null
+++ b/Plugin/Plugin.cs
@@ -0,0 +1,49 @@
+using BepInEx;
+using BepInEx.Configuration;
+using BepInEx.Logging;
+using BepInEx.NET.Common;
+using BepInExResoniteShim;
+using Elements.Core;
+using FrooxEngine;
+
+namespace ResoniteImGuiLib;
+
+public static class ImGuiLib
+{
+ public static ImGuiInstance GetOrCreateInstance(ImGuiReady onReady)
+ {
+ return GetOrCreateInstance("global", onReady);
+ }
+ public static ImGuiInstance GetOrCreateInstance(string name = "global", ImGuiReady? onReady = null)
+ {
+ return ImGuiInstance.GetOrCreate(name, onReady);
+ }
+}
+
+[ResonitePlugin(PluginMetadata.GUID, PluginMetadata.NAME, PluginMetadata.VERSION, PluginMetadata.AUTHORS, PluginMetadata.REPOSITORY_URL)]
+[BepInDependency(BepInExResoniteShim.PluginMetadata.GUID, BepInDependency.DependencyFlags.HardDependency)]
+public class Plugin : BasePlugin
+{
+#nullable disable
+ internal static new ManualLogSource Log;
+ internal static new ConfigFile Config;
+
+ internal static ConfigEntry DefaultBackgroundColor;
+#nullable enable
+
+ public static readonly CancellationTokenSource CancellationToken = new CancellationTokenSource();
+
+ public override void Load()
+ {
+ Log = base.Log;
+ Config = base.Config;
+
+ DefaultBackgroundColor = Config.Bind("General", "DefaultBackgroundColor", new color(0.45f, 0.55f, 0.60f, 1.00f), "Default background color that new ImGui windows will have.");
+
+ BepisResoniteWrapper.ResoniteHooks.OnEngineReady += () =>
+ {
+ Engine.Current.OnShutdown += () => CancellationToken.Cancel();
+ Engine.Current.OnShutdownRequest += _ => CancellationToken.Cancel();
+ };
+ }
+}
diff --git a/Plugin/Properties/launchSettings.json b/Plugin/Properties/launchSettings.json
new file mode 100644
index 0000000..b7c743c
--- /dev/null
+++ b/Plugin/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Launch": {
+ "commandName": "Executable",
+ "executablePath": "$(GamePath)Renderite.Host.exe",
+ "commandLineArgs": "-Screen $(ResoniteLaunchArguments)",
+ "workingDirectory": "$(GamePath)"
+ }
+ }
+}
diff --git a/Plugin/ResoniteImGuiLib.csproj b/Plugin/ResoniteImGuiLib.csproj
new file mode 100644
index 0000000..13c02e3
--- /dev/null
+++ b/Plugin/ResoniteImGuiLib.csproj
@@ -0,0 +1,70 @@
+
+
+
+ 2.0.0
+ art0007i
+ net9.0
+ https://github.com/art0007i/ResoniteImGuiLib
+ art0007i.ResoniteImGuiLib
+ ResoniteImGuiLib
+ ResoniteImGuiLib
+ enable
+ enable
+ true
+ false
+ false
+ true
+ $(ResonitePath)/
+ $(MSBuildProgramFiles32)\Steam\steamapps\common\Resonite\
+ $(HOME)/.steam/steam/steamapps/common/Resonite/
+ $(GamePath)BepInEx\plugins\$(AssemblyName)
+
+ https://nuget-modding.resonite.net/v3/index.json;
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(GamePath)FrooxEngine.dll
+ False
+
+
+ $(GamePath)Elements.Core.dll
+ False
+
+
+ $(GamePath)Renderite.Shared.dll
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugin/SDL3/ImGuiSDL3.cs b/Plugin/SDL3/ImGuiSDL3.cs
new file mode 100644
index 0000000..ef0e334
--- /dev/null
+++ b/Plugin/SDL3/ImGuiSDL3.cs
@@ -0,0 +1,427 @@
+using ImGuiNET;
+using SDL3;
+
+namespace ResoniteImGuiLib.SDL3;
+
+///
+/// Implementation of SDL3 platform backend for ImGui.
+/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdl3.h
+///
+public class ImGuiSDL3 : IDisposable
+{
+ public readonly IntPtr Window;
+ public readonly IntPtr Renderer;
+ public readonly uint WindowId;
+ private uint _mouseWindowId;
+ private int _mousePendingLeaveFrame;
+ private IntPtr[] _mouseCursors = new IntPtr[(int)ImGuiMouseCursor.COUNT];
+ private IntPtr _mouseLastCursor = -1;
+ private int _mouseButtonsDown;
+
+ // I don't think we actually need these since it works just fine without them
+ //
+ // [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ // public delegate IntPtr Platform_GetClipboardTextFn(IntPtr ctx);
+ //
+ // [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ // public delegate void Platform_SetClipboardTextFn(IntPtr ctx, IntPtr text);
+ //
+ // private static Platform_GetClipboardTextFn getClipboardDelegate = GetClipboardText;
+ // private static Platform_SetClipboardTextFn setClipboardDelegate = SetClipboardText;
+ //
+ // private static IntPtr getClipboardPtr;
+ // private static IntPtr setClipboardPtr;
+ //
+ // private static IntPtr clipboardTextPtr = IntPtr.Zero;
+
+ public ImGuiSDL3(IntPtr window, IntPtr renderer)
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
+ io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos;
+ Window = window;
+ WindowId = SDL.GetWindowID(Window);
+ Renderer = renderer;
+
+ // I don't think we actually need these since it works just fine without them
+ //
+ // ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO();
+ // platformIo.Platform_SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(setClipboardDelegate);
+ // platformIo.Platform_GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(getClipboardDelegate);
+ // getClipboardPtr = platformIo.Platform_GetClipboardTextFn;
+ // setClipboardPtr = platformIo.Platform_SetClipboardTextFn;
+
+ _mouseCursors[(int)ImGuiMouseCursor.Arrow] = SDL.CreateSystemCursor(SDL.SystemCursor.Default);
+ _mouseCursors[(int)ImGuiMouseCursor.TextInput] = SDL.CreateSystemCursor(SDL.SystemCursor.Text);
+ _mouseCursors[(int)ImGuiMouseCursor.ResizeAll] = SDL.CreateSystemCursor(SDL.SystemCursor.Move);
+ _mouseCursors[(int)ImGuiMouseCursor.ResizeNS] = SDL.CreateSystemCursor(SDL.SystemCursor.NSResize);
+ _mouseCursors[(int)ImGuiMouseCursor.ResizeEW] = SDL.CreateSystemCursor(SDL.SystemCursor.EWResize);
+ _mouseCursors[(int)ImGuiMouseCursor.ResizeNESW] = SDL.CreateSystemCursor(SDL.SystemCursor.NESWResize);
+ _mouseCursors[(int)ImGuiMouseCursor.ResizeNWSE] = SDL.CreateSystemCursor(SDL.SystemCursor.NWSEResize);
+ _mouseCursors[(int)ImGuiMouseCursor.Hand] = SDL.CreateSystemCursor(SDL.SystemCursor.Pointer);
+ _mouseCursors[(int)ImGuiMouseCursor.NotAllowed] = SDL.CreateSystemCursor(SDL.SystemCursor.NotAllowed);
+
+ ImGuiViewportPtr viewport = ImGui.GetMainViewport();
+ SetupPlatformHandles(viewport, window);
+ }
+
+ public void Dispose()
+ {
+ foreach (IntPtr cursor in _mouseCursors)
+ SDL.DestroyCursor(cursor);
+ }
+
+ public void NewFrame()
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ SDL.GetWindowSize(Window, out int w, out int h);
+ if (SDL.GetWindowFlags(Window).HasFlag(SDL.WindowFlags.Minimized))
+ {
+ w = h = 0;
+ }
+
+ SDL.GetWindowSizeInPixels(Window, out int displayW, out int displayH);
+ io.DisplaySize = new System.Numerics.Vector2(w, h);
+
+ if (w > 0 && h > 0)
+ io.DisplayFramebufferScale = new System.Numerics.Vector2((float)displayW / w, (float)displayH / h);
+
+ if (_mousePendingLeaveFrame > 0 && _mousePendingLeaveFrame >= ImGui.GetFrameCount())
+ {
+ _mouseWindowId = 0;
+ _mousePendingLeaveFrame = 0;
+ io.AddMousePosEvent(-float.MaxValue, -float.MaxValue);
+ }
+
+ UpdateMouseData();
+ UpdateMouseCursor();
+ }
+
+ public unsafe bool ProcessEvent(SDL.Event e)
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ 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);
+ _mouseButtonsDown = (SDL.EventType)e.Type == SDL.EventType.MouseButtonDown ? _mouseButtonsDown | 1 << mouseButton : _mouseButtonsDown & ~(1 << mouseButton);
+ return true;
+ case SDL.EventType.TextInput:
+ if (GetViewportForWindowId(e.Text.WindowID) == null)
+ return false;
+
+ ImGuiNative.ImGuiIO_AddInputCharactersUTF8(io, (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;
+
+ _mouseWindowId = e.Window.WindowID;
+ _mousePendingLeaveFrame = 0;
+ return true;
+ case SDL.EventType.WindowMouseLeave:
+ if (GetViewportForWindowId(e.Window.WindowID) == null)
+ return false;
+
+ _mousePendingLeaveFrame = 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;
+ default:
+ break;
+ }
+
+ return false;
+ }
+
+ private void UpdateMouseData()
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ IntPtr focusedWindow = SDL.GetKeyboardFocus();
+ bool isAppFocused = focusedWindow == Window;
+ if (isAppFocused)
+ {
+ if (io.WantSetMousePos)
+ {
+ SDL.WarpMouseInWindow(Window, (int)io.MousePos.X, (int)io.MousePos.Y);
+ }
+ }
+ }
+
+ private void UpdateMouseCursor()
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+ if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) != 0)
+ return;
+
+ ImGuiMouseCursor imguiCursor = ImGui.GetMouseCursor();
+
+ if (io.MouseDrawCursor || imguiCursor == ImGuiMouseCursor.None)
+ {
+ SDL.HideCursor();
+ }
+ else
+ {
+ IntPtr expectedCursor = _mouseCursors[(int)imguiCursor];
+ if (_mouseLastCursor != expectedCursor)
+ {
+ SDL.SetCursor(expectedCursor);
+ _mouseLastCursor = expectedCursor;
+ }
+
+ SDL.ShowCursor();
+ }
+ }
+
+ private ImGuiKey KeyEventToImGui(SDL.Keycode keycoade, SDL.Scancode scancode)
+ {
+ switch (scancode)
+ {
+ case SDL.Scancode.Kp0: return ImGuiKey.Keypad0;
+ case SDL.Scancode.Kp1: return ImGuiKey.Keypad1;
+ case SDL.Scancode.Kp2: return ImGuiKey.Keypad2;
+ case SDL.Scancode.Kp3: return ImGuiKey.Keypad3;
+ case SDL.Scancode.Kp4: return ImGuiKey.Keypad4;
+ case SDL.Scancode.Kp5: return ImGuiKey.Keypad5;
+ case SDL.Scancode.Kp6: return ImGuiKey.Keypad6;
+ case SDL.Scancode.Kp7: return ImGuiKey.Keypad7;
+ case SDL.Scancode.Kp8: return ImGuiKey.Keypad8;
+ case SDL.Scancode.Kp9: return ImGuiKey.Keypad9;
+ case SDL.Scancode.KpPeriod: return ImGuiKey.KeypadDecimal;
+ case SDL.Scancode.KpDivide: return ImGuiKey.KeypadDivide;
+ case SDL.Scancode.KpMultiply: return ImGuiKey.KeypadMultiply;
+ case SDL.Scancode.KpMinus: return ImGuiKey.KeypadSubtract;
+ case SDL.Scancode.KpPlus: return ImGuiKey.KeypadAdd;
+ case SDL.Scancode.KpEnter: return ImGuiKey.KeypadEnter;
+ case SDL.Scancode.KpEquals: return ImGuiKey.KeypadEqual;
+ default: break;
+ }
+
+ switch (keycoade)
+ {
+ case SDL.Keycode.Tab: return ImGuiKey.Tab;
+ case SDL.Keycode.Left: return ImGuiKey.LeftArrow;
+ case SDL.Keycode.Right: return ImGuiKey.RightArrow;
+ case SDL.Keycode.Up: return ImGuiKey.UpArrow;
+ case SDL.Keycode.Down: return ImGuiKey.DownArrow;
+ case SDL.Keycode.Pageup: return ImGuiKey.PageUp;
+ case SDL.Keycode.Pagedown: return ImGuiKey.PageDown;
+ case SDL.Keycode.Home: return ImGuiKey.Home;
+ case SDL.Keycode.End: return ImGuiKey.End;
+ case SDL.Keycode.Insert: return ImGuiKey.Insert;
+ case SDL.Keycode.Delete: return ImGuiKey.Delete;
+ case SDL.Keycode.Backspace: return ImGuiKey.Backspace;
+ case SDL.Keycode.Space: return ImGuiKey.Space;
+ case SDL.Keycode.Return: return ImGuiKey.Enter;
+ case SDL.Keycode.Escape: return ImGuiKey.Escape;
+ case SDL.Keycode.Apostrophe: return ImGuiKey.Apostrophe;
+ case SDL.Keycode.Comma: return ImGuiKey.Comma;
+ case SDL.Keycode.Minus: return ImGuiKey.Minus;
+ case SDL.Keycode.Period: return ImGuiKey.Period;
+ case SDL.Keycode.Slash: return ImGuiKey.Slash;
+ case SDL.Keycode.Semicolon: return ImGuiKey.Semicolon;
+ case SDL.Keycode.Equals: return ImGuiKey.Equal;
+ case SDL.Keycode.LeftBracket: return ImGuiKey.LeftBracket;
+ case SDL.Keycode.Backslash: return ImGuiKey.Backslash;
+ case SDL.Keycode.RightBracket: return ImGuiKey.RightBracket;
+ case SDL.Keycode.Grave: return ImGuiKey.GraveAccent;
+ case SDL.Keycode.Capslock: return ImGuiKey.CapsLock;
+ case SDL.Keycode.ScrollLock: return ImGuiKey.ScrollLock;
+ case SDL.Keycode.NumLockClear: return ImGuiKey.NumLock;
+ case SDL.Keycode.PrintScreen: return ImGuiKey.PrintScreen;
+ case SDL.Keycode.Pause: return ImGuiKey.Pause;
+ case SDL.Keycode.LCtrl: return ImGuiKey.LeftCtrl;
+ case SDL.Keycode.LShift: return ImGuiKey.LeftShift;
+ case SDL.Keycode.LAlt: return ImGuiKey.LeftAlt;
+ case SDL.Keycode.LGui: return ImGuiKey.LeftSuper;
+ case SDL.Keycode.RCtrl: return ImGuiKey.RightCtrl;
+ case SDL.Keycode.RShift: return ImGuiKey.RightShift;
+ case SDL.Keycode.RAlt: return ImGuiKey.RightAlt;
+ case SDL.Keycode.RGUI: return ImGuiKey.RightSuper;
+ case SDL.Keycode.Application: return ImGuiKey.Menu;
+ case SDL.Keycode.Alpha0: return ImGuiKey._0;
+ case SDL.Keycode.Alpha1: return ImGuiKey._1;
+ case SDL.Keycode.Alpha2: return ImGuiKey._2;
+ case SDL.Keycode.Alpha3: return ImGuiKey._3;
+ case SDL.Keycode.Alpha4: return ImGuiKey._4;
+ case SDL.Keycode.Alpha5: return ImGuiKey._5;
+ case SDL.Keycode.Alpha6: return ImGuiKey._6;
+ case SDL.Keycode.Alpha7: return ImGuiKey._7;
+ case SDL.Keycode.Alpha8: return ImGuiKey._8;
+ case SDL.Keycode.Alpha9: return ImGuiKey._9;
+ case SDL.Keycode.A: return ImGuiKey.A;
+ case SDL.Keycode.B: return ImGuiKey.B;
+ case SDL.Keycode.C: return ImGuiKey.C;
+ case SDL.Keycode.D: return ImGuiKey.D;
+ case SDL.Keycode.E: return ImGuiKey.E;
+ case SDL.Keycode.F: return ImGuiKey.F;
+ case SDL.Keycode.G: return ImGuiKey.G;
+ case SDL.Keycode.H: return ImGuiKey.H;
+ case SDL.Keycode.I: return ImGuiKey.I;
+ case SDL.Keycode.J: return ImGuiKey.J;
+ case SDL.Keycode.K: return ImGuiKey.K;
+ case SDL.Keycode.L: return ImGuiKey.L;
+ case SDL.Keycode.M: return ImGuiKey.M;
+ case SDL.Keycode.N: return ImGuiKey.N;
+ case SDL.Keycode.O: return ImGuiKey.O;
+ case SDL.Keycode.P: return ImGuiKey.P;
+ case SDL.Keycode.Q: return ImGuiKey.Q;
+ case SDL.Keycode.R: return ImGuiKey.R;
+ case SDL.Keycode.S: return ImGuiKey.S;
+ case SDL.Keycode.T: return ImGuiKey.T;
+ case SDL.Keycode.U: return ImGuiKey.U;
+ case SDL.Keycode.V: return ImGuiKey.V;
+ case SDL.Keycode.W: return ImGuiKey.W;
+ case SDL.Keycode.X: return ImGuiKey.X;
+ case SDL.Keycode.Y: return ImGuiKey.Y;
+ case SDL.Keycode.Z: return ImGuiKey.Z;
+ case SDL.Keycode.F1: return ImGuiKey.F1;
+ case SDL.Keycode.F2: return ImGuiKey.F2;
+ case SDL.Keycode.F3: return ImGuiKey.F3;
+ case SDL.Keycode.F4: return ImGuiKey.F4;
+ case SDL.Keycode.F5: return ImGuiKey.F5;
+ case SDL.Keycode.F6: return ImGuiKey.F6;
+ case SDL.Keycode.F7: return ImGuiKey.F7;
+ case SDL.Keycode.F8: return ImGuiKey.F8;
+ case SDL.Keycode.F9: return ImGuiKey.F9;
+ case SDL.Keycode.F10: return ImGuiKey.F10;
+ case SDL.Keycode.F11: return ImGuiKey.F11;
+ case SDL.Keycode.F12: return ImGuiKey.F12;
+ case SDL.Keycode.F13: return ImGuiKey.F13;
+ case SDL.Keycode.F14: return ImGuiKey.F14;
+ case SDL.Keycode.F15: return ImGuiKey.F15;
+ case SDL.Keycode.F16: return ImGuiKey.F16;
+ case SDL.Keycode.F17: return ImGuiKey.F17;
+ case SDL.Keycode.F18: return ImGuiKey.F18;
+ case SDL.Keycode.F19: return ImGuiKey.F19;
+ case SDL.Keycode.F20: return ImGuiKey.F20;
+ case SDL.Keycode.F21: return ImGuiKey.F21;
+ case SDL.Keycode.F22: return ImGuiKey.F22;
+ case SDL.Keycode.F23: return ImGuiKey.F23;
+ case SDL.Keycode.F24: return ImGuiKey.F24;
+ case SDL.Keycode.AcBack: return ImGuiKey.AppBack;
+ case SDL.Keycode.AcForward: return ImGuiKey.AppForward;
+ default: break;
+ }
+
+ return ImGuiKey.None;
+ }
+
+ public static IntPtr Data() => ImGui.GetIO().BackendPlatformUserData;
+
+ // I don't think we actually need these since it works just fine without them
+ //
+ // public static IntPtr GetClipboardText(IntPtr ctx)
+ // {
+ // if (clipboardTextPtr != IntPtr.Zero)
+ // {
+ // Marshal.FreeHGlobal(clipboardTextPtr);
+ // clipboardTextPtr = IntPtr.Zero;
+ // }
+ //
+ // string text = SDL.GetClipboardText();
+ // clipboardTextPtr = Marshal.StringToHGlobalAnsi(text ?? string.Empty);
+ // return clipboardTextPtr;
+ // }
+ //
+ // public static void SetClipboardText(IntPtr ctx, IntPtr text)
+ // {
+ // string managedText = Marshal.PtrToStringAnsi(text);
+ // if (managedText != null)
+ // {
+ // SDL.SetClipboardText(managedText);
+ // }
+ // }
+
+ // !! 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);
+ }
+
+ public static ImGuiViewportPtr? GetViewportForWindowId(uint id)
+ {
+ ImGuiViewportPtr viewport = ImGui.GetMainViewport();
+ return viewport.ID == id ? ImGui.GetMainViewport() : null;
+ }
+
+ private static void SetupPlatformHandles(ImGuiViewportPtr viewport, IntPtr window)
+ {
+ viewport.PlatformHandle = window;
+ viewport.PlatformHandleRaw = 0;
+#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
+ }
+}
\ No newline at end of file
diff --git a/Plugin/SDL3/ImGuiSDL3Renderer.cs b/Plugin/SDL3/ImGuiSDL3Renderer.cs
new file mode 100644
index 0000000..ffdd958
--- /dev/null
+++ b/Plugin/SDL3/ImGuiSDL3Renderer.cs
@@ -0,0 +1,302 @@
+using System.Numerics;
+using ImGuiNET;
+using SDL3;
+
+namespace ResoniteImGuiLib.SDL3;
+
+///
+/// Implementation of ImGui for SDL3 Renderer backend.
+/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdlrenderer3.h
+///
+public class ImGuiSDL3Renderer : IDisposable
+{
+ struct BackupSDLRendererState
+ {
+ public SDL.Rect Viewport;
+ public bool ViewportEnabled;
+ public bool ClipEnabled;
+ public SDL.Rect ClipRect;
+ }
+
+ public SDL.Rect DefaultClipRect = new SDL.Rect();
+ public readonly IntPtr Renderer;
+ private IntPtr _fontTexture = IntPtr.Zero;
+
+ public ImGuiSDL3Renderer(IntPtr renderer)
+ {
+ Renderer = renderer;
+
+ ImGuiIOPtr io = ImGui.GetIO();
+ io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
+ }
+
+ public void Dispose()
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+ if (_fontTexture != IntPtr.Zero)
+ {
+ io.Fonts.SetTexID(IntPtr.Zero);
+ SDL.DestroyTexture(_fontTexture);
+ _fontTexture = IntPtr.Zero;
+ }
+ }
+
+ public void NewFrame()
+ {
+ if (_fontTexture == IntPtr.Zero)
+ {
+ CreateDeviceObjects();
+ }
+ }
+
+ public void RenderDrawData(ImDrawDataPtr drawData)
+ {
+ // Skip if no data to render
+ unsafe
+ {
+ if (drawData.NativePtr == null || drawData.CmdListsCount == 0)
+ return;
+ }
+
+ // Get display size & framebuffer scale
+ Vector2 renderScale = new Vector2(drawData.FramebufferScale.X, drawData.FramebufferScale.Y);
+
+ int fbWidth = (int)(drawData.DisplaySize.X * renderScale.X);
+ int fbHeight = (int)(drawData.DisplaySize.Y * renderScale.Y);
+ if (fbWidth <= 0 || fbHeight <= 0)
+ return;
+
+ // Backup SDL renderer state
+ BackupSDLRendererState oldState = BackupRendererState();
+
+ // Set up render state
+ SetupRenderState();
+
+ // Set render state in platform IO
+ ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO();
+ platformIo.Renderer_RenderState = 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++)
+ {
+ ImDrawCmdPtr cmd = cmdList.CmdBuffer[cmdIndex];
+
+ if (cmd.UserCallback != IntPtr.Zero)
+ {
+ // User callback not implemented
+ continue;
+ }
+
+ // Apply clipping rectangle
+ Vector4 clipRect = cmd.ClipRect;
+ SDL.Rect r = CalculateClipRect(clipRect, clipOffset, renderScale, fbWidth, fbHeight);
+ SDL.SetRenderClipRect(Renderer, r);
+
+ // Get texture
+ IntPtr texId = cmd.GetTexID();
+
+ // Convert ImGui vertices to SDL vertices
+ if (!RenderDrawCommand(cmdList, cmd, texId, renderScale))
+ {
+ Console.WriteLine($"Failed to render ImGui draw command: {SDL.GetError()}");
+ }
+ }
+ }
+
+ // Reset render state
+ platformIo.Renderer_RenderState = IntPtr.Zero;
+
+ // Restore renderer state
+ RestoreRendererState(oldState);
+ }
+
+ private bool RenderDrawCommand(ImDrawListPtr drawList, ImDrawCmdPtr cmd, IntPtr texId, Vector2 scale)
+ {
+ // Get indices and vertices for this command
+ int indexOffset = (int)cmd.IdxOffset;
+ int vertexOffset = (int)cmd.VtxOffset;
+ int elemCount = (int)cmd.ElemCount;
+
+ // Create SDL vertices just for the vertices used by this command
+ // Determine the vertex range by looking at the indices
+ int minVertexIdx = int.MaxValue;
+ int maxVertexIdx = 0;
+
+ for (int i = 0; i < elemCount; i++)
+ {
+ ushort idx = drawList.IdxBuffer[indexOffset + i];
+ minVertexIdx = Math.Min(minVertexIdx, idx);
+ maxVertexIdx = Math.Max(maxVertexIdx, idx);
+ }
+
+ // Adjust for the vertex offset
+ minVertexIdx += vertexOffset;
+ maxVertexIdx += vertexOffset;
+
+ // Calculate the number of vertices we need
+ int numVertices = maxVertexIdx - minVertexIdx + 1;
+
+ // Create arrays for the vertices and indices we're going to use
+ SDL.Vertex[] vertices = new SDL.Vertex[numVertices];
+ int[] indices = new int[elemCount];
+
+ // Convert only the vertices we need
+ for (int i = 0; i < numVertices; i++)
+ {
+ int vertIdx = minVertexIdx + i;
+ ImDrawVertPtr srcVert = drawList.VtxBuffer[vertIdx];
+
+ // Convert from ImGui ABGR color to SDL RGBA color
+ uint col = srcVert.col;
+ byte r = (byte)(col >> 0 & 0xFF); // Extract R from the least significant byte
+ byte g = (byte)(col >> 8 & 0xFF); // Extract G
+ byte b = (byte)(col >> 16 & 0xFF); // Extract B
+ byte a = (byte)(col >> 24 & 0xFF); // Extract A from the most significant byte
+
+ vertices[i] = new SDL.Vertex
+ {
+ Position = new SDL.FPoint { X = srcVert.pos.X, Y = srcVert.pos.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 }
+ };
+ }
+
+ // Adjust indices to be relative to our new vertex array
+ for (int i = 0; i < elemCount; i++)
+ {
+ ushort originalIdx = drawList.IdxBuffer[indexOffset + i];
+ indices[i] = (ushort)(originalIdx - (minVertexIdx - vertexOffset));
+ }
+
+ // Call your SDL.RenderGeometry wrapper with the managed arrays
+ return SDL.RenderGeometry(
+ Renderer,
+ texId,
+ vertices,
+ numVertices,
+ indices,
+ elemCount
+ );
+ }
+
+ private void SetupRenderState()
+ {
+ SDL.SetRenderViewport(Renderer, 0);
+ SDL.SetRenderClipRect(Renderer, IntPtr.Zero);
+ SDL.SetRenderDrawBlendMode(Renderer, SDL.BlendMode.Blend);
+ }
+
+ private BackupSDLRendererState BackupRendererState()
+ {
+ BackupSDLRendererState state = new BackupSDLRendererState
+ {
+ ViewportEnabled = SDL.RenderViewportSet(Renderer),
+ ClipEnabled = SDL.RenderClipEnabled(Renderer)
+ };
+ SDL.GetRenderViewport(Renderer, out state.Viewport);
+ SDL.GetRenderClipRect(Renderer, out state.ClipRect);
+ return state;
+ }
+
+ private void RestoreRendererState(BackupSDLRendererState state)
+ {
+ if (state.ViewportEnabled)
+ SDL.SetRenderViewport(Renderer, state.Viewport);
+ else
+ SDL.SetRenderViewport(Renderer, IntPtr.Zero);
+
+ if (state.ClipEnabled)
+ SDL.SetRenderClipRect(Renderer, state.ClipRect);
+ else
+ SDL.SetRenderClipRect(Renderer, IntPtr.Zero);
+ }
+
+ private SDL.Rect CalculateClipRect(Vector4 clipRect, Vector2 clipOffset, Vector2 scale, int fbWidth, int fbHeight)
+ {
+ Vector2 clipMin = new Vector2((clipRect.X - clipOffset.X) * scale.X, (clipRect.Y - clipOffset.Y) * scale.Y);
+ Vector2 clipMax = new Vector2((clipRect.Z - clipOffset.X) * scale.X, (clipRect.W - clipOffset.Y) * scale.Y);
+
+ // Clamp to framebuffer bounds
+ 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);
+
+ 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)
+ };
+
+ return r;
+ }
+
+ private unsafe bool CreateDeviceObjects()
+ {
+ ImGuiIOPtr io = ImGui.GetIO();
+
+ io.Fonts.AddFontDefault();
+
+ // TODO: Load custom fonts from "Fonts" directory
+ //string fontsPath = Path.Combine(Plugin.AssemblyDirectory, "Fonts");
+ //if (Path.Exists(fontsPath))
+ //{
+ // string[] fonts = Directory.GetFiles(fontsPath, "*.ttf");
+ // if (fonts.Length > 0)
+ // {
+ // foreach (string font in fonts)
+ // {
+ // io.Fonts.AddFontFromFileTTF(font, 20f);
+ // }
+ // }
+ //}
+
+ // Build texture atlas
+ io.Fonts.GetTexDataAsRGBA32(out byte* pixels, out int width, out int height);
+
+ // Create surface from pixel data
+ IntPtr surface = SDL.CreateSurfaceFrom(width, height, SDL.PixelFormat.RGBA8888, (IntPtr)pixels, width * 4);
+ if (surface == IntPtr.Zero)
+ {
+ SDL.LogError(SDL.LogCategory.Application, $"Failed to create font surface: {SDL.GetError()}");
+ return false;
+ }
+
+ // Create texture
+ _fontTexture = SDL.CreateTextureFromSurface(Renderer, surface);
+ if (_fontTexture == IntPtr.Zero)
+ {
+ SDL.LogError(SDL.LogCategory.Application, $"Failed to create font texture: {SDL.GetError()}");
+ return false;
+ }
+
+ // Update texture directly without converting pixel format
+ if (!SDL.UpdateTexture(_fontTexture, IntPtr.Zero, (IntPtr)pixels, width * 4))
+ {
+ SDL.LogError(SDL.LogCategory.Application, $"Failed to update font texture: {SDL.GetError()}");
+ return false;
+ }
+
+ // Ensure proper blending for font rendering
+ SDL.SetTextureBlendMode(_fontTexture, SDL.BlendMode.Blend);
+
+ // Use nearest neighbor filtering for crisp font rendering at small sizes
+ SDL.SetTextureScaleMode(_fontTexture, SDL.ScaleMode.Linear);
+
+ // Store our identifier
+ io.Fonts.SetTexID(_fontTexture);
+
+ SDL.DestroySurface(surface);
+ io.Fonts.ClearTexData();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Plugin/SDL3/SDL3Window.cs b/Plugin/SDL3/SDL3Window.cs
new file mode 100644
index 0000000..0b61ce0
--- /dev/null
+++ b/Plugin/SDL3/SDL3Window.cs
@@ -0,0 +1,220 @@
+using CodeGenerationConfig;
+using Elements.Core;
+using ImGuiNET;
+using SDL3;
+using System.Diagnostics;
+
+namespace ResoniteImGuiLib.SDL3;
+
+public class SDL3Window : IDisposable
+{
+ public readonly IntPtr Window;
+ public readonly IntPtr Device;
+ public readonly ImGuiSDL3 Platform;
+ public readonly ImGuiSDL3Renderer Renderer;
+ internal readonly Queue EventQueue = new();
+ public event Action? WindowRectModified;
+
+ public color? ClearColor = null;
+
+ public Action? RenderCallback { get; set; }
+
+ private readonly Stopwatch _timer = Stopwatch.StartNew();
+ private TimeSpan _time = TimeSpan.Zero;
+
+ private SDL.Rect _screenClipRect;
+ private IntPtr _imGuiContext;
+
+ public SDL3Window(string name, int posX, int posY, int width, int height)
+ {
+ if (!SDL.Init(SDL.InitFlags.Video))
+ throw new Exception($"SDL_Init failed: {SDL.GetError()}");
+
+ // Create window & renderer
+ const SDL.WindowFlags windowFlags = SDL.WindowFlags.Resizable;
+ if (!SDL.CreateWindowAndRenderer(name, width, height, windowFlags, out Window, out Device))
+ throw new Exception($"SDL_CreateWindowAndRenderer failed: {SDL.GetError()}");
+
+ SDL.SetWindowTitle(Window, name);
+ SDL.SetWindowSize(Window, width, height);
+ SDL.SetWindowPosition(Window, posX, posY);
+
+ // Enable VSync
+ SDL.SetRenderVSync(Device, 1);
+
+ // Setup screen clip rect
+ SetupScreenClipRect();
+
+ // Create ImGui context
+ _imGuiContext = ImGui.CreateContext();
+ ImGui.SetCurrentContext(_imGuiContext);
+
+ ImGuiIOPtr io = ImGui.GetIO();
+ io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.DockingEnable;
+ io.Fonts.Flags |= ImFontAtlasFlags.NoBakedLines;
+
+ // Init platform and renderer
+ Platform = new ImGuiSDL3(Window, Device);
+ Renderer = new ImGuiSDL3Renderer(Device);
+
+ //ImGuiManager.TriggerImGuiRecreated();
+ }
+
+ private bool _disposed;
+
+ ~SDL3Window()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ if (Window != IntPtr.Zero)
+ {
+ SDL.GetWindowPosition(Window, out int x, out int y);
+ SDL.GetWindowSize(Window, out int w, out int h);
+ WindowRectModified?.Invoke(new(x, y, w, h));
+ }
+ }
+ catch
+ {
+ // Ignore if SDL window is already invalid
+ }
+
+ if (disposing)
+ {
+ Renderer?.Dispose();
+ Platform?.Dispose();
+
+ RenderCallback = null;
+ }
+
+ if (_imGuiContext != IntPtr.Zero)
+ {
+ ImGui.SetCurrentContext(_imGuiContext);
+ ImGui.DestroyContext();
+ _imGuiContext = IntPtr.Zero;
+ }
+
+ if (Device != IntPtr.Zero)
+ {
+ SDL.DestroyRenderer(Device);
+ }
+
+ if (Window != IntPtr.Zero)
+ {
+ SDL.DestroyWindow(Window);
+ }
+ }
+
+ public void RunOneFrame()
+ {
+ if (_disposed) return;
+
+ ImGui.SetCurrentContext(_imGuiContext);
+
+ ImGui.GetIO().DeltaTime = (float) (_timer.Elapsed - _time).TotalSeconds;
+ _time = _timer.Elapsed;
+
+ PollEvents();
+
+ Update();
+
+ var clear = ClearColor ?? Plugin.DefaultBackgroundColor.Value;
+ SDL.SetRenderDrawColor(Device, (byte) (clear.R * 255), (byte) (clear.G * 255), (byte) (clear.B * 255), (byte) (clear.A * 255));
+
+ Render();
+
+ if (_shouldClose || Plugin.CancellationToken.IsCancellationRequested)
+ {
+ Dispose();
+ }
+ ImGui.SetCurrentContext(IntPtr.Zero);
+ }
+
+ private 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 (EventQueue.TryDequeue(out var ev))
+ {
+ Platform.ProcessEvent(ev);
+
+ switch ((SDL.EventType) ev.Type)
+ {
+ // TODO: reimplement these events
+ case SDL.EventType.WindowFocusGained:
+ //ImGuiManager.TriggerImGuiFocusChanged(true);
+ break;
+ case SDL.EventType.WindowFocusLost:
+ //ImGuiManager.TriggerImGuiFocusChanged(false);
+ break;
+ case SDL.EventType.Terminating:
+ case SDL.EventType.WindowCloseRequested:
+ case SDL.EventType.Quit:
+ _shouldClose = true;
+ break;
+ case SDL.EventType.WindowResized:
+ SetupScreenClipRect();
+ SDL.GetWindowPosition(Window, out var x, out var y);
+ WindowRectModified?.Invoke(new(x, y, ev.Window.Data1, ev.Window.Data2));
+ break;
+ case SDL.EventType.WindowMoved:
+ SDL.GetWindowSize(Window, out var h, out var w);
+ WindowRectModified?.Invoke(new(ev.Window.Data1, ev.Window.Data2, w, h));
+ break;
+ }
+ }
+ }
+
+ private void Update()
+ {
+ Platform.NewFrame();
+ Renderer.NewFrame();
+ ImGui.NewFrame();
+ RenderCallback?.Invoke();
+ ImGui.EndFrame();
+ }
+
+ private void Render()
+ {
+ SDL.RenderClear(Device);
+
+ // Reset the clip rect to the screen size
+ SDL.SetRenderClipRect(Device, _screenClipRect);
+
+ // Render ImGui
+ ImGui.Render();
+ Renderer.RenderDrawData(ImGui.GetDrawData());
+
+ SDL.RenderPresent(Device);
+ }
+
+ private void SetupScreenClipRect()
+ {
+ SDL.GetWindowSize(Window, out int w, out int h);
+ _screenClipRect = new SDL.Rect
+ {
+ X = 0,
+ Y = 0,
+ W = w,
+ H = h
+ };
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 12385f4..783ff4e 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,11 @@
# ResoniteImGuiLib
+[](https://thunderstore.io/c/resonite/)
-A [ResoniteModLoader](https://github.com/resonite-modding-group/ResoniteModLoader) mod for [Resonite](https://resonite.com/) that is a library which allows modders to use [ImGuiUnityInject](https://github.com/art0007i/ImGuiUnityInject) in resonite.
+A [Resonite](https://resonite.com/) library that allows modders to create ImGui windows.
-Example usage can be found here: https://github.com/art0007i/ImGuiExample
-
-## Installation
-1. Install [ResoniteModLoader](https://github.com/resonite-modding-group/ResoniteModLoader).
-1. Place [ResoniteImGuiLib.dll](https://github.com/art0007i/ResoniteImGuiLib/releases/latest/download/ResoniteImGuiLib.dll) into your `rml_mods` folder. This folder should be at `C:\Program Files (x86)\Steam\steamapps\common\Resonite\rml_mods` for a default install. You can create it if it's missing, or if you launch the game once with ResoniteModLoader installed it will create the folder for you.
-2. Place [ImGuiUnityInject.dll](https://github.com/art0007i/ImGuiUnityInject/releases/latest/download/ImGuiUnityInject.dll) into your `rml_libs` folder.
-3. Place the `cimgui.dll` file from [ImGuiUnityInject/Plugins](https://github.com/art0007i/ImGuiUnityInject/tree/master/Plugins) into your `Resonite_Data/Plugins/x86_64` folder.
-1. Start the game. If you want to verify that the mod is working you can check your Resonite logs.
+## Installation (Manual)
+1. Install [BepisLoader](https://github.com/ResoniteModding/BepisLoader) for Resonite.
+2. Download the latest release ZIP file (e.g., `art0007i-ResoniteImGuiLib-2.0.0.zip`) from the [Releases](https://github.com/art0007i/ResoniteImGuiLib/releases) page.
+3. Extract the ZIP and copy the `plugins` folder to your BepInEx folder in your Resonite installation directory:
+ - **Default location:** `C:\Program Files (x86)\Steam\steamapps\common\Resonite\BepInEx\`
+4. Start the game. If you want to verify that the mod is working you can check your BepInEx logs.
\ No newline at end of file
diff --git a/ResoniteImGuiLib.sln b/ResoniteImGuiLib.sln
index 48bc990..bbd4f9b 100644
--- a/ResoniteImGuiLib.sln
+++ b/ResoniteImGuiLib.sln
@@ -1,21 +1,34 @@
+
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResoniteImGuiLib", "ResoniteImGuiLib\ResoniteImGuiLib.csproj", "{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}"
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResoniteImGuiLib", "Plugin\ResoniteImGuiLib.csproj", "{797E46C4-2CE9-40D2-9B24-64E35847ECC6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Release|Any CPU.Build.0 = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|x64.Build.0 = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|x86.Build.0 = Debug|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|x64.ActiveCfg = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|x64.Build.0 = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|x86.ActiveCfg = Release|Any CPU
+ {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}
- EndGlobalSection
EndGlobal
diff --git a/ResoniteImGuiLib/Properties/AssemblyInfo.cs b/ResoniteImGuiLib/Properties/AssemblyInfo.cs
deleted file mode 100644
index c6d795e..0000000
--- a/ResoniteImGuiLib/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("ResoniteImGuiLib")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("art0007i")]
-[assembly: AssemblyProduct("ResoniteImGuiLib")]
-[assembly: AssemblyCopyright("Copyright © 2025")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("FFA93FA9-4040-46FF-8A1C-2A190C0CC235")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.1.0")]
-[assembly: AssemblyFileVersion("1.1.0")]
diff --git a/ResoniteImGuiLib/Properties/launchSettings.json b/ResoniteImGuiLib/Properties/launchSettings.json
deleted file mode 100644
index cdceea2..0000000
--- a/ResoniteImGuiLib/Properties/launchSettings.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "profiles": {
- "Launch Resonite": {
- "commandName": "Executable",
- "executablePath": "$(GamePath)Resonite.exe",
- "commandLineArgs": "-DataPath \"D:\\YDMS\\Resonite Data\" -CachePath \"D:\\YDMS\\Resonite Cache\" -Screen -Invisible -LoadAssembly \"Libraries/ResoniteModLoader.dll\" -DoNotAutoLoadHome",
- "workingDirectory": "$(GamePath)"
- }
- }
-}
\ No newline at end of file
diff --git a/ResoniteImGuiLib/ResoniteImGuiLib.cs b/ResoniteImGuiLib/ResoniteImGuiLib.cs
deleted file mode 100644
index 10cb735..0000000
--- a/ResoniteImGuiLib/ResoniteImGuiLib.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using HarmonyLib;
-using ResoniteModLoader;
-using FrooxEngine;
-using Elements.Core;
-using ImGuiNET;
-using UnityEngine;
-using ImGuiUnityInject;
-using UnityEngine.SceneManagement;
-using System.Linq;
-using UnityFrooxEngineRunner;
-using System.Collections.Generic;
-
-namespace ResoniteImGuiLib;
-
-public static class ImGuiLib
-{
- public static ImGuiInstance GetOrCreateInstance(ImGuiReady onReady)
- {
- return GetOrCreateInstance("global", onReady);
- }
- public static ImGuiInstance GetOrCreateInstance(string name = "global", ImGuiReady onReady = null)
- {
- return ImGuiInstance.GetOrCreate(name, (gui, isNew) =>
- {
- if (isNew)
- {
- gui._camera = SceneManager.GetActiveScene().GetRootGameObjects().Where(go => go.name == "FrooxEngine").First().GetComponent().OverlayCamera;
- gui.Layout += () =>
- {
- var io = ImGui.GetIO();
- ResoniteImGuiLib.WantCapture[name] = (io.WantCaptureMouse, io.WantCaptureKeyboard);
- };
- }
-
- if (onReady != null) onReady(gui, isNew);
- else gui.enabled = true;
- });
- }
-}
-
-public class ResoniteImGuiLib : ResoniteMod
-{
- public override string Name => "ResoniteImGuiLib";
- public override string Author => "art0007i";
- public override string Version => "1.1.0";
- public override string Link => "https://github.com/art0007i/ResoniteImGuiLib/";
- public override void OnEngineInit()
- {
- Harmony harmony = new Harmony("me.art0007i.ResoniteImGuiLib");
- harmony.PatchAll();
- }
- internal static Dictionary WantCapture = new();
-
- [HarmonyPatch(typeof(MouseDriver), "UpdateMouse")]
- class CursorUpdatePatch
- {
- public static bool Prefix(Mouse mouse)
- {
- if (WantCapture.Any(x=>x.Value.Item1))
- {
- mouse.LeftButton.UpdateState(false);
- mouse.RightButton.UpdateState(false);
- mouse.MiddleButton.UpdateState(false);
- mouse.MouseButton4.UpdateState(false);
- mouse.MouseButton5.UpdateState(false);
- mouse.DirectDelta.UpdateValue(float2.Zero, Time.deltaTime);
- mouse.ScrollWheelDelta.UpdateValue(float2.Zero, Time.deltaTime);
- mouse.NormalizedScrollWheelDelta.UpdateValue(float2.Zero, Time.deltaTime);
-
- var cursor = ImGui.GetMouseCursor();
- Cursor.visible = cursor != ImGuiMouseCursor.None;
- Cursor.lockState = CursorLockMode.None;
- return false;
- }
- return true;
- }
- }
-
- [HarmonyPatch(typeof(KeyboardDriver), "Current_onTextInput")]
- class KeyboardDeltaPatch
- {
- public static bool Prefix()
- {
- if (WantCapture.Any(x => x.Value.Item2))
- {
- return false;
- }
- return true;
- }
- }
- [HarmonyPatch(typeof(KeyboardDriver), "GetKeyState")]
- class KeyboardStatePatch
- {
- public static bool Prefix(ref bool __result)
- {
- if (WantCapture.Any(x => x.Value.Item2))
- {
- __result = false;
- return false;
- }
- return true;
- }
- }
-}
diff --git a/ResoniteImGuiLib/ResoniteImGuiLib.csproj b/ResoniteImGuiLib/ResoniteImGuiLib.csproj
deleted file mode 100644
index 6e50751..0000000
--- a/ResoniteImGuiLib/ResoniteImGuiLib.csproj
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}
- Library
- Properties
- ResoniteImGuiLib
- ResoniteImGuiLib
- false
- 10
- net472
- 512
- true
- $(MSBuildThisFileDirectory)Resonite
- C:\Program Files (x86)\Steam\steamapps\common\Resonite\
- $(HOME)/.steam/steam/steamapps/common/Resonite/
- E:\Programs\Steam\steamapps\common\Resonite\
- false
- true
- false
- None
-
-
-
-
- $(GamePath)rml_libs\0Harmony.dll
- $(GamePath)0Harmony.dll
-
-
- $(GamePath)Resonite_Data\Managed\Elements.Core.dll
-
-
- $(GamePath)Resonite_Data\Managed\FrooxEngine.dll
-
-
- $(GamePath)ResoniteModLoader.dll
- $(GamePath)Libraries\ResoniteModLoader.dll
-
-
- $(GamePath)Resonite_Data/Managed/UnityFrooxEngineRunner.dll
-
-
- $(GamePath)Resonite_Data/Managed/Assembly-CSharp.dll
-
-
- $(GamePath)rml_libs/ImGuiUnityInject.dll
-
-
- $(GamePath)Resonite_Data/Managed/UnityEngine.CoreModule.dll
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..ccbfc72
Binary files /dev/null and b/icon.png differ
diff --git a/thunderstore.toml b/thunderstore.toml
new file mode 100644
index 0000000..2737c4c
--- /dev/null
+++ b/thunderstore.toml
@@ -0,0 +1,55 @@
+# This file is used by the Thunderstore CLI to build and publish your mod to Thunderstore. https://github.com/thunderstore-io/thunderstore-cli
+
+[config]
+schemaVersion = "0.0.1"
+
+[package]
+namespace = "art0007i"
+name = "ResoniteImGuiLib"
+description = "Library that allows modders to create ImGui windows."
+websiteUrl = "https://github.com/art0007i/ResoniteImGuiLib"
+containsNsfwContent = false
+
+[package.dependencies]
+ResoniteModding-BepisLoader = "1.4.1"
+ResoniteModding-BepisResoniteWrapper = "1.0.0"
+
+[build]
+icon = "./icon.png"
+readme = "./README.md"
+outdir = "./build"
+
+[[build.copy]]
+source = "./Plugin/bin/Release/ResoniteImGuiLib.dll"
+target = "plugins/ResoniteImGuiLib/"
+
+[[build.copy]]
+source = "./Plugin/bin/Release/ResoniteImGuiLib.pdb"
+target = "plugins/ResoniteImGuiLib/"
+
+[[build.copy]]
+source = "./Plugin/bin/Release/ImGui.NET.dll"
+target = "plugins/ResoniteImGuiLib/"
+
+[[build.copy]]
+source = "./Plugin/bin/Release/runtimes/linux-x64/native/libcimgui.so"
+target = "plugins/ResoniteImGuiLib/native/"
+
+[[build.copy]]
+source = "./Plugin/bin/Release/runtimes/win-x64/native/cimgui.dll"
+target = "plugins/ResoniteImGuiLib/native/"
+
+[[build.copy]]
+source = "./CHANGELOG.md"
+target = "/"
+
+[[build.copy]]
+source = "./LICENSE"
+target = "/"
+
+[publish]
+repository = "https://thunderstore.io"
+communities = ["resonite"]
+
+[publish.categories]
+resonite = [ "libraries", "technicaltweaks" ] # TODO: Change this to your mod's categories, see https://thunderstore.io/api/experimental/community/resonite/category/ for a list of available categories