5 Commits

Author SHA1 Message Date
NepuShiro 42cfbeb111 Initial 2026-05-18 02:05:15 -05:00
art0007i f57a671d9f fix typo
fixes windows loading tall instead of wide sometimes on startup
2025-10-03 00:35:38 +02:00
art0007i bdca389f2e fix native libs
add SdlEvent callback
add OpenWindow function
2025-10-02 03:35:32 +02:00
art0007i 0d3fb60b4c switch to SDL3+BepisLoader 2025-10-01 22:21:23 +02:00
art0007i cdfcc46019 Update README.md 2025-04-11 20:16:42 +02:00
21 changed files with 1513 additions and 287 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"tcli": {
"version": "0.2.4",
"commands": [
"tcli"
],
"rollForward": true
}
}
}
-63
View File
@@ -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
+91 -3
View File
@@ -1,7 +1,10 @@
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## 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 # User-specific files
*.rsuser *.rsuser
@@ -57,11 +60,14 @@ dlldata.c
# Benchmark Results # Benchmark Results
BenchmarkDotNet.Artifacts/ BenchmarkDotNet.Artifacts/
# .NET Core # .NET
project.lock.json project.lock.json
project.fragment.lock.json project.fragment.lock.json
artifacts/ artifacts/
# Tye
.tye/
# ASP.NET Scaffolding # ASP.NET Scaffolding
ScaffoldingReadMe.txt ScaffoldingReadMe.txt
@@ -82,6 +88,8 @@ StyleCopReport.xml
*.pgc *.pgc
*.pgd *.pgd
*.rsp *.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr *.sbr
*.tlb *.tlb
*.tli *.tli
@@ -395,4 +403,84 @@ FodyWeavers.xsd
*.msp *.msp
# JetBrains Rider # JetBrains Rider
*.sln.iml *.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/
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## 2.0.0
Initial BepisLoader Release
+20
View File
@@ -0,0 +1,20 @@
<Project>
<!--
Build Thunderstore package by calling `dotnet build -c Release -target:PackTS -v d` (verbosity detailed).
Publish to Thunderstore by including `-property:PublishTS=true` in the command.
-->
<Target Name="PackTS" DependsOnTargets="Build">
<CallTarget Targets="PackTS_Execute" Condition="'$(ThunderstorePackable)' == 'true' and '$(Configuration)' == 'Release'" />
</Target>
<Target Name="PackTS_Execute">
<PropertyGroup>
<BuildArgument Condition="'$(PublishTS)' != 'true'">build</BuildArgument>
<BuildArgument Condition="'$(PublishTS)' == 'true'">publish</BuildArgument>
</PropertyGroup>
<Exec Command="dotnet tool restore" WorkingDirectory="$(SolutionDir)" />
<Exec Command="dotnet tcli $(BuildArgument) --package-version $(Version)" WorkingDirectory="$(SolutionDir)" />
</Target>
</Project>
+1 -1
View File
@@ -1,6 +1,6 @@
MIT No Attribution MIT No Attribution
Copyright 2023 art0007i Copyright (c) 2025 art0007i
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+203
View File
@@ -0,0 +1,203 @@
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;
/// <summary>
/// Called whenever ImGui is being rendered. You can use ImGui functions inside here.
/// </summary>
public event Action? Layout;
/// <summary>
/// Called whenever the window is processing an event. Returning false means that the event will not be processed further.
/// </summary>
public event Func<SDL.Event, bool>? SdlEvent;
private string _name;
private ConfigEntry<int4> _entry;
private static bool IsSDL3Running => _sdl3Thread is { IsAlive: true };
private static Thread? _sdl3Thread;
private static Dictionary<string, ImGuiInstance> GuiInstances = new();
internal static ConcurrentQueue<ImGuiInstance> newInstances = new();
internal static ConcurrentQueue<(string, Action<SDL3Window>)> windowRequests = new();
/// <summary>
/// Tries to open the window. If it's already open, nothing happens. This function may be expensive to call rapidly.
/// </summary>
public void OpenWindow()
{
newInstances.Enqueue(this);
}
/// <summary>
/// Gets a reference to the SDL3Window inside of a callback.
/// The callback will be called inside the SDL3 Thread.
/// </summary>
/// <param name="callback"></param>
public void GetImGui(Action<SDL3Window> callback)
{
windowRequests.Enqueue((Name, callback));
}
/// <summary>
/// Creates a new ImGuiInstance with the "global" name or returns it if it already exists.
/// </summary>
/// <param name="onReady">Callback for when the ImGui instance is ready. You can put initialization logic here.</param>
/// <returns></returns>
public static ImGuiInstance GetOrCreate(ImGuiReady onReady) => GetOrCreate("global", onReady);
/// <summary>
/// Creates a new ImGuiInstance or returns one if it already exists.
/// </summary>
/// <param name="onReady">Callback for when the ImGui instance is ready. You can put initialization logic here.</param>
/// <returns></returns>
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<string, SDL3Window> windows = new();
static Dictionary<uint, SDL3Window> windowsById = new();
private static void RunSDL3()
{
try
{
while (true)
{
while (newInstances.TryDequeue(out var request))
{
if (windows.ContainsKey(request.Name)) continue;
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;
app.SdlEventReceived = request.SdlEvent;
if (!windows.TryAdd(request.Name, app))
{
Plugin.Log.LogError($"Failed to add window with name {request.Name}!");
}
if (!windowsById.TryAdd(app.SdlWindowId, app))
{
Plugin.Log.LogError($"Failed to add window with id {app.SdlWindowId}, 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);
}
}
var removed = new HashSet<(string, uint)>();
foreach (var (key, window) in windows)
{
if(window.Disposed)
{
removed.Add((key, window.SdlWindowId));
continue;
}
window.SetContext();
if (Plugin.CancellationToken.IsCancellationRequested)
{
window.ForceDispose();
continue;
}
if (window.ShouldClose)
{
removed.Add((key, window.SdlWindowId));
window.Dispose();
continue;
}
window.RunOneFrame();
}
foreach (var (k, k2) in removed)
{
var b = windows.Remove(k);
var b2 = windowsById.Remove(k2);
Plugin.Log.LogDebug($"Removed window {k}, success codes: {b}|{b2}");
}
if (Plugin.CancellationToken.IsCancellationRequested)
{
break;
}
// Prevent busy looping when nothing is happening.
if (windows.Count == 0)
{
Thread.Sleep(1000);
}
}
}
catch (Exception e)
{
Plugin.Log.LogError($"SDL3 Thread crashed: {e}");
}
_sdl3Thread?.Interrupt();
_sdl3Thread = null;
}
}
public delegate void ImGuiReady(SDL3Window imGui, bool isNewInstance);
+49
View File
@@ -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<color> 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();
};
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"profiles": {
"Launch": {
"commandName": "Executable",
"executablePath": "$(GamePath)Renderite.Host.exe",
"commandLineArgs": "-Screen $(ResoniteLaunchArguments)",
"workingDirectory": "$(GamePath)"
}
}
}
+70
View File
@@ -0,0 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>2.1.0</Version>
<Authors>art0007i</Authors>
<TargetFramework>net10.0</TargetFramework>
<RepositoryUrl>https://github.com/art0007i/ResoniteImGuiLib</RepositoryUrl>
<PackageId>art0007i.ResoniteImGuiLib</PackageId>
<Product>ResoniteImGuiLib</Product>
<RootNamespace>ResoniteImGuiLib</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Deterministic>true</Deterministic>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<CopyToPlugins>false</CopyToPlugins>
<ThunderstorePackable>true</ThunderstorePackable>
<GamePath Condition="'$(ResonitePath)' != ''">$(ResonitePath)/</GamePath>
<GamePath Condition="Exists('$(MSBuildProgramFiles32)\Steam\steamapps\common\Resonite\')">$(MSBuildProgramFiles32)\Steam\steamapps\common\Resonite\</GamePath>
<GamePath Condition="Exists('$(HOME)/.steam/steam/steamapps/common/Resonite/')">$(HOME)/.steam/steam/steamapps/common/Resonite/</GamePath>
<PluginTargetDir>$(GamePath)BepInEx\plugins\$(AssemblyName)</PluginTargetDir>
<RestoreAdditionalProjectSources>
https://nuget-modding.resonite.net/v3/index.json;
</RestoreAdditionalProjectSources>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<!-- Modding dependencies -->
<ItemGroup>
<PackageReference Include="BepInEx.ResonitePluginInfoProps" Version="3.*" />
<PackageReference Include="ResoniteModding.BepInExResoniteShim" Version="0.8.*" />
<PackageReference Include="ResoniteModding.BepisResoniteWrapper" Version="1.0.*" />
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
<PackageReference Include="SDL3-CS" Version="3.2.18" />
<PackageReference Include="SDL3-CS.Native" Version="3.2.18" />
</ItemGroup>
<!-- NuGet fallback stripped game references -->
<ItemGroup Condition="!Exists('$(GamePath)')">
<PackageReference Include="Resonite.GameLibs" Version="2025.*" PrivateAssets="all" />
</ItemGroup>
<!-- Local game references -->
<ItemGroup Condition="Exists('$(GamePath)')">
<Reference Include="FrooxEngine">
<HintPath>$(GamePath)FrooxEngine.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Elements.Core">
<HintPath>$(GamePath)Elements.Core.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Renderite.Shared">
<HintPath>$(GamePath)Renderite.Shared.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<!-- Post-build copy to game plugins folder -->
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<ItemGroup>
<PluginFiles Include="$(TargetPath)" />
<PluginFiles Include="$(TargetDir)$(TargetName).pdb" Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
</ItemGroup>
<Copy SourceFiles="@(PluginFiles)" DestinationFolder="$(PluginTargetDir)" Condition="'$(CopyToPlugins)' == 'true'" />
<Message Text="Copied plugin files to $(PluginTargetDir)" Importance="high" Condition="'$(CopyToPlugins)' == 'true'" />
</Target>
</Project>
+427
View File
@@ -0,0 +1,427 @@
using ImGuiNET;
using SDL3;
namespace ResoniteImGuiLib.SDL3;
/// <summary>
/// Implementation of SDL3 platform backend for ImGui.
/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdl3.h
/// </summary>
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
}
}
+302
View File
@@ -0,0 +1,302 @@
using System.Numerics;
using ImGuiNET;
using SDL3;
namespace ResoniteImGuiLib.SDL3;
/// <summary>
/// Implementation of ImGui for SDL3 Renderer backend.
/// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdlrenderer3.h
/// </summary>
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;
}
}
+238
View File
@@ -0,0 +1,238 @@
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;
public readonly uint SdlWindowId;
internal readonly Queue<SDL.Event> EventQueue = new();
internal Action? RenderCallback { get; set; }
internal Func<SDL.Event, bool>? SdlEventReceived { get; set; }
public event Action<int4>? WindowRectModified;
public color? ClearColor = null;
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);
SdlWindowId = SDL.GetWindowID(Window);
//ImGuiManager.TriggerImGuiRecreated();
}
public bool Disposed => _disposed;
private bool _disposed;
~SDL3Window()
{
Dispose(false);
}
public void Dispose()
{
Dispose(false);
}
public void ForceDispose()
{
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(IntPtr.Zero);
ImGui.DestroyContext(_imGuiContext);
_imGuiContext = IntPtr.Zero;
}
if (Device != IntPtr.Zero)
{
SDL.DestroyRenderer(Device);
}
if (Window != IntPtr.Zero)
{
SDL.DestroyWindow(Window);
}
}
public void SetContext()
{
ImGui.SetCurrentContext(_imGuiContext);
}
public void RunOneFrame()
{
if (_disposed) return;
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();
ImGui.SetCurrentContext(IntPtr.Zero);
}
public bool ShouldClose;
private void PollEvents()
{
if (ImGui.GetIO().WantTextInput && !SDL.TextInputActive(Window))
SDL.StartTextInput(Window);
else if (!ImGui.GetIO().WantTextInput && SDL.TextInputActive(Window))
SDL.StopTextInput(Window);
while (EventQueue.TryDequeue(out var ev))
{
var skip = false;
foreach (var func in Delegate.EnumerateInvocationList(SdlEventReceived))
{
if (!func(ev))
{
skip = true;
break;
}
}
if (skip) continue;
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 w, out var h);
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
};
}
}
+8 -7
View File
@@ -1,12 +1,13 @@
# ResoniteImGuiLib # 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 Example usage can be found here: https://github.com/art0007i/ImGuiExample
## Installation ## Installation (Manual)
1. Install [ResoniteModLoader](https://github.com/resonite-modding-group/ResoniteModLoader). 1. Install [BepisLoader](https://github.com/ResoniteModding/BepisLoader) for Resonite.
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. Download the latest release ZIP file (e.g., `art0007i-ResoniteImGuiLib-2.0.0.zip`) from the [Releases](https://github.com/art0007i/ResoniteImGuiLib/releases) page.
2. Place [ImGuiUnityInject.dll](https://github.com/art0007i/ImGuiUnityInject/releases/latest/download/ImGuiUnityInject.dll) into your `rml_libs` folder. 3. Extract the ZIP and copy the `plugins` folder to your BepInEx folder in your Resonite installation directory:
3. Place the files from [ImGuiUnityInject/Plugins](https://github.com/art0007i/ImGuiUnityInject/tree/master/Plugins) into your `Resonite_Data/Plugins/x86_64` folder. - **Default location:** `C:\Program Files (x86)\Steam\steamapps\common\Resonite\BepInEx\`
1. Start the game. If you want to verify that the mod is working you can check your Resonite logs. 4. Start the game. If you want to verify that the mod is working you can check your BepInEx logs.
+21 -8
View File
@@ -1,21 +1,34 @@
Microsoft Visual Studio Solution File, Format Version 12.00 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Debug|Any CPU.Build.0 = Debug|Any CPU {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Release|Any CPU.ActiveCfg = Release|Any CPU {797E46C4-2CE9-40D2-9B24-64E35847ECC6}.Debug|x64.ActiveCfg = Debug|Any CPU
{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}.Release|Any CPU.Build.0 = Release|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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FFA93FA9-4040-46FF-8A1C-2A190C0CC235}
EndGlobalSection
EndGlobal EndGlobal
@@ -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")]
@@ -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)"
}
}
}
-104
View File
@@ -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<FrooxEngineRunner>().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<string, (bool, bool)> 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;
}
}
}
-56
View File
@@ -1,56 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectGuid>{FFA93FA9-4040-46FF-8A1C-2A190C0CC235}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>ResoniteImGuiLib</RootNamespace>
<AssemblyTitle>ResoniteImGuiLib</AssemblyTitle>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<LangVersion>10</LangVersion>
<TargetFramework>net472</TargetFramework>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<GamePath>$(MSBuildThisFileDirectory)Resonite</GamePath>
<GamePath Condition="Exists('C:\Program Files (x86)\Steam\steamapps\common\Resonite\')">C:\Program Files (x86)\Steam\steamapps\common\Resonite\</GamePath>
<GamePath Condition="Exists('$(HOME)/.steam/steam/steamapps/common/Resonite/')">$(HOME)/.steam/steam/steamapps/common/Resonite/</GamePath>
<GamePath Condition="Exists('E:\Programs\Steam\steamapps\common\Resonite')">E:\Programs\Steam\steamapps\common\Resonite\</GamePath>
<CopyLocal>false</CopyLocal>
<CopyToMods Condition="'$(CopyToMods)'==''">true</CopyToMods>
<DebugSymbols Condition="'$(Configuration)'=='Release'">false</DebugSymbols>
<DebugType Condition="'$(Configuration)'=='Release'">None</DebugType>
</PropertyGroup>
<ItemGroup>
<Reference Include="HarmonyLib">
<HintPath>$(GamePath)rml_libs\0Harmony.dll</HintPath>
<HintPath Condition="Exists('$(GamePath)0Harmony.dll')">$(GamePath)0Harmony.dll</HintPath>
</Reference>
<Reference Include="Elements.Core">
<HintPath>$(GamePath)Resonite_Data\Managed\Elements.Core.dll</HintPath>
</Reference>
<Reference Include="FrooxEngine">
<HintPath>$(GamePath)Resonite_Data\Managed\FrooxEngine.dll</HintPath>
</Reference>
<Reference Include="ResoniteModLoader">
<HintPath>$(GamePath)ResoniteModLoader.dll</HintPath>
<HintPath>$(GamePath)Libraries\ResoniteModLoader.dll</HintPath>
</Reference>
<Reference Include="UnityFrooxEngineRunner.dll">
<HintPath>$(GamePath)Resonite_Data/Managed/UnityFrooxEngineRunner.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>$(GamePath)Resonite_Data/Managed/Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="ImGuiUnityInject.dll">
<HintPath>$(GamePath)rml_libs/ImGuiUnityInject.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule.dll">
<HintPath>$(GamePath)Resonite_Data/Managed/UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(CopyToMods)'=='true'">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(GamePath)rml_mods" />
<Message Text="Copied $(TargetFileName) to $(GamePath)rml_mods" Importance="high" />
</Target>
</Project>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+55
View File
@@ -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/"
[[build.copy]]
source = "./Plugin/bin/Release/runtimes/win-x64/native/cimgui.dll"
target = "plugins/ResoniteImGuiLib/"
[[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