From 0d3fb60b4c820d4552e1a4d466d7f073b18356e7 Mon Sep 17 00:00:00 2001 From: art0007i Date: Wed, 1 Oct 2025 22:21:23 +0200 Subject: [PATCH] switch to SDL3+BepisLoader --- .config/dotnet-tools.json | 13 + .gitattributes | 63 --- .gitignore | 94 +++- CHANGELOG.md | 5 + Directory.Build.targets | 20 + LICENSE.txt => LICENSE | 2 +- Plugin/ImGuiInstance.cs | 147 ++++++ Plugin/Plugin.cs | 49 ++ Plugin/Properties/launchSettings.json | 10 + Plugin/ResoniteImGuiLib.csproj | 70 +++ Plugin/SDL3/ImGuiSDL3.cs | 427 ++++++++++++++++++ Plugin/SDL3/ImGuiSDL3Renderer.cs | 302 +++++++++++++ Plugin/SDL3/SDL3Window.cs | 220 +++++++++ README.md | 17 +- ResoniteImGuiLib.sln | 29 +- ResoniteImGuiLib/Properties/AssemblyInfo.cs | 35 -- .../Properties/launchSettings.json | 10 - ResoniteImGuiLib/ResoniteImGuiLib.cs | 104 ----- ResoniteImGuiLib/ResoniteImGuiLib.csproj | 56 --- icon.png | Bin 0 -> 1755 bytes thunderstore.toml | 55 +++ 21 files changed, 1439 insertions(+), 289 deletions(-) create mode 100644 .config/dotnet-tools.json delete mode 100644 .gitattributes create mode 100644 CHANGELOG.md create mode 100644 Directory.Build.targets rename LICENSE.txt => LICENSE (96%) create mode 100644 Plugin/ImGuiInstance.cs create mode 100644 Plugin/Plugin.cs create mode 100644 Plugin/Properties/launchSettings.json create mode 100644 Plugin/ResoniteImGuiLib.csproj create mode 100644 Plugin/SDL3/ImGuiSDL3.cs create mode 100644 Plugin/SDL3/ImGuiSDL3Renderer.cs create mode 100644 Plugin/SDL3/SDL3Window.cs delete mode 100644 ResoniteImGuiLib/Properties/AssemblyInfo.cs delete mode 100644 ResoniteImGuiLib/Properties/launchSettings.json delete mode 100644 ResoniteImGuiLib/ResoniteImGuiLib.cs delete mode 100644 ResoniteImGuiLib/ResoniteImGuiLib.csproj create mode 100644 icon.png create mode 100644 thunderstore.toml 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 +[![Thunderstore Badge](https://modding.resonite.net/assets/available-on-thunderstore.svg)](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 0000000000000000000000000000000000000000..ccbfc72edf38c662ceb7157797963ff180ba01bb GIT binary patch literal 1755 zcmZuv3pA8z82${#xC_xpW`rh^a_gWN3=Ju)I@`FMHNqH&5DtTeNf8N|Q<75ICDq2X zNHZj3IT@?SC6`=gRJ*h;Q*L35*?-zy;q;#KpZEE`|Gj|4u_74jwFSK2LrG>kD6!e(&VY0=5gHFaIajP${CJl zk#WYQ*sDewjgFyvs{ba=bYaO|S4x;xauw1tDH^{jnY7p1Fy&Ht${BUMlJT7eBiGe^ znkl27?b_?cK8;XmuOFGcnY{LMSs@o3%<^^x~{E3A@y_a zjb%19pL&q>rwJ(-XK-6LXZORyOOEZqX|oz3PkchAl4dmF@^6o%PgEbyW2@3#94=h8 zF}nY|NR_�NI(?&A}v}ju7G&<#; zzE3gSMeSXydE@(fouYbjLu9(<(NislMMsj_2Dd}?zh_rUgRljkfCe-GU|#{dLP=B)a`d{ZShE%@KmA*Jt%ugZl|!@+|L4o<SGS(a;7#+nLmgbfSR@?ee`^e746{}eKuk+$RqiYa zh0k4G2@$P!c3_t0<3;uAr1evZgOSgqS(UkgD5|aC4Wld`3)rgvQ?WMh2EZ#ZNx{;_ zsZ0E1TgrmqASSUv@}-!$by15Rt;wlAVMU26(=7uZ2UBOa?P?(!vT!{Lz<2}OwcSD? zsO&J$uxk)(A#Nhk9(VpJ`mf0`B*e`cw_=*vv5GG;Fv%X_ChEbz7EwWwCl)Qg(^{lcha>2XEwbBdZlPWXuJJQ< zfy&S_RwTSel7xYC<)V?QbG>cKeK7l|iH3QuGi+`#5DsJ_g#Nb4=~S3oh#U#1_zF;F z@?^{7Du7%)?=RrDLn=x275RiM-|Z;;0Rh7MA7Wnw8kSC44x_SO_Um#C_O+>k8f8vv zV-0@W&v+>*pq0)$Dv-!;Zd3(7R@7G=n8f=9qQT>Uso_MT9bDAoorYRCXdzDr_H_=2 ztcPz8V)oBJ$>m+9=9W9C0)jQC1jl4wKGYj+J*!z^*$IoTUM;2G$EpVKn5IC`jnsaNgBA5 rTHBwR3*V`@0j8-rV7@Ng0RWnZ3Nj$;=i7a`FW!Tzv%6D;BQf