diff --git a/Directory.Packages.props b/Directory.Packages.props
index de08a5db..6e4cddf7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,9 +9,13 @@
all
+
+
+
-
+
+
@@ -21,7 +25,7 @@
-
+
@@ -33,11 +37,11 @@
-
-
+
+
-
\ No newline at end of file
+
diff --git a/ImGuiApp.Test/AdvancedCoverageTests.cs b/ImGuiApp.Test/AdvancedCoverageTests.cs
index 4bd6413b..d0f28f66 100644
--- a/ImGuiApp.Test/AdvancedCoverageTests.cs
+++ b/ImGuiApp.Test/AdvancedCoverageTests.cs
@@ -7,6 +7,7 @@ namespace ktsu.ImGuiApp.Test;
using System;
using Hexa.NET.ImGui;
using ktsu.ImGuiApp.ImGuiController;
+using ktsu.Semantics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
///
@@ -179,8 +180,8 @@ public void ImGuiApp_WindowState_MultipleAccess_ReturnsConsistentValues()
[TestMethod]
public void ImGuiApp_Textures_MultipleAccess_ReturnsSameInstance()
{
- System.Collections.Concurrent.ConcurrentDictionary textures1 = ImGuiApp.Textures;
- System.Collections.Concurrent.ConcurrentDictionary textures2 = ImGuiApp.Textures;
+ System.Collections.Concurrent.ConcurrentDictionary textures1 = ImGuiApp.Textures;
+ System.Collections.Concurrent.ConcurrentDictionary textures2 = ImGuiApp.Textures;
// Should return the same collection instance
Assert.AreSame(textures1, textures2);
diff --git a/ImGuiApp.Test/ImGuiAppCoreTests.cs b/ImGuiApp.Test/ImGuiAppCoreTests.cs
index 5fe2f9ed..f91f8603 100644
--- a/ImGuiApp.Test/ImGuiAppCoreTests.cs
+++ b/ImGuiApp.Test/ImGuiAppCoreTests.cs
@@ -6,7 +6,7 @@ namespace ktsu.ImGuiApp.Test;
using System.Collections.Concurrent;
using ktsu.Extensions;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
///
diff --git a/ImGuiApp.Test/ImGuiAppDataStructureTests.cs b/ImGuiApp.Test/ImGuiAppDataStructureTests.cs
index b4c4b99c..39cb9600 100644
--- a/ImGuiApp.Test/ImGuiAppDataStructureTests.cs
+++ b/ImGuiApp.Test/ImGuiAppDataStructureTests.cs
@@ -6,7 +6,7 @@ namespace ktsu.ImGuiApp.Test;
using System.Numerics;
using ktsu.Extensions;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Silk.NET.Windowing;
diff --git a/ImGuiApp.Test/ImGuiAppTests.cs b/ImGuiApp.Test/ImGuiAppTests.cs
index a0b3363e..a8de52bc 100644
--- a/ImGuiApp.Test/ImGuiAppTests.cs
+++ b/ImGuiApp.Test/ImGuiAppTests.cs
@@ -2,15 +2,13 @@
// All rights reserved.
// Licensed under the MIT license.
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
[assembly: DoNotParallelize]
namespace ktsu.ImGuiApp.Test;
using System.Numerics;
using ktsu.Extensions;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Silk.NET.Core.Contexts;
diff --git a/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs b/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs
index dfc4326b..bbcbc105 100644
--- a/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs
+++ b/ImGuiApp.Test/ImGuiAppWindowManagementTests.cs
@@ -5,8 +5,7 @@
namespace ktsu.ImGuiApp.Test;
using System.Numerics;
-
-using ktsu.StrongPaths;
+using ktsu.Semantics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Silk.NET.Windowing;
diff --git a/ImGuiApp/DebugLogger.cs b/ImGuiApp/DebugLogger.cs
index cd96905e..b1447faf 100644
--- a/ImGuiApp/DebugLogger.cs
+++ b/ImGuiApp/DebugLogger.cs
@@ -5,7 +5,7 @@
namespace ktsu.ImGuiApp;
using ktsu.Extensions;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
///
/// Simple file logger for debugging crashes
diff --git a/ImGuiApp/ImGuiApp.cs b/ImGuiApp/ImGuiApp.cs
index d6527a77..3556468f 100644
--- a/ImGuiApp/ImGuiApp.cs
+++ b/ImGuiApp/ImGuiApp.cs
@@ -15,7 +15,7 @@ namespace ktsu.ImGuiApp;
using ktsu.ImGuiApp.ImGuiController;
using ktsu.Invoker;
using ktsu.ScopedAction;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
@@ -101,9 +101,16 @@ public static ImGuiAppWindowState WindowState
///
internal static void OnUserInput() => lastInputTime = DateTime.UtcNow;
- internal static bool showImGuiMetrics;
- internal static bool showImGuiDemo;
- internal static bool showPerformanceMonitor;
+ internal static bool isImGuiMetricsVisible;
+ internal static bool isImGuiDemoVisible;
+ internal static bool isImGuiStyleEditorVisible;
+ internal static bool isPerformanceMonitorVisible;
+
+ // Track whether we need to focus windows on their first frame
+ internal static bool shouldFocusImGuiDemo;
+ internal static bool shouldFocusImGuiMetrics;
+ internal static bool shouldFocusImGuiStyleEditor;
+ internal static bool shouldFocusPerformanceMonitor;
// Performance monitoring data structures
internal static readonly Queue performanceFrameTimes = new();
@@ -133,6 +140,54 @@ public static void Stop()
window.Close();
}
+ ///
+ /// Opens the ImGui Demo window.
+ ///
+ public static void ShowImGuiDemo()
+ {
+ if (!isImGuiDemoVisible)
+ {
+ shouldFocusImGuiDemo = true;
+ }
+ isImGuiDemoVisible = true;
+ }
+
+ ///
+ /// Opens the ImGui Metrics window.
+ ///
+ public static void ShowImGuiMetrics()
+ {
+ if (!isImGuiMetricsVisible)
+ {
+ shouldFocusImGuiMetrics = true;
+ }
+ isImGuiMetricsVisible = true;
+ }
+
+ ///
+ /// Opens the ImGui Style Editor window.
+ ///
+ public static void ShowImGuiStyleEditor()
+ {
+ if (!isImGuiStyleEditorVisible)
+ {
+ shouldFocusImGuiStyleEditor = true;
+ }
+ isImGuiStyleEditorVisible = true;
+ }
+
+ ///
+ /// Opens the Performance Monitor window.
+ ///
+ public static void ShowPerformanceMonitor()
+ {
+ if (!isPerformanceMonitorVisible)
+ {
+ shouldFocusPerformanceMonitor = true;
+ }
+ isPerformanceMonitorVisible = true;
+ }
+
internal static ImGuiAppConfig Config { get; set; } = new();
internal static void InitializeWindow(ImGuiAppConfig config)
@@ -337,6 +392,8 @@ internal static void SetupWindowUpdateHandler(ImGuiAppConfig config)
UpdateWindowPerformance();
UpdatePerformanceMonitoring((float)delta);
+ // SetupContext prepares ImGui context for this frame
+ controller?.SetupContext();
controller?.Update((float)delta);
config.OnUpdate?.Invoke((float)delta);
Invoker.DoInvokes();
@@ -688,19 +745,54 @@ internal static void RenderAppMenu(Action? menuDelegate)
if (ImGui.BeginMenu("Debug"))
{
- if (ImGui.MenuItem("Show ImGui Demo", "", showImGuiDemo))
+ if (ImGui.MenuItem("Show ImGui Demo", "", isImGuiDemoVisible))
{
- showImGuiDemo = !showImGuiDemo;
+ if (!isImGuiDemoVisible)
+ {
+ ShowImGuiDemo();
+ }
+ else
+ {
+ isImGuiDemoVisible = false;
+ }
}
- if (ImGui.MenuItem("Show ImGui Metrics", "", showImGuiMetrics))
+ if (ImGui.MenuItem("Show ImGui Metrics", "", isImGuiMetricsVisible))
{
- showImGuiMetrics = !showImGuiMetrics;
+ if (!isImGuiMetricsVisible)
+ {
+ ShowImGuiMetrics();
+ }
+ else
+ {
+ isImGuiMetricsVisible = false;
+ }
}
- if (ImGui.MenuItem("Show Performance Monitor", "", showPerformanceMonitor))
+ if (ImGui.MenuItem("Show Style Editor", "", isImGuiStyleEditorVisible))
{
- showPerformanceMonitor = !showPerformanceMonitor;
+ if (!isImGuiStyleEditorVisible)
+ {
+ ShowImGuiStyleEditor();
+ }
+ else
+ {
+ isImGuiStyleEditorVisible = false;
+ }
+ }
+
+ ImGui.Separator();
+
+ if (ImGui.MenuItem("Show Performance Monitor", "", isPerformanceMonitorVisible))
+ {
+ if (!isPerformanceMonitorVisible)
+ {
+ ShowPerformanceMonitor();
+ }
+ else
+ {
+ isPerformanceMonitorVisible = false;
+ }
}
ImGui.EndMenu();
@@ -723,7 +815,7 @@ internal static void RenderWindowContents(Action? tickDelegate, float dt)
ImGui.SetNextWindowPos(ImGui.GetMainViewport().WorkPos);
ImGuiStylePtr style = ImGui.GetStyle();
System.Numerics.Vector4 borderColor = style.Colors[(int)ImGuiCol.Border];
- if (ImGui.Begin("##mainWindow", ref b, ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings))
+ if (ImGui.Begin("##mainWindow", ref b, ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoBringToFrontOnFocus))
{
style.Colors[(int)ImGuiCol.Border] = borderColor;
tickDelegate?.Invoke(dt);
@@ -731,14 +823,57 @@ internal static void RenderWindowContents(Action? tickDelegate, float dt)
ImGui.End();
- if (showImGuiDemo)
+ if (isImGuiDemoVisible)
+ {
+ // Position the demo window to ensure it's visible and accessible
+ ImGui.SetNextWindowPos(new System.Numerics.Vector2(50, 50), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowSize(new System.Numerics.Vector2(800, 600), ImGuiCond.FirstUseEver);
+
+ // Focus on first frame after opening for good UX
+ if (shouldFocusImGuiDemo)
+ {
+ ImGui.SetNextWindowFocus();
+ shouldFocusImGuiDemo = false;
+ }
+
+ ImGui.ShowDemoWindow(ref isImGuiDemoVisible);
+ }
+
+ if (isImGuiMetricsVisible)
{
- ImGui.ShowDemoWindow(ref showImGuiDemo);
+ // Position the metrics window to ensure it's visible and accessible
+ ImGui.SetNextWindowPos(new System.Numerics.Vector2(100, 100), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowSize(new System.Numerics.Vector2(600, 500), ImGuiCond.FirstUseEver);
+
+ // Focus on first frame after opening for good UX
+ if (shouldFocusImGuiMetrics)
+ {
+ ImGui.SetNextWindowFocus();
+ shouldFocusImGuiMetrics = false;
+ }
+
+ ImGui.ShowMetricsWindow(ref isImGuiMetricsVisible);
}
- if (showImGuiMetrics)
+ if (isImGuiStyleEditorVisible)
{
- ImGui.ShowMetricsWindow(ref showImGuiMetrics);
+ // Position and configure the style editor window for better accessibility
+ ImGui.SetNextWindowPos(new System.Numerics.Vector2(150, 150), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowSize(new System.Numerics.Vector2(500, 400), ImGuiCond.FirstUseEver);
+
+ // Focus on first frame after opening for good UX
+ if (shouldFocusImGuiStyleEditor)
+ {
+ ImGui.SetNextWindowFocus();
+ shouldFocusImGuiStyleEditor = false;
+ }
+
+ if (ImGui.Begin("Style Editor", ref isImGuiStyleEditorVisible))
+ {
+ ImGui.ShowStyleEditor();
+ }
+
+ ImGui.End();
}
}
@@ -1198,9 +1333,16 @@ internal static void Reset()
IsIdle = false;
lastInputTime = DateTime.UtcNow;
targetFrameTimeMs = 1000.0 / 30.0;
- showImGuiMetrics = false;
- showImGuiDemo = false;
- showPerformanceMonitor = false;
+ isImGuiMetricsVisible = false;
+ isImGuiDemoVisible = false;
+ isImGuiStyleEditorVisible = false;
+ isPerformanceMonitorVisible = false;
+
+ shouldFocusImGuiDemo = false;
+ shouldFocusImGuiMetrics = false;
+ shouldFocusImGuiStyleEditor = false;
+ shouldFocusPerformanceMonitor = false;
+
performanceFrameTimes.Clear();
performanceFrameTimeSum = 0;
performanceFpsHistory.Clear();
@@ -1319,12 +1461,23 @@ internal static void UpdatePerformanceMonitoring(float dt)
///
internal static void RenderPerformanceMonitor()
{
- if (!showPerformanceMonitor)
+ if (!isPerformanceMonitorVisible)
{
return;
}
- if (ImGui.Begin("Performance Monitor", ref showPerformanceMonitor))
+ // Position the performance monitor window to ensure it's visible and accessible
+ ImGui.SetNextWindowPos(new System.Numerics.Vector2(200, 200), ImGuiCond.FirstUseEver);
+ ImGui.SetNextWindowSize(new System.Numerics.Vector2(500, 350), ImGuiCond.FirstUseEver);
+
+ // Focus on first frame after opening for good UX
+ if (shouldFocusPerformanceMonitor)
+ {
+ ImGui.SetNextWindowFocus();
+ shouldFocusPerformanceMonitor = false;
+ }
+
+ if (ImGui.Begin("Performance Monitor", ref isPerformanceMonitorVisible))
{
ImGui.TextWrapped("This window shows the current performance state and throttling behavior.");
ImGui.Separator();
diff --git a/ImGuiApp/ImGuiApp.csproj b/ImGuiApp/ImGuiApp.csproj
index 2ac3da5c..e9350d89 100644
--- a/ImGuiApp/ImGuiApp.csproj
+++ b/ImGuiApp/ImGuiApp.csproj
@@ -7,9 +7,10 @@
+
-
+
diff --git a/ImGuiApp/ImGuiAppTextureInfo.cs b/ImGuiApp/ImGuiAppTextureInfo.cs
index adc870bf..88960afd 100644
--- a/ImGuiApp/ImGuiAppTextureInfo.cs
+++ b/ImGuiApp/ImGuiAppTextureInfo.cs
@@ -4,7 +4,7 @@
namespace ktsu.ImGuiApp;
using Hexa.NET.ImGui;
-using ktsu.StrongPaths;
+using ktsu.Semantics;
///
/// Represents information about a texture, including its file path, texture ID, width, and height.
diff --git a/ImGuiApp/ImGuiController/ImGuiController.cs b/ImGuiApp/ImGuiController/ImGuiController.cs
index baf3f728..30f7055a 100644
--- a/ImGuiApp/ImGuiController/ImGuiController.cs
+++ b/ImGuiApp/ImGuiController/ImGuiController.cs
@@ -45,6 +45,8 @@ internal class ImGuiController : IDisposable
internal bool _disposed;
+ internal ImGuiContextPtr imGuiContextPtr;
+
///
/// Constructs a new ImGuiController.
///
@@ -102,8 +104,8 @@ public ImGuiController(GL gl, IView view, IInputContext input, ImGuiFontConfig?
SetPerFrameImGuiData(1f / 60f);
- DebugLogger.Log("ImGuiController: Beginning frame");
- BeginFrame();
+ DebugLogger.Log("ImGuiController: Setting up input handlers");
+ SetupInputHandlers();
DebugLogger.Log("ImGuiController: Initialization completed");
}
@@ -115,15 +117,24 @@ internal void Init(GL gl, IView view, IInputContext input)
_windowWidth = view.Size.X;
_windowHeight = view.Size.Y;
- ImGui.CreateContext();
+ imGuiContextPtr = ImGui.CreateContext();
+
+ // Initialize ImGui extensions (ImGuizmo, ImNodes, ImPlot) if available
+ ImGuiExtensionManager.Initialize();
+
+ // Create extension contexts and apply dark styles
+ ImGuiExtensionManager.CreateExtensionContexts();
+
+ SetupContext();
ImGui.StyleColorsDark();
}
- internal void BeginFrame()
+ ///
+ /// Sets up input event handlers. Should only be called once during initialization.
+ ///
+ internal void SetupInputHandlers()
{
- ImGui.NewFrame();
- _frameBegun = true;
_keyboard = _input?.Keyboards[0];
_mouse = _input?.Mice[0];
if (_view is not null)
@@ -147,6 +158,16 @@ internal void BeginFrame()
}
}
+ ///
+ /// Sets up ImGui context for the current frame. Should be called every frame before Update().
+ ///
+ internal void SetupContext()
+ {
+ // Set context for ImGui and extensions
+ ImGui.SetCurrentContext(imGuiContextPtr);
+ ImGuiExtensionManager.SetImGuiContext(imGuiContextPtr);
+ }
+
///
/// Delegate to receive keyboard key down events.
///
@@ -265,8 +286,13 @@ internal void Update(float deltaSeconds)
SetPerFrameImGuiData(deltaSeconds);
UpdateImGuiInput();
- _frameBegun = true;
+ // Start ImGui frame first
ImGui.NewFrame();
+ _frameBegun = true;
+
+ // IMPORTANT: Call extension BeginFrame methods AFTER ImGui.NewFrame()
+ // ImGuizmo, ImNodes, and ImPlot require ImGui to have started the frame first
+ ImGuiExtensionManager.BeginFrame();
}
///
@@ -933,6 +959,9 @@ protected virtual void Dispose(bool disposing)
_fontTexture.Dispose();
_shader.Dispose();
+ // Cleanup extension contexts before destroying ImGui context
+ ImGuiExtensionManager.Cleanup();
+
ImGui.DestroyContext();
}
}
diff --git a/ImGuiApp/ImGuiExtensionManager.cs b/ImGuiApp/ImGuiExtensionManager.cs
new file mode 100644
index 00000000..9ea78d46
--- /dev/null
+++ b/ImGuiApp/ImGuiExtensionManager.cs
@@ -0,0 +1,395 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp;
+
+using System.Reflection;
+using Hexa.NET.ImGui;
+
+///
+/// Manages automatic initialization and lifecycle of ImGui extensions like ImGuizmo, ImNodes, and ImPlot.
+///
+public static class ImGuiExtensionManager
+{
+ // Cached reflection info for performance
+ private static MethodInfo? imGuizmoBeginFrame;
+ private static MethodInfo? imGuizmoSetImGuiContext;
+ private static MethodInfo? imNodesBeginFrame;
+ private static MethodInfo? imNodesSetImGuiContext;
+ private static MethodInfo? imNodesCreateContext;
+ private static MethodInfo? imNodesSetCurrentContext;
+ private static MethodInfo? imNodesStyleColorsDark;
+ private static MethodInfo? imNodesGetStyle;
+ private static MethodInfo? imNodesDestroyContext;
+ private static MethodInfo? imPlotBeginFrame;
+ private static MethodInfo? imPlotSetImGuiContext;
+ private static MethodInfo? imPlotCreateContext;
+ private static MethodInfo? imPlotSetCurrentContext;
+ private static MethodInfo? imPlotStyleColorsDark;
+ private static MethodInfo? imPlotGetStyle;
+ private static MethodInfo? imPlotDestroyContext;
+
+ // Extension contexts
+ private static object? nodesContext;
+ private static object? plotContext;
+
+ private static bool initialized;
+
+ ///
+ /// Initialize extension detection. Called once during application startup.
+ ///
+ public static void Initialize()
+ {
+ if (initialized)
+ {
+ return;
+ }
+
+ initialized = true;
+
+ InitializeImGuizmo();
+ InitializeImNodes();
+ InitializeImPlot();
+ }
+
+ private static void InitializeImGuizmo()
+ {
+ try
+ {
+ Assembly imGuizmoAssembly = Assembly.Load("Hexa.NET.ImGuizmo");
+ Type? imGuizmoType = imGuizmoAssembly.GetType("Hexa.NET.ImGuizmo.ImGuizmo");
+ imGuizmoBeginFrame = imGuizmoType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+
+ // Find SetImGuiContext method that takes ImGuiContextPtr parameter
+ Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr");
+ if (imGuiContextPtrType != null)
+ {
+ imGuizmoSetImGuiContext = imGuizmoType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]);
+ }
+
+ if (imGuizmoBeginFrame != null)
+ {
+ DebugLogger.Log("ImGuiExtensionManager: ImGuizmo detected and will be auto-initialized");
+ }
+ }
+ catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException)
+ {
+ // ImGuizmo not available or has ambiguous methods - this is fine
+ DebugLogger.Log($"ImGuiExtensionManager: ImGuizmo not available or has issues: {ex.Message}");
+ }
+ }
+
+ private static void InitializeImNodes()
+ {
+ try
+ {
+ Assembly imNodesAssembly = Assembly.Load("Hexa.NET.ImNodes");
+ Type? imNodesType = imNodesAssembly.GetType("Hexa.NET.ImNodes.ImNodes");
+ imNodesBeginFrame = imNodesType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+ imNodesCreateContext = imNodesType?.GetMethod("CreateContext", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+ imNodesGetStyle = imNodesType?.GetMethod("GetStyle", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+
+ // Find SetImGuiContext method that takes ImGuiContextPtr parameter
+ Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr");
+ if (imGuiContextPtrType != null)
+ {
+ imNodesSetImGuiContext = imNodesType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]);
+ }
+
+ // Find context-related methods
+ if (imNodesCreateContext != null)
+ {
+ Type? contextType = imNodesCreateContext.ReturnType;
+ if (contextType != null)
+ {
+ imNodesSetCurrentContext = imNodesType?.GetMethod("SetCurrentContext", BindingFlags.Public | BindingFlags.Static, [contextType]);
+
+ // Find DestroyContext method that takes the context type parameter
+ imNodesDestroyContext = imNodesType?.GetMethod("DestroyContext", BindingFlags.Public | BindingFlags.Static, [contextType]);
+
+ // Find StyleColorsDark method that takes ImNodesStylePtr parameter
+ Type? styleType = imNodesAssembly.GetType("Hexa.NET.ImNodes.ImNodesStylePtr");
+ if (styleType != null)
+ {
+ imNodesStyleColorsDark = imNodesType?.GetMethod("StyleColorsDark", BindingFlags.Public | BindingFlags.Static, [styleType]);
+ }
+ }
+ }
+
+ if (imNodesBeginFrame != null)
+ {
+ DebugLogger.Log("ImGuiExtensionManager: ImNodes detected and will be auto-initialized");
+ }
+ }
+ catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException)
+ {
+ // ImNodes not available or has ambiguous methods - this is fine
+ DebugLogger.Log($"ImGuiExtensionManager: ImNodes not available or has issues: {ex.Message}");
+ }
+ }
+
+ private static void InitializeImPlot()
+ {
+ try
+ {
+ Assembly imPlotAssembly = Assembly.Load("Hexa.NET.ImPlot");
+ Type? imPlotType = imPlotAssembly.GetType("Hexa.NET.ImPlot.ImPlot");
+ imPlotBeginFrame = imPlotType?.GetMethod("BeginFrame", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+ imPlotCreateContext = imPlotType?.GetMethod("CreateContext", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+ imPlotGetStyle = imPlotType?.GetMethod("GetStyle", BindingFlags.Public | BindingFlags.Static, Type.EmptyTypes);
+
+ // Find SetImGuiContext method that takes ImGuiContextPtr parameter
+ Type? imGuiContextPtrType = Assembly.Load("Hexa.NET.ImGui").GetType("Hexa.NET.ImGui.ImGuiContextPtr");
+ if (imGuiContextPtrType != null)
+ {
+ imPlotSetImGuiContext = imPlotType?.GetMethod("SetImGuiContext", BindingFlags.Public | BindingFlags.Static, [imGuiContextPtrType]);
+ }
+
+ // Find context-related methods
+ if (imPlotCreateContext != null)
+ {
+ Type? contextType = imPlotCreateContext.ReturnType;
+ if (contextType != null)
+ {
+ imPlotSetCurrentContext = imPlotType?.GetMethod("SetCurrentContext", BindingFlags.Public | BindingFlags.Static, [contextType]);
+
+ // Find DestroyContext method that takes the context type parameter
+ imPlotDestroyContext = imPlotType?.GetMethod("DestroyContext", BindingFlags.Public | BindingFlags.Static, [contextType]);
+
+ // Find StyleColorsDark method that takes ImPlotStylePtr parameter
+ Type? styleType = imPlotAssembly.GetType("Hexa.NET.ImPlot.ImPlotStylePtr");
+ if (styleType != null)
+ {
+ imPlotStyleColorsDark = imPlotType?.GetMethod("StyleColorsDark", BindingFlags.Public | BindingFlags.Static, [styleType]);
+ }
+ }
+ }
+
+ if (imPlotBeginFrame != null)
+ {
+ DebugLogger.Log("ImGuiExtensionManager: ImPlot detected and will be auto-initialized");
+ }
+ }
+ catch (Exception ex) when (ex is FileNotFoundException or FileLoadException or BadImageFormatException or AmbiguousMatchException)
+ {
+ // ImPlot not available or has ambiguous methods - this is fine
+ DebugLogger.Log($"ImGuiExtensionManager: ImPlot not available or has issues: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Call BeginFrame for all detected extensions. Should be called once per frame after ImGui.NewFrame().
+ ///
+ public static void BeginFrame()
+ {
+ // Call ImGuizmo.BeginFrame() if available
+ try
+ {
+ imGuizmoBeginFrame?.Invoke(null, null);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImGuizmo.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+
+ // Call ImNodes.BeginFrame() if available
+ try
+ {
+ imNodesBeginFrame?.Invoke(null, null);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImNodes.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+
+ // Call ImPlot.BeginFrame() if available
+ try
+ {
+ imPlotBeginFrame?.Invoke(null, null);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImPlot.BeginFrame(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+
+ ///
+ /// Sets the ImGui context for all detected extensions. Should be called once after ImGui.CreateContext().
+ ///
+ /// The ImGui context to set for the extensions. If null, uses the current context.
+ public static void SetImGuiContext(ImGuiContextPtr? context = null)
+ {
+ // Use current context if none provided
+ ImGuiContextPtr contextToSet = context ?? ImGui.GetCurrentContext();
+
+ // Set context for ImGuizmo if available
+ try
+ {
+ imGuizmoSetImGuiContext?.Invoke(null, [contextToSet]);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImGuizmo.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+
+ // Set context for ImNodes if available
+ try
+ {
+ imNodesSetImGuiContext?.Invoke(null, [contextToSet]);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImNodes.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+
+ // Set context for ImPlot if available
+ try
+ {
+ imPlotSetImGuiContext?.Invoke(null, [contextToSet]);
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error calling ImPlot.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+
+ ///
+ /// Creates extension contexts and applies dark styles. Should be called once after SetImGuiContext().
+ ///
+ public static void CreateExtensionContexts()
+ {
+ // Create and set ImNodes context and set style
+ if (imNodesCreateContext != null && imNodesSetCurrentContext != null)
+ {
+ try
+ {
+ nodesContext = imNodesCreateContext.Invoke(null, null);
+ if (nodesContext != null)
+ {
+ imNodesSetCurrentContext.Invoke(null, [nodesContext]);
+
+ // Apply dark style if available
+ if (imNodesStyleColorsDark != null && imNodesGetStyle != null)
+ {
+ object? style = imNodesGetStyle.Invoke(null, null);
+ if (style != null)
+ {
+ imNodesStyleColorsDark.Invoke(null, [style]);
+ }
+ }
+
+ DebugLogger.Log("ImGuiExtensionManager: ImNodes context created and dark style applied");
+ }
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error creating ImNodes context: {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+
+ // Create and set ImPlot context and set style
+ if (imPlotCreateContext != null && imPlotSetCurrentContext != null)
+ {
+ try
+ {
+ plotContext = imPlotCreateContext.Invoke(null, null);
+ if (plotContext != null)
+ {
+ imPlotSetCurrentContext.Invoke(null, [plotContext]);
+
+ // Apply dark style if available
+ if (imPlotStyleColorsDark != null && imPlotGetStyle != null)
+ {
+ object? style = imPlotGetStyle.Invoke(null, null);
+ if (style != null)
+ {
+ imPlotStyleColorsDark.Invoke(null, [style]);
+ }
+ }
+
+ DebugLogger.Log("ImGuiExtensionManager: ImPlot context created and dark style applied");
+ }
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error creating ImPlot context: {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Cleanup extension contexts. Should be called during application shutdown.
+ ///
+ public static void Cleanup()
+ {
+ // Clear ImGuizmo context by setting it to null to free any internal state
+ if (imGuizmoSetImGuiContext != null)
+ {
+ try
+ {
+ imGuizmoSetImGuiContext.Invoke(null, [default(ImGuiContextPtr)]);
+ DebugLogger.Log("ImGuiExtensionManager: ImGuizmo context cleared");
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error clearing ImGuizmo context: {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+
+ // Destroy ImNodes context if created
+ if (nodesContext != null && imNodesDestroyContext != null)
+ {
+ try
+ {
+ imNodesDestroyContext.Invoke(null, [nodesContext]);
+ nodesContext = null;
+ DebugLogger.Log("ImGuiExtensionManager: ImNodes context destroyed");
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error destroying ImNodes context: {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+
+ // Destroy ImPlot context if created
+ if (plotContext != null && imPlotDestroyContext != null)
+ {
+ try
+ {
+ imPlotDestroyContext.Invoke(null, [plotContext]);
+ plotContext = null;
+ DebugLogger.Log("ImGuiExtensionManager: ImPlot context destroyed");
+ }
+ catch (TargetInvocationException ex)
+ {
+ DebugLogger.Log($"ImGuiExtensionManager: Error destroying ImPlot context: {ex.InnerException?.Message ?? ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Gets whether ImGuizmo is available and initialized.
+ ///
+ public static bool IsImGuizmoAvailable => imGuizmoBeginFrame != null;
+
+ ///
+ /// Gets whether ImNodes is available and initialized.
+ ///
+ public static bool IsImNodesAvailable => imNodesBeginFrame != null;
+
+ ///
+ /// Gets whether ImPlot is available and initialized.
+ ///
+ public static bool IsImPlotAvailable => imPlotBeginFrame != null;
+
+ ///
+ /// Gets whether ImNodes context has been created.
+ ///
+ public static bool IsImNodesContextCreated => nodesContext != null;
+
+ ///
+ /// Gets whether ImPlot context has been created.
+ ///
+ public static bool IsImPlotContextCreated => plotContext != null;
+}
diff --git a/ImGuiApp/PidFrameLimiter.cs b/ImGuiApp/PidFrameLimiter.cs
index 90b88913..be14c77a 100644
--- a/ImGuiApp/PidFrameLimiter.cs
+++ b/ImGuiApp/PidFrameLimiter.cs
@@ -538,17 +538,6 @@ public void LimitFrameRate(double targetFrameTimeMs)
{
HandleAutoTuningProgression(error);
}
-
- // Optional: Log PID state for debugging (remove in production)
-#if DEBUG
- if (DateTime.UtcNow.Millisecond % 100 < 16) // Log roughly every 100ms
- {
- Debug.WriteLine(
- $"PID Frame Limiter - Target: {targetFrameTimeMs:F1}ms, " +
- $"Actual: {smoothedFrameTime:F1}ms, Error: {error:F1}ms, " +
- $"Sleep: {baseSleepMs:F1}ms, P: {CurrentKp * error:F2}, I: {CurrentKi * integral:F2}, D: {CurrentKd * derivative:F2}");
- }
-#endif
}
internal static double CalculateStability(List errors)
diff --git a/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs b/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs
new file mode 100644
index 00000000..b93681ed
--- /dev/null
+++ b/ImGuiAppDemo/Demos/AdvancedWidgetsDemo.cs
@@ -0,0 +1,89 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+
+///
+/// Demo for advanced ImGui widgets
+///
+internal sealed class AdvancedWidgetsDemo : IDemoTab
+{
+ private Vector3 colorPickerValue = new(0.4f, 0.7f, 0.2f);
+ private Vector4 color4Value = new(1.0f, 0.5f, 0.2f, 1.0f);
+ private float animationTime;
+
+ public string TabName => "Advanced Widgets";
+
+ public void Update(float deltaTime) => animationTime += deltaTime;
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ // Color controls
+ ImGui.SeparatorText("Color Controls:");
+ ImGui.ColorEdit3("Color RGB", ref colorPickerValue);
+ ImGui.ColorEdit4("Color RGBA", ref color4Value);
+ ImGui.SetNextItemWidth(200.0f);
+ ImGui.ColorPicker3("Color Picker", ref colorPickerValue);
+
+ // Tree view
+ ImGui.SeparatorText("Tree View:");
+ if (ImGui.TreeNode("Root Node"))
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ string nodeName = $"Child Node {i}";
+ bool nodeOpen = ImGui.TreeNode(nodeName);
+
+ if (i == 2 && nodeOpen)
+ {
+ for (int j = 0; j < 3; j++)
+ {
+ if (ImGui.TreeNode($"Grandchild {j}"))
+ {
+ ImGui.Text($"Leaf item {j}");
+ ImGui.TreePop();
+ }
+ }
+ }
+ else if (nodeOpen)
+ {
+ ImGui.Text($"Content of {nodeName}");
+ }
+
+ if (nodeOpen)
+ {
+ ImGui.TreePop();
+ }
+ }
+ ImGui.TreePop();
+ }
+
+ // Progress bars and loading indicators
+ ImGui.SeparatorText("Progress Indicators:");
+ float progress = ((float)Math.Sin(animationTime * 2.0) * 0.5f) + 0.5f;
+ ImGui.ProgressBar(progress, new Vector2(-1, 0), $"{progress * 100:F1}%");
+
+ // Spinner-like effect
+ ImGui.Text("Loading...");
+ ImGui.SameLine();
+ for (int i = 0; i < 8; i++)
+ {
+ float rotation = (animationTime * 5.0f) + (i * MathF.PI / 4.0f);
+ float alpha = (MathF.Sin(rotation) + 1.0f) * 0.5f;
+ ImGui.TextColored(new Vector4(1, 1, 1, alpha), "●");
+ if (i < 7)
+ {
+ ImGui.SameLine();
+ }
+ }
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/AnimationDemo.cs b/ImGuiAppDemo/Demos/AnimationDemo.cs
new file mode 100644
index 00000000..fcf930de
--- /dev/null
+++ b/ImGuiAppDemo/Demos/AnimationDemo.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+
+///
+/// Demo for animations and effects
+///
+internal sealed class AnimationDemo : IDemoTab
+{
+ private float animationTime;
+ private float bounceOffset;
+ private float pulseScale = 1.0f;
+ private float textSpeed = 50.0f;
+
+ public string TabName => "Animation & Effects";
+
+ public void Update(float deltaTime)
+ {
+ animationTime += deltaTime;
+
+ // Bouncing animation
+ bounceOffset = MathF.Abs(MathF.Sin(animationTime * 3)) * 50;
+
+ // Pulse animation
+ pulseScale = 0.8f + (0.4f * MathF.Sin(animationTime * 4));
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.SeparatorText("Animation Examples:");
+
+ // Simple animations
+ ImGui.Text("Bouncing Animation:");
+ Vector2 ballPos = ImGui.GetCursorScreenPos();
+ ballPos.Y += bounceOffset;
+ ImDrawListPtr drawList = ImGui.GetWindowDrawList();
+ drawList.AddCircleFilled(ballPos + new Vector2(50, 50), 20, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 0, 1)));
+ ImGui.Dummy(new Vector2(100, 100));
+
+ // Pulsing element
+ ImGui.Text("Pulse Animation:");
+ Vector2 pulsePos = ImGui.GetCursorScreenPos();
+ float pulseSize = 20 * pulseScale;
+ drawList.AddCircleFilled(pulsePos + new Vector2(50, 50), pulseSize,
+ ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0, 1, 0.7f)));
+ ImGui.Dummy(new Vector2(100, 100));
+
+ ImGui.SeparatorText("Animated Text:");
+ ImGui.SliderFloat("Text Speed", ref textSpeed, 10.0f, 200.0f);
+
+ for (int i = 0; i < 20; i++)
+ {
+ float wave = (MathF.Sin((animationTime * 3.0f) + (i * 0.5f)) * 0.5f) + 0.5f;
+ ImGui.TextColored(new Vector4(wave, 1.0f - wave, 0.5f, 1.0f), i % 5 == 4 ? " " : "▓");
+ if (i % 5 != 4)
+ {
+ ImGui.SameLine();
+ }
+ }
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs b/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs
new file mode 100644
index 00000000..4f8d96bd
--- /dev/null
+++ b/ImGuiAppDemo/Demos/BasicWidgetsDemo.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+
+///
+/// Demo for basic ImGui widgets
+///
+internal sealed class BasicWidgetsDemo : IDemoTab
+{
+ private float sliderValue = 0.5f;
+ private int counter;
+ private bool checkboxState;
+ private string inputText = "Type here...";
+ private int comboSelection;
+ private readonly string[] comboItems = ["Item 1", "Item 2", "Item 3", "Item 4"];
+ private int listboxSelection;
+ private readonly string[] listboxItems = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
+ private float dragFloat = 1.0f;
+ private int dragInt = 50;
+ private Vector3 dragVector = new(1.0f, 2.0f, 3.0f);
+ private float angle;
+ private int radioSelection;
+
+ public string TabName => "Basic Widgets";
+
+ public void Update(float deltaTime)
+ {
+ // No updates needed for basic widgets
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.TextWrapped("This tab demonstrates basic ImGui widgets and controls.");
+
+ // Buttons
+ ImGui.SeparatorText("Buttons:");
+ if (ImGui.Button("Regular Button"))
+ {
+ counter++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Small"))
+ {
+ counter++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.ArrowButton("##left", ImGuiDir.Left))
+ {
+ counter--;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.ArrowButton("##right", ImGuiDir.Right))
+ {
+ counter++;
+ }
+
+ ImGui.SameLine();
+ ImGui.Text($"Counter: {counter}");
+
+ // Checkboxes and Radio buttons
+ ImGui.SeparatorText("Selection Controls");
+ ImGui.Checkbox("Checkbox", ref checkboxState);
+
+ ImGui.RadioButton("Option 1", ref radioSelection, 0);
+ ImGui.SameLine();
+ ImGui.RadioButton("Option 2", ref radioSelection, 1);
+ ImGui.SameLine();
+ ImGui.RadioButton("Option 3", ref radioSelection, 2);
+
+ // Sliders
+ ImGui.SeparatorText("Sliders");
+ ImGui.SliderFloat("Float Slider", ref sliderValue, 0.0f, 1.0f);
+ ImGui.SliderFloat("Angle", ref angle, 0.0f, 360.0f, "%.1f deg");
+ ImGui.SliderInt("Int Slider", ref dragInt, 0, 100);
+
+ // Input fields
+ ImGui.SeparatorText("Input Fields");
+ ImGui.InputText("Text Input", ref inputText, 100);
+ ImGui.InputFloat("Float Input", ref dragFloat);
+ ImGui.InputFloat3("Vector3 Input", ref dragVector);
+
+ // Combo boxes
+ ImGui.SeparatorText("Dropdowns");
+ ImGui.Combo("Combo Box", ref comboSelection, comboItems, comboItems.Length);
+ ImGui.ListBox("List Box", ref listboxSelection, listboxItems, listboxItems.Length, 4);
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/DataVisualizationDemo.cs b/ImGuiAppDemo/Demos/DataVisualizationDemo.cs
new file mode 100644
index 00000000..8a07ae3e
--- /dev/null
+++ b/ImGuiAppDemo/Demos/DataVisualizationDemo.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+using ktsu.ImGuiApp;
+using ktsu.ImGuiAppDemo.Properties;
+
+///
+/// Demo for data visualization features
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")]
+internal sealed class DataVisualizationDemo : IDemoTab
+{
+ private readonly List plotValues = [];
+ private readonly Random random = new();
+ private float plotRefreshTime;
+
+ public string TabName => "Data Visualization";
+
+ public void Update(float deltaTime)
+ {
+ // Update plot data
+ plotRefreshTime += deltaTime;
+ if (plotRefreshTime >= 0.1f) // Update every 100ms
+ {
+ plotRefreshTime = 0;
+ plotValues.Add((float)random.NextDouble());
+ if (plotValues.Count > 100) // Keep last 100 values
+ {
+ plotValues.RemoveAt(0);
+ }
+ }
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.SeparatorText("Real-time Data Plots:");
+
+ // Line plot
+ if (plotValues.Count > 0)
+ {
+ float[] values = [.. plotValues];
+ ImGui.PlotLines("Random Values", ref values[0], values.Length, 0,
+ $"Current: {values[^1]:F2}", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100));
+
+ ImGui.PlotHistogram("Distribution", ref values[0], values.Length, 0,
+ "Histogram", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100));
+ }
+
+ // Performance note
+ ImGui.SeparatorText("Performance Metrics:");
+ ImGui.TextWrapped("Performance monitoring is now available in the Debug menu! Use 'Debug > Show Performance Monitor' to see real-time FPS graphs and throttling state.");
+
+ // Font demonstrations
+ ImGui.SeparatorText("Custom Font Rendering:");
+ using (new FontAppearance(nameof(Resources.CARDCHAR), 16))
+ {
+ ImGui.Text("Small custom font text");
+ }
+
+ using (new FontAppearance(nameof(Resources.CARDCHAR), 24))
+ {
+ ImGui.Text("Medium custom font text");
+ }
+
+ using (new FontAppearance(nameof(Resources.CARDCHAR), 32))
+ {
+ ImGui.Text("Large custom font text");
+ }
+
+ // Text formatting examples
+ ImGui.SeparatorText("Text Formatting:");
+ ImGui.TextColored(new Vector4(1, 0, 0, 1), "Red text");
+ ImGui.TextColored(new Vector4(0, 1, 0, 1), "Green text");
+ ImGui.TextColored(new Vector4(0, 0, 1, 1), "Blue text");
+ ImGui.TextWrapped("This is a long line of text that should wrap to multiple lines when the window is not wide enough to contain it all on a single line.");
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/GraphicsDemo.cs b/ImGuiAppDemo/Demos/GraphicsDemo.cs
new file mode 100644
index 00000000..a356a3ef
--- /dev/null
+++ b/ImGuiAppDemo/Demos/GraphicsDemo.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+using ktsu.Extensions;
+using ktsu.ImGuiApp;
+using ktsu.Semantics;
+
+///
+/// Demo for graphics and drawing capabilities
+///
+internal sealed class GraphicsDemo : IDemoTab
+{
+ private readonly List canvasPoints = [];
+ private Vector4 drawColor = new(1.0f, 1.0f, 0.0f, 1.0f);
+ private float brushSize = 5.0f;
+ private float animationTime;
+
+ public string TabName => "Graphics & Drawing";
+
+ public void Update(float deltaTime) => animationTime += deltaTime;
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ // Image display
+ AbsoluteFilePath iconPath = AppContext.BaseDirectory.As() / "icon.png".As();
+ ImGuiAppTextureInfo iconTexture = ImGuiApp.GetOrLoadTexture(iconPath);
+
+ ImGui.SeparatorText("Image Display:");
+ ImGui.Image(iconTexture.TextureRef, new Vector2(64, 64));
+ ImGui.SameLine();
+ ImGui.Image(iconTexture.TextureRef, new Vector2(32, 32));
+ ImGui.SameLine();
+ ImGui.Image(iconTexture.TextureRef, new Vector2(16, 16));
+
+ // Custom drawing with ImDrawList
+ ImGui.SeparatorText("Custom Drawing Canvas:");
+ ImGui.ColorEdit4("Draw Color", ref drawColor);
+ ImGui.SliderFloat("Brush Size", ref brushSize, 1.0f, 20.0f);
+
+ if (ImGui.Button("Clear Canvas"))
+ {
+ canvasPoints.Clear();
+ }
+
+ Vector2 canvasPos = ImGui.GetCursorScreenPos();
+ Vector2 canvasSize = new(400, 200);
+
+ // Draw canvas background
+ ImDrawListPtr drawList = ImGui.GetWindowDrawList();
+ drawList.AddRectFilled(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.1f, 0.1f, 0.1f, 1.0f)));
+ drawList.AddRect(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0.5f, 0.5f, 1.0f)));
+
+ // Handle mouse input for drawing
+ ImGui.InvisibleButton("Canvas", canvasSize);
+ if (ImGui.IsItemActive() && ImGui.IsMouseDown(ImGuiMouseButton.Left))
+ {
+ Vector2 mousePos = ImGui.GetMousePos() - canvasPos;
+ if (mousePos.X >= 0 && mousePos.Y >= 0 && mousePos.X <= canvasSize.X && mousePos.Y <= canvasSize.Y)
+ {
+ canvasPoints.Add(mousePos);
+ }
+ }
+
+ // Draw points
+ uint color = ImGui.ColorConvertFloat4ToU32(drawColor);
+ foreach (Vector2 point in canvasPoints)
+ {
+ drawList.AddCircleFilled(canvasPos + point, brushSize, color);
+ }
+
+ // Draw some simple shapes for demonstration
+ ImGui.SeparatorText("Shape Examples:");
+ Vector2 shapeStart = ImGui.GetCursorScreenPos();
+
+ // Simple animated circle
+ float t = animationTime;
+ Vector2 center = shapeStart + new Vector2(100, 50);
+ float radius = 20 + (MathF.Sin(t * 2) * 5);
+ drawList.AddCircle(center, radius, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 1)), 16, 2.0f);
+
+ // Moving rectangle
+ Vector2 rectPos = shapeStart + new Vector2(200 + (MathF.Sin(t) * 30), 30);
+ drawList.AddRectFilled(rectPos, rectPos + new Vector2(40, 40), ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 0, 0.7f)));
+
+ ImGui.Dummy(new Vector2(400, 100)); // Reserve space
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/IDemoTab.cs b/ImGuiAppDemo/Demos/IDemoTab.cs
new file mode 100644
index 00000000..9b805970
--- /dev/null
+++ b/ImGuiAppDemo/Demos/IDemoTab.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+///
+/// Interface for demo tab implementations
+///
+internal interface IDemoTab
+{
+ ///
+ /// Gets the name of the tab to display in the UI
+ ///
+ public string TabName { get; }
+
+ ///
+ /// Renders the demo tab content
+ ///
+ public void Render();
+
+ ///
+ /// Updates the demo state (called each frame)
+ ///
+ /// Time elapsed since last frame
+ public void Update(float deltaTime);
+}
diff --git a/ImGuiAppDemo/Demos/ImGuizmoDemo.cs b/ImGuiAppDemo/Demos/ImGuizmoDemo.cs
new file mode 100644
index 00000000..9a02d7b4
--- /dev/null
+++ b/ImGuiAppDemo/Demos/ImGuizmoDemo.cs
@@ -0,0 +1,127 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System;
+using System.Numerics;
+using Hexa.NET.ImGui;
+using Hexa.NET.ImGuizmo;
+
+///
+/// Demo for ImGuizmo 3D manipulation gizmos
+///
+internal sealed class ImGuizmoDemo : IDemoTab
+{
+ private Matrix4x4 gizmoTransform = Matrix4x4.Identity;
+ private Matrix4x4 gizmoView = Matrix4x4.CreateLookAt(new Vector3(0, 0, 5), Vector3.Zero, Vector3.UnitY);
+ private Matrix4x4 gizmoProjection;
+ private ImGuizmoOperation gizmoOperation = ImGuizmoOperation.Translate;
+ private ImGuizmoMode gizmoMode = ImGuizmoMode.Local;
+ private bool gizmoEnabled = true;
+ private float animationTime;
+
+ public string TabName => "ImGuizmo 3D Gizmos";
+
+ public void Update(float deltaTime)
+ {
+ animationTime += deltaTime;
+
+ // Update gizmo view matrix for rotation demo
+ float cameraAngle = animationTime * 0.2f;
+ Vector3 cameraPos = new(MathF.Sin(cameraAngle) * 5f, 3f, MathF.Cos(cameraAngle) * 5f);
+ gizmoView = Matrix4x4.CreateLookAt(cameraPos, Vector3.Zero, Vector3.UnitY);
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.TextWrapped("ImGuizmo provides 3D manipulation gizmos for translate, rotate, and scale operations.");
+
+ // Gizmo controls
+ ImGui.SeparatorText("Gizmo Controls:.");
+ ImGui.Checkbox("Enable Gizmo", ref gizmoEnabled);
+
+ // Operation selection
+ string[] operationNames = Enum.GetNames();
+ ImGuizmoOperation[] operations = Enum.GetValues();
+ int opIndex = Array.IndexOf(operations, gizmoOperation);
+ if (ImGui.Combo("Operation", ref opIndex, operationNames, operationNames.Length))
+ {
+ gizmoOperation = operations[opIndex];
+ }
+
+ // Mode selection
+ string[] modeNames = Enum.GetNames();
+ ImGuizmoMode[] modes = Enum.GetValues();
+ int modeIndex = Array.IndexOf(modes, gizmoMode);
+ if (ImGui.Combo("Mode", ref modeIndex, modeNames, modeNames.Length))
+ {
+ gizmoMode = modes[modeIndex];
+ }
+
+ // Display transform matrix values
+ ImGui.SeparatorText("Transform Matrix:");
+ ImGui.Text($"[{gizmoTransform.M11:F2}, {gizmoTransform.M12:F2}, {gizmoTransform.M13:F2}, {gizmoTransform.M14:F2}]");
+ ImGui.Text($"[{gizmoTransform.M21:F2}, {gizmoTransform.M22:F2}, {gizmoTransform.M23:F2}, {gizmoTransform.M24:F2}]");
+ ImGui.Text($"[{gizmoTransform.M31:F2}, {gizmoTransform.M32:F2}, {gizmoTransform.M33:F2}, {gizmoTransform.M34:F2}]");
+ ImGui.Text($"[{gizmoTransform.M41:F2}, {gizmoTransform.M42:F2}, {gizmoTransform.M43:F2}, {gizmoTransform.M44:F2}]");
+
+ if (ImGui.Button("Reset Transform"))
+ {
+ gizmoTransform = Matrix4x4.Identity;
+ }
+
+ // Gizmo viewport - use a reasonable size instead of all available space
+ Vector2 availableSize = ImGui.GetContentRegionAvail();
+ Vector2 gizmoSize = new(availableSize.X, Math.Min(availableSize.Y, availableSize.X * 0.6f)); // Use width-based aspect ratio
+
+ // Get position for gizmo (NO space reservation - see what happens!)
+ Vector2 gizmoPos = ImGui.GetCursorScreenPos();
+
+ // Update projection matrix based on gizmo size
+ float aspectRatio = gizmoSize.Y > 0 ? gizmoSize.X / gizmoSize.Y : 1.0f;
+ gizmoProjection = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 4f, aspectRatio, 0.1f, 100f);
+
+ // Set up ImGuizmo for this viewport
+ if (gizmoEnabled)
+ {
+ // IMPORTANT: Set the drawlist and enable ImGuizmo and set the rect, before using any ImGuizmo functions
+ ImGuizmo.SetDrawlist();
+ ImGuizmo.Enable(true);
+ ImGuizmo.SetRect(gizmoPos.X, gizmoPos.Y, gizmoSize.X, gizmoSize.Y);
+
+ // Create view and projection matrices for the gizmo
+ Matrix4x4 view = gizmoView;
+ Matrix4x4 proj = gizmoProjection;
+
+ // Draw grid
+ Matrix4x4 identity = Matrix4x4.Identity;
+ ImGuizmo.DrawGrid(ref view, ref proj, ref identity, 10.0f);
+
+ // IMPORTANT: Use ID management for proper gizmo isolation
+ ImGuizmo.PushID(0);
+
+ // Draw the gizmo
+ Matrix4x4 transform = gizmoTransform;
+ if (ImGuizmo.Manipulate(ref view, ref proj, gizmoOperation, gizmoMode, ref transform))
+ {
+ gizmoTransform = transform;
+ }
+
+ ImGuizmo.PopID();
+ }
+
+ // Display gizmo state below the gizmo area
+ if (gizmoEnabled)
+ {
+ ImGui.Text($"Gizmo Over: {ImGuizmo.IsOver()}");
+ ImGui.Text($"Gizmo Using: {ImGuizmo.IsUsing()}");
+ }
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/ImNodesDemo.cs b/ImGuiAppDemo/Demos/ImNodesDemo.cs
new file mode 100644
index 00000000..60029439
--- /dev/null
+++ b/ImGuiAppDemo/Demos/ImNodesDemo.cs
@@ -0,0 +1,1365 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+using Hexa.NET.ImNodes;
+
+///
+/// Demo for ImNodes node editor
+///
+internal sealed class ImNodesDemo : IDemoTab
+{
+ private int nextNodeId = 1;
+ private int nextLinkId = 1;
+ private readonly List nodes = [];
+ private readonly List links = [];
+ private bool initialPositionsSet;
+ private bool automaticLayout;
+ private readonly Dictionary nodeVelocities = [];
+ private readonly Dictionary nodeForces = [];
+ private Vector2? physicsCenter; // Cached center point for physics simulation
+ private bool showDebugVisualization; // Show physics debug info
+
+ // Debug information
+ private List linkFixLog = [];
+ private string linkFixSummary = "";
+
+ // Physics parameters
+ private float repulsionStrength = 5000.0f;
+ private float attractionStrength = 0.5f;
+ private float centerForce = 0.12f;
+ private float idealLinkDistance = 200.0f;
+ private float damping = 0.8f;
+ private float maxVelocity = 200.0f;
+
+ private sealed record SimpleNode(int Id, Vector2 Position, string Name, List InputPins, List OutputPins, Vector2 Dimensions);
+ private sealed record SimpleLink(int Id, int InputPinId, int OutputPinId);
+
+ public string TabName => "ImNodes Editor";
+
+ public ImNodesDemo()
+ {
+ // Initialize demo data for ImNodes with better spacing
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(100, 150), "Input Node", [], [1, 2], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 100), "Process Node A", [3], [4, 5], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 250), "Process Node B", [6], [7], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(700, 175), "Output Node", [8, 9], [], Vector2.Zero));
+
+ // Create some demo links showing a more complex flow
+ links.Add(new SimpleLink(nextLinkId++, 1, 3)); // Input to Process A
+ links.Add(new SimpleLink(nextLinkId++, 2, 6)); // Input to Process B
+ links.Add(new SimpleLink(nextLinkId++, 4, 8)); // Process A to Output
+ links.Add(new SimpleLink(nextLinkId++, 7, 9)); // Process B to Output
+
+ // Update nextNodeId to account for all the pin IDs we used
+ nextNodeId = 10;
+ }
+
+ public void Update(float deltaTime)
+ {
+ if (automaticLayout && nodes.Count > 0)
+ {
+ // If we just enabled automatic layout, reset positioning flags
+ if (initialPositionsSet)
+ {
+ initialPositionsSet = false;
+ }
+ UpdatePhysicsSimulation(deltaTime);
+ }
+ else if (!automaticLayout)
+ {
+ // Reset physics center when automatic layout is disabled
+ physicsCenter = null;
+ }
+ }
+
+ private void UpdatePhysicsSimulation(float deltaTime)
+ {
+ // Initialize forces and velocities for new nodes
+ foreach (SimpleNode node in nodes)
+ {
+ if (!nodeForces.ContainsKey(node.Id))
+ {
+ nodeForces[node.Id] = Vector2.Zero;
+ nodeVelocities[node.Id] = Vector2.Zero;
+ }
+ }
+
+ // Calculate all forces
+ CalculateForces();
+
+ // Apply forces and update positions
+ ApplyForces(deltaTime);
+ }
+
+ private void CalculateForces()
+ {
+ // Reset all forces
+ foreach (int nodeId in nodeForces.Keys.ToList())
+ {
+ nodeForces[nodeId] = Vector2.Zero;
+ }
+
+ // Node repulsion forces (nodes push each other away)
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ for (int j = i + 1; j < nodes.Count; j++)
+ {
+ SimpleNode nodeA = nodes[i];
+ SimpleNode nodeB = nodes[j];
+
+ // Use node centers for physics calculations
+ Vector2 centerA = nodeA.Position + (nodeA.Dimensions * 0.5f);
+ Vector2 centerB = nodeB.Position + (nodeB.Dimensions * 0.5f);
+ Vector2 direction = centerA - centerB;
+ float distance = direction.Length();
+
+ if (distance > 0)
+ {
+ // Repulsion force decreases with distance
+ float force = repulsionStrength / ((distance * distance) + 1.0f);
+ Vector2 forceVector = Vector2.Normalize(direction) * force;
+
+ nodeForces[nodeA.Id] += forceVector;
+ nodeForces[nodeB.Id] -= forceVector;
+ }
+ }
+ }
+
+ // Link attraction forces (connected nodes pull toward each other)
+ foreach (SimpleLink link in links)
+ {
+ SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId);
+ SimpleNode? endNode = GetNodeByInputPin(link.InputPinId);
+
+ if (startNode != null && endNode != null)
+ {
+ // Use node centers for physics calculations
+ Vector2 startCenter = startNode.Position + (startNode.Dimensions * 0.5f);
+ Vector2 endCenter = endNode.Position + (endNode.Dimensions * 0.5f);
+ Vector2 direction = endCenter - startCenter;
+ float distance = direction.Length();
+
+ if (distance > 0)
+ {
+ // Attraction force - stronger for longer links
+ float force = (distance - idealLinkDistance) * attractionStrength;
+ Vector2 forceVector = Vector2.Normalize(direction) * force;
+
+ nodeForces[startNode.Id] += forceVector;
+ nodeForces[endNode.Id] -= forceVector;
+ }
+ }
+ }
+
+ // Center attraction (pull all nodes toward the canvas origin)
+ // Initialize physics center if not set - always use canvas origin (0,0)
+ if (!physicsCenter.HasValue)
+ {
+ physicsCenter = Vector2.Zero; // Canvas origin (0,0)
+ }
+
+ // Use the physics center (canvas origin)
+ Vector2 editorCenter = physicsCenter.Value;
+ foreach (SimpleNode node in nodes)
+ {
+ // Use node center for physics calculations
+ Vector2 nodeCenter = node.Position + (node.Dimensions * 0.5f);
+ Vector2 toCenter = editorCenter - nodeCenter;
+ float distance = toCenter.Length();
+ if (distance > 10.0f) // Small deadzone to prevent jittering at center
+ {
+ // Centering force that increases with distance from center
+ float force = distance * centerForce;
+ nodeForces[node.Id] += Vector2.Normalize(toCenter) * force;
+ }
+ }
+ }
+
+ private void ApplyForces(float deltaTime)
+ {
+
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ SimpleNode node = nodes[i];
+ Vector2 force = nodeForces[node.Id];
+
+ // Update velocity (F = ma, assuming mass = 1)
+ nodeVelocities[node.Id] += force * deltaTime;
+ nodeVelocities[node.Id] *= damping; // Apply damping
+
+ // Limit velocity
+ Vector2 velocity = nodeVelocities[node.Id];
+ if (velocity.Length() > maxVelocity)
+ {
+ nodeVelocities[node.Id] = Vector2.Normalize(velocity) * maxVelocity;
+ }
+
+ // Update position
+ Vector2 newPosition = node.Position + (nodeVelocities[node.Id] * deltaTime);
+
+ // Create updated node and replace in list
+ nodes[i] = node with { Position = newPosition };
+
+ // Update ImNodes position in real-time
+ ImNodes.SetNodeEditorSpacePos(node.Id, newPosition);
+ }
+ }
+
+ private SimpleNode? GetNodeByOutputPin(int pinId) => nodes.FirstOrDefault(n => n.OutputPins.Contains(pinId));
+
+ private SimpleNode? GetNodeByInputPin(int pinId) => nodes.FirstOrDefault(n => n.InputPins.Contains(pinId));
+
+ private void RenderDebugInformation()
+ {
+ ImGui.SeparatorText("Debug Information:");
+
+ // Get canvas panning once and reuse it throughout debug info
+ Vector2 canvasPanning = ImNodes.EditorContextGetPanning();
+
+ RenderGeneralDebugInfo();
+ RenderCanvasDebugInfo(canvasPanning);
+ RenderNodeLayoutDebugInfo(canvasPanning);
+
+ if (automaticLayout)
+ {
+ RenderPhysicsDebugInfo();
+ }
+ else
+ {
+ RenderBasicNodeDebugInfo();
+ }
+ }
+
+ private void RenderGeneralDebugInfo()
+ {
+ ImGui.Text($"Total Nodes: {nodes.Count}");
+ ImGui.Text($"Total Links: {links.Count}");
+ }
+
+ private static void RenderCanvasDebugInfo(Vector2 canvasPanning)
+ {
+ // Canvas panning info (flip Y for intuitive display)
+ Vector2 displayPanning = new(canvasPanning.X, -canvasPanning.Y);
+ ImGui.Text($"Origin Offset: ({displayPanning.X:F1}, {displayPanning.Y:F1})");
+ ImGui.TextDisabled("(Where origin is relative to center of view)");
+
+ // Explain what the panning values mean
+ if (canvasPanning.X == 0.0f && canvasPanning.Y == 0.0f)
+ {
+ ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), "✓ Origin at center - (0,0) marker should be visible");
+ }
+ else
+ {
+ string direction = GetDirectionString(canvasPanning);
+ ImGui.TextColored(new Vector4(1.0f, 1.0f, 0.0f, 1.0f), $"Origin is {direction} from center");
+ }
+ }
+
+ private static string GetDirectionString(Vector2 canvasPanning)
+ {
+ string direction = "";
+ if (canvasPanning.X > 0)
+ {
+ direction += "right ";
+ }
+ else if (canvasPanning.X < 0)
+ {
+ direction += "left ";
+ }
+
+ if (canvasPanning.Y > 0)
+ {
+ direction += "down";
+ }
+ else if (canvasPanning.Y < 0)
+ {
+ direction += "up";
+ }
+
+ return direction.Trim();
+ }
+
+ private void RenderNodeLayoutDebugInfo(Vector2 canvasPanning)
+ {
+ if (nodes.Count == 0)
+ {
+ return;
+ }
+
+ // Calculate accurate bounding box using cached node dimensions
+ SimpleNode firstNode = nodes[0];
+ Vector2 firstNodePos = firstNode.Position;
+ Vector2 firstNodeDims = firstNode.Dimensions;
+
+ Vector2 minPos = firstNodePos;
+ Vector2 maxPos = firstNodePos + firstNodeDims;
+
+ // Area-weighted center of mass calculation
+ Vector2 weightedCenterSum = Vector2.Zero;
+ float totalArea = 0.0f;
+
+ foreach (SimpleNode node in nodes)
+ {
+ Vector2 nodePos = node.Position;
+ Vector2 nodeDims = node.Dimensions;
+
+ // Update bounding box to include full node rectangle
+ minPos.X = Math.Min(minPos.X, nodePos.X);
+ minPos.Y = Math.Min(minPos.Y, nodePos.Y);
+ maxPos.X = Math.Max(maxPos.X, nodePos.X + nodeDims.X);
+ maxPos.Y = Math.Max(maxPos.Y, nodePos.Y + nodeDims.Y);
+
+ // Area-weighted center of mass
+ Vector2 nodeCenter = nodePos + (nodeDims * 0.5f);
+ float nodeArea = nodeDims.X * nodeDims.Y;
+ weightedCenterSum += nodeCenter * nodeArea;
+ totalArea += nodeArea;
+ }
+
+ Vector2 centerOfMass = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero;
+
+ Vector2 boundingSize = maxPos - minPos;
+ ImGui.Text($"Bounding Box: {boundingSize.X:F1} × {boundingSize.Y:F1} (including node dimensions)");
+ Vector2 displayCenterOfMass = new(centerOfMass.X, -centerOfMass.Y);
+ ImGui.Text($"Center of Mass: ({displayCenterOfMass.X:F1}, {displayCenterOfMass.Y:F1}) (area-weighted)");
+
+ // Show distances from current view center to key points
+ float distanceToOrigin = canvasPanning.Length();
+ Vector2 centerOffset = centerOfMass - (-canvasPanning); // Center relative to view center
+ float distanceToCenter = centerOffset.Length();
+
+ ImGui.Text($"Distance to Origin: {distanceToOrigin:F1}px");
+ ImGui.Text($"Distance to Center: {distanceToCenter:F1}px");
+ }
+
+ private void RenderPhysicsDebugInfo()
+ {
+ // Physics center info
+ if (physicsCenter.HasValue)
+ {
+ Vector2 center = physicsCenter.Value;
+ Vector2 displayPhysicsCenter = new(center.X, -center.Y);
+ ImGui.Text($"Physics Center: ({displayPhysicsCenter.X:F1}, {displayPhysicsCenter.Y:F1})");
+ }
+ else
+ {
+ ImGui.Text("Physics Center: Not set");
+ }
+
+ RenderNodePhysicsData();
+ RenderPhysicsLinkDistances();
+ }
+
+ private void RenderNodePhysicsData()
+ {
+ ImGui.SeparatorText("Node Physics Data:");
+
+ foreach (SimpleNode node in nodes)
+ {
+ ImGui.PushID(node.Id);
+
+ if (ImGui.TreeNode($"Node {node.Id}: {node.Name}"))
+ {
+ Vector2 displayNodePos = new(node.Position.X, -node.Position.Y);
+ ImGui.Text($"Position: ({displayNodePos.X:F1}, {displayNodePos.Y:F1})");
+
+ if (nodeForces.TryGetValue(node.Id, out Vector2 force))
+ {
+ float forceMagnitude = force.Length();
+ Vector2 displayForce = new(force.X, -force.Y);
+ ImGui.Text($"Force: ({displayForce.X:F2}, {displayForce.Y:F2}) | Mag: {forceMagnitude:F2}");
+ }
+
+ if (nodeVelocities.TryGetValue(node.Id, out Vector2 velocity))
+ {
+ float velocityMagnitude = velocity.Length();
+ Vector2 displayVelocity = new(velocity.X, -velocity.Y);
+ ImGui.Text($"Velocity: ({displayVelocity.X:F2}, {displayVelocity.Y:F2}) | Mag: {velocityMagnitude:F2}");
+ }
+
+ ImGui.TreePop();
+ }
+
+ ImGui.PopID();
+ }
+ }
+
+ private void RenderPhysicsLinkDistances()
+ {
+ ImGui.SeparatorText("Link Distances:");
+ ImGui.Text($"Total Links: {links.Count}");
+
+ if (links.Count == 0)
+ {
+ ImGui.TextColored(new Vector4(1.0f, 0.7f, 0.0f, 1.0f), "No links found - try creating connections between nodes");
+ return;
+ }
+
+ foreach (SimpleLink link in links)
+ {
+ SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId);
+ SimpleNode? endNode = GetNodeByInputPin(link.InputPinId);
+
+ if (startNode != null && endNode != null)
+ {
+ float distance = (endNode.Position - startNode.Position).Length();
+ Vector4 color = distance > idealLinkDistance
+ ? new Vector4(1.0f, 0.3f, 0.3f, 1.0f) // Red if too far
+ : new Vector4(0.3f, 1.0f, 0.3f, 1.0f); // Green if close enough
+
+ ImGui.TextColored(color, $"Link {link.Id}: {distance:F1}px (ideal: {idealLinkDistance:F0}px)");
+ }
+ else
+ {
+ ImGui.TextColored(new Vector4(1.0f, 0.3f, 0.3f, 1.0f), $"Link {link.Id}: ERROR - Missing nodes (Out:{link.OutputPinId} → In:{link.InputPinId})");
+ }
+ }
+ }
+
+ private void RenderBasicNodeDebugInfo()
+ {
+ // Basic node positions (when physics is not active)
+ if (nodes.Count > 0)
+ {
+ ImGui.SeparatorText("Node Positions:");
+
+ foreach (SimpleNode node in nodes)
+ {
+ Vector2 displayNodePos = new(node.Position.X, -node.Position.Y);
+ ImGui.Text($"Node {node.Id} ({node.Name}): ({displayNodePos.X:F1}, {displayNodePos.Y:F1})");
+ }
+ }
+
+ RenderBasicLinkDistances();
+ }
+
+ private void RenderBasicLinkDistances()
+ {
+ ImGui.SeparatorText("Link Distances:");
+ ImGui.Text($"Total Links: {links.Count}");
+
+ // Show link fix results if available
+ if (!string.IsNullOrEmpty(linkFixSummary))
+ {
+ ImGui.SeparatorText("Link Fix Results:");
+ ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), linkFixSummary);
+
+ if (linkFixLog.Count > 0)
+ {
+ if (ImGui.CollapsingHeader("Fix Details"))
+ {
+ foreach (string logEntry in linkFixLog)
+ {
+ ImGui.Text(logEntry);
+ }
+ }
+ }
+ }
+
+ // Debug: Show all node pin configurations
+ ImGui.SeparatorText("Node Pin Debug:");
+ foreach (SimpleNode node in nodes)
+ {
+ string inputPins = string.Join(",", node.InputPins);
+ string outputPins = string.Join(",", node.OutputPins);
+ ImGui.Text($"Node {node.Id} ({node.Name}): In=[{inputPins}] Out=[{outputPins}]");
+ }
+
+ if (links.Count == 0)
+ {
+ ImGui.TextColored(new Vector4(1.0f, 0.7f, 0.0f, 1.0f), "No links found - try creating connections between nodes");
+ return;
+ }
+
+ foreach (SimpleLink link in links)
+ {
+ SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId);
+ SimpleNode? endNode = GetNodeByInputPin(link.InputPinId);
+
+ if (startNode != null && endNode != null)
+ {
+ float distance = (endNode.Position - startNode.Position).Length();
+ ImGui.Text($"Link {link.Id}: {distance:F1}px");
+ }
+ else
+ {
+ ImGui.TextColored(new Vector4(1.0f, 0.3f, 0.3f, 1.0f), $"Link {link.Id}: ERROR - Missing nodes (Out:{link.OutputPinId} → In:{link.InputPinId})");
+
+ // Debug: Show which nodes contain these pins
+ SimpleNode? nodeWithOutput = nodes.FirstOrDefault(n => n.OutputPins.Contains(link.OutputPinId) || n.InputPins.Contains(link.OutputPinId));
+ SimpleNode? nodeWithInput = nodes.FirstOrDefault(n => n.InputPins.Contains(link.InputPinId) || n.OutputPins.Contains(link.InputPinId));
+
+ if (nodeWithOutput != null)
+ {
+ bool isOutput = nodeWithOutput.OutputPins.Contains(link.OutputPinId);
+ string pinType = isOutput ? "OUTPUT" : "INPUT";
+ ImGui.Text($" Pin {link.OutputPinId} found in Node {nodeWithOutput.Id} as {pinType}");
+ }
+ else
+ {
+ ImGui.Text($" Pin {link.OutputPinId} not found in any node");
+ }
+
+ if (nodeWithInput != null)
+ {
+ bool isInput = nodeWithInput.InputPins.Contains(link.InputPinId);
+ string pinType = isInput ? "INPUT" : "OUTPUT";
+ ImGui.Text($" Pin {link.InputPinId} found in Node {nodeWithInput.Id} as {pinType}");
+ }
+ else
+ {
+ ImGui.Text($" Pin {link.InputPinId} not found in any node");
+ }
+ }
+ }
+ }
+
+ private void RenderAllDebugOverlays(Vector2 editorAreaPos, Vector2 editorAreaSize)
+ {
+ // Get window draw list for screen space drawing
+ ImDrawListPtr drawList = ImGui.GetWindowDrawList();
+
+ // Helper function to convert grid space coordinates to screen space
+ // For node-relative positions, use reference-based approach
+ // For absolute world positions, use direct mathematical transformation
+ Vector2 GridSpaceToScreenSpace(Vector2 gridPos, bool isAbsoluteWorldPosition = false)
+ {
+ if (isAbsoluteWorldPosition)
+ {
+ // For absolute world positions like origin (0,0)
+ // Transform directly using ImNodes coordinate system: add panning, not subtract
+ Vector2 panning = ImNodes.EditorContextGetPanning();
+ Vector2 editorCenter = editorAreaPos + (editorAreaSize * 0.5f);
+ Vector2 screenPos = editorCenter + gridPos + panning;
+ return screenPos;
+ }
+ else if (nodes.Count > 0)
+ {
+ // For positions relative to nodes (like bounding box, center of mass)
+ // Use the first node as a reference to ensure exact coordinate matching
+ SimpleNode referenceNode = nodes[0];
+ Vector2 referenceGridPos = referenceNode.Position;
+ Vector2 referenceScreenPos = ImNodes.GetNodeScreenSpacePos(referenceNode.Id);
+
+ // Calculate the offset from the reference node in grid space
+ Vector2 offset = gridPos - referenceGridPos;
+
+ // Apply the same offset in screen space
+ return referenceScreenPos + offset;
+ }
+ else
+ {
+ // Fallback for when no nodes exist
+ Vector2 panning = ImNodes.EditorContextGetPanning();
+ Vector2 editorCenter = editorAreaPos + (editorAreaSize * 0.5f);
+ Vector2 screenPos = editorCenter + gridPos + panning;
+ return screenPos;
+ }
+ }
+
+ // Colors
+ uint forceColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.5f, 0.0f, 0.8f)); // Orange
+ uint velocityColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 1.0f, 0.5f, 0.8f)); // Green
+ uint centerColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.0f, 1.0f, 0.8f)); // Magenta
+ uint repulsionColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.0f, 0.0f, 0.3f)); // Red
+ uint originColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 0.8f, 1.0f, 1.0f)); // Cyan
+ uint boundingBoxColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 1.0f, 0.0f, 0.6f)); // Yellow
+ uint centerOfMassColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.0f, 1.0f, 1.0f, 0.9f)); // Cyan
+
+ // Draw canvas origin (0,0) marker
+ Vector2 originScreen = GridSpaceToScreenSpace(new Vector2(0.0f, 0.0f), isAbsoluteWorldPosition: true);
+ drawList.AddLine(originScreen + new Vector2(-50, 0), originScreen + new Vector2(50, 0), originColor, 4.0f);
+ drawList.AddLine(originScreen + new Vector2(0, -50), originScreen + new Vector2(0, 50), originColor, 4.0f);
+ drawList.AddCircle(originScreen, 15.0f, originColor, 16, 3.0f);
+ drawList.AddCircle(originScreen, 25.0f, originColor, 16, 2.0f);
+ drawList.AddCircleFilled(originScreen, 5.0f, originColor);
+ drawList.AddText(originScreen + new Vector2(30, -15), originColor, "ORIGIN (0,0)");
+
+ // Physics-specific debug elements (only when physics is active)
+ if (automaticLayout)
+ {
+ // Draw physics center
+ if (physicsCenter.HasValue)
+ {
+ Vector2 centerScreen = GridSpaceToScreenSpace(physicsCenter.Value, isAbsoluteWorldPosition: false);
+ drawList.AddCircleFilled(centerScreen, 8.0f, centerColor);
+ drawList.AddText(centerScreen + new Vector2(10, -5), centerColor, "Physics Center");
+ }
+
+ // Draw forces and velocities for each node
+ foreach (SimpleNode node in nodes)
+ {
+ // Draw from node center for accurate physics visualization
+ Vector2 nodeCenter = node.Position + (node.Dimensions * 0.5f);
+ Vector2 nodeCenterScreen = GridSpaceToScreenSpace(nodeCenter);
+
+ // Draw force vector (scaled for visibility)
+ if (nodeForces.TryGetValue(node.Id, out Vector2 force))
+ {
+ Vector2 forceEndScreen = GridSpaceToScreenSpace(nodeCenter + (force * 0.01f)); // Scale down for visibility
+ if (force.Length() > 0.1f) // Only draw if significant
+ {
+ drawList.AddLine(nodeCenterScreen, forceEndScreen, forceColor, 2.0f);
+ drawList.AddCircleFilled(forceEndScreen, 3.0f, forceColor);
+ }
+ }
+
+ // Draw velocity vector (scaled for visibility)
+ if (nodeVelocities.TryGetValue(node.Id, out Vector2 velocity))
+ {
+ Vector2 velocityEndScreen = GridSpaceToScreenSpace(nodeCenter + (velocity * 0.1f)); // Scale down for visibility
+ if (velocity.Length() > 0.1f) // Only draw if significant
+ {
+ drawList.AddLine(nodeCenterScreen, velocityEndScreen, velocityColor, 2.0f);
+ drawList.AddCircleFilled(velocityEndScreen, 3.0f, velocityColor);
+ }
+ }
+
+ // Draw repulsion zones (faint circles)
+ drawList.AddCircle(nodeCenterScreen, 100.0f, repulsionColor, 32, 1.0f);
+ }
+ }
+
+ // Draw bounding box and center of mass for all nodes
+ if (nodes.Count > 0)
+ {
+ // Calculate accurate bounding box using cached node dimensions and area-weighted center of mass
+ SimpleNode firstNode = nodes[0];
+ Vector2 firstNodePos = firstNode.Position;
+ Vector2 firstNodeDims = firstNode.Dimensions;
+
+ // Initialize bounding box with first node's full rectangle
+ Vector2 minPosGrid = firstNodePos;
+ Vector2 maxPosGrid = firstNodePos + firstNodeDims;
+
+ // For area-weighted center of mass calculation
+ Vector2 weightedCenterSum = Vector2.Zero;
+ float totalArea = 0.0f;
+
+ // Calculate accurate bounding box and area-weighted center of mass
+ foreach (SimpleNode node in nodes)
+ {
+ Vector2 nodePos = node.Position;
+ Vector2 nodeDims = node.Dimensions;
+
+ // Update bounding box to include full node rectangle
+ minPosGrid.X = Math.Min(minPosGrid.X, nodePos.X);
+ minPosGrid.Y = Math.Min(minPosGrid.Y, nodePos.Y);
+ maxPosGrid.X = Math.Max(maxPosGrid.X, nodePos.X + nodeDims.X);
+ maxPosGrid.Y = Math.Max(maxPosGrid.Y, nodePos.Y + nodeDims.Y);
+
+ // Calculate area-weighted center of mass (center of each node weighted by its area)
+ Vector2 nodeCenter = nodePos + (nodeDims * 0.5f);
+ float nodeArea = nodeDims.X * nodeDims.Y;
+ weightedCenterSum += nodeCenter * nodeArea;
+ totalArea += nodeArea;
+ }
+
+ // Convert to screen coordinates
+ Vector2 minPosScreen = GridSpaceToScreenSpace(minPosGrid);
+ Vector2 maxPosScreen = GridSpaceToScreenSpace(maxPosGrid);
+
+ // Calculate final area-weighted center of mass
+ Vector2 centerOfMassGrid = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero;
+ Vector2 centerOfMassScreen = GridSpaceToScreenSpace(centerOfMassGrid);
+
+ // Draw accurate bounding box
+ drawList.AddRect(minPosScreen, maxPosScreen, boundingBoxColor, 0.0f, ImDrawFlags.None, 2.0f);
+ drawList.AddText(minPosScreen + new Vector2(5, -20), boundingBoxColor, "Bounding Box");
+
+ // Draw area-weighted center of mass
+ drawList.AddCircleFilled(centerOfMassScreen, 6.0f, centerOfMassColor);
+ drawList.AddCircle(centerOfMassScreen, 12.0f, centerOfMassColor, 16, 2.0f);
+ drawList.AddText(centerOfMassScreen + new Vector2(15, -8), centerOfMassColor, "Center of Mass");
+ }
+
+ // Draw link connections and distances (always when debug visualization is on)
+ foreach (SimpleLink link in links)
+ {
+ SimpleNode? startNode = GetNodeByOutputPin(link.OutputPinId);
+ SimpleNode? endNode = GetNodeByInputPin(link.InputPinId);
+
+ if (startNode != null && endNode != null)
+ {
+ Vector2 startScreen = GridSpaceToScreenSpace(startNode.Position);
+ Vector2 endScreen = GridSpaceToScreenSpace(endNode.Position);
+ float distance = (endNode.Position - startNode.Position).Length();
+
+ // Color based on distance vs ideal (only when physics is enabled)
+ Vector4 color;
+ if (automaticLayout)
+ {
+ color = distance > idealLinkDistance
+ ? new Vector4(1.0f, 0.0f, 0.0f, 0.5f) // Red if too far
+ : new Vector4(0.0f, 1.0f, 0.0f, 0.5f); // Green if close enough
+ }
+ else
+ {
+ // Use neutral blue color when physics is off
+ color = new Vector4(0.3f, 0.7f, 1.0f, 0.6f); // Light blue
+ }
+
+ uint lineColor = ImGui.ColorConvertFloat4ToU32(color);
+ drawList.AddLine(startScreen, endScreen, lineColor, 1.0f);
+
+ // Draw distance text at midpoint
+ Vector2 midpointScreen = (startScreen + endScreen) * 0.5f;
+ string distanceText = automaticLayout ? $"{distance:F0}px" : $"{distance:F0}px";
+ drawList.AddText(midpointScreen, lineColor, distanceText);
+ }
+ }
+
+ // Draw compass (always visible when debug is on and not at origin)
+ Vector2 currentPanning = ImNodes.EditorContextGetPanning();
+ if (currentPanning.X != 0.0f || currentPanning.Y != 0.0f)
+ {
+ Vector2 directionToOrigin = currentPanning;
+ float distance = directionToOrigin.Length();
+
+ if (distance > 10.0f) // Only show if we're not too close to origin
+ {
+ Vector2 normalizedDirection = Vector2.Normalize(directionToOrigin);
+
+ // Position compass in the center of the editor area in screen space
+ Vector2 compassCenter = editorAreaPos + (editorAreaSize * 0.5f);
+
+ // Colors
+ uint compassBgColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 0.8f));
+ uint compassArrowColor = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.3f, 0.3f, 1.0f)); // Red arrow
+
+ // Draw compass background circle
+ drawList.AddCircleFilled(compassCenter, 35.0f, compassBgColor);
+ drawList.AddCircle(compassCenter, 35.0f, originColor, 32, 2.0f);
+
+ // Draw compass arrow pointing to origin
+ Vector2 arrowEnd = compassCenter + (normalizedDirection * 25.0f);
+ Vector2 arrowLeft = compassCenter + (new Vector2(-normalizedDirection.Y, normalizedDirection.X) * 8.0f) + (normalizedDirection * 15.0f);
+ Vector2 arrowRight = compassCenter + (new Vector2(normalizedDirection.Y, -normalizedDirection.X) * 8.0f) + (normalizedDirection * 15.0f);
+
+ // Draw arrow shaft
+ drawList.AddLine(compassCenter, arrowEnd, compassArrowColor, 3.0f);
+ // Draw arrow head
+ drawList.AddTriangleFilled(arrowEnd, arrowLeft, arrowRight, compassArrowColor);
+
+ // Add distance text
+ drawList.AddText(compassCenter + new Vector2(-15, 40), originColor, $"{distance:F0}px");
+ drawList.AddText(compassCenter + new Vector2(-20, -50), originColor, "TO ORIGIN");
+ }
+ }
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ RenderHeader();
+ RenderControls();
+ RenderNodeEditor();
+ HandleLinkEvents();
+
+ ImGui.EndTabItem();
+ }
+ }
+
+ private static void RenderHeader()
+ {
+ ImGui.TextWrapped("ImNodes provides a node editor with support for nodes, pins, and connections.");
+ ImGui.Separator();
+ }
+
+ private void RenderControls()
+ {
+ if (ImGui.Button("Add Node"))
+ {
+ // Place new nodes in a grid pattern to avoid overlap
+ int nodeIndex = nodes.Count;
+ int row = nodeIndex / 3;
+ int col = nodeIndex % 3;
+ Vector2 nodePos = new(150 + (col * 250), 100 + (row * 150));
+
+ nodes.Add(new SimpleNode(
+ nextNodeId++,
+ nodePos,
+ $"Custom Node {nodeIndex + 1}",
+ [nextNodeId, nextNodeId + 1], // Input pins
+ [nextNodeId + 2, nextNodeId + 3], // Output pins
+ Vector2.Zero // Dimensions will be updated after rendering
+));
+ nextNodeId += 4; // Reserve IDs for pins
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Clear All"))
+ {
+ ClearAllNodes();
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Reset Demo"))
+ {
+ ResetToDemo();
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Fix Links"))
+ {
+ FixCorruptedLinks();
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Fix any corrupted links with incorrect pin mappings");
+ }
+
+ if (!string.IsNullOrEmpty(linkFixSummary))
+ {
+ ImGui.SameLine();
+ if (ImGui.Button("Clear Log"))
+ {
+ linkFixLog.Clear();
+ linkFixSummary = "";
+ }
+ }
+ }
+
+ private void ClearAllNodes()
+ {
+ nodes.Clear();
+ links.Clear();
+ nodeVelocities.Clear();
+ nodeForces.Clear();
+ physicsCenter = null;
+ nextNodeId = 1;
+ nextLinkId = 1;
+ initialPositionsSet = false;
+ }
+
+ private void ResetToDemo()
+ {
+ ClearAllNodes();
+
+ // Recreate the demo layout
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(100, 150), "Input Node", [], [1, 2], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 100), "Process Node A", [3], [4, 5], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(400, 250), "Process Node B", [6], [7], Vector2.Zero));
+ nodes.Add(new SimpleNode(nextNodeId++, new Vector2(700, 175), "Output Node", [8, 9], [], Vector2.Zero));
+
+ links.Add(new SimpleLink(nextLinkId++, 1, 3)); // Input to Process A
+ links.Add(new SimpleLink(nextLinkId++, 2, 6)); // Input to Process B
+ links.Add(new SimpleLink(nextLinkId++, 4, 8)); // Process A to Output
+ links.Add(new SimpleLink(nextLinkId++, 7, 9)); // Process B to Output
+
+ nextNodeId = 10;
+ }
+
+ private void FixCorruptedLinks()
+ {
+ List fixedLinks = [];
+ int originalCount = links.Count;
+ int fixedCount = 0;
+ int correctCount = 0;
+ List fixLog = [];
+
+ foreach (SimpleLink link in links)
+ {
+ fixLog.Add($"Processing Link {link.Id}: Out:{link.OutputPinId} → In:{link.InputPinId}");
+
+ // Find nodes containing these pins
+ SimpleNode? nodeWithOutputPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(link.OutputPinId) || n.InputPins.Contains(link.OutputPinId));
+ SimpleNode? nodeWithInputPin = nodes.FirstOrDefault(n => n.InputPins.Contains(link.InputPinId) || n.OutputPins.Contains(link.InputPinId));
+
+ fixLog.Add($" nodeWithOutputPin: {nodeWithOutputPin?.Name} (ID: {nodeWithOutputPin?.Id})");
+ fixLog.Add($" nodeWithInputPin: {nodeWithInputPin?.Name} (ID: {nodeWithInputPin?.Id})");
+
+ if (nodeWithOutputPin != null && nodeWithInputPin != null)
+ {
+ bool outputPinIsActuallyOutput = nodeWithOutputPin.OutputPins.Contains(link.OutputPinId);
+ bool inputPinIsActuallyInput = nodeWithInputPin.InputPins.Contains(link.InputPinId);
+
+ fixLog.Add($" outputPinIsActuallyOutput: {outputPinIsActuallyOutput}");
+ fixLog.Add($" inputPinIsActuallyInput: {inputPinIsActuallyInput}");
+
+ if (outputPinIsActuallyOutput && inputPinIsActuallyInput)
+ {
+ // Link is correct
+ fixLog.Add($" Link {link.Id} is already correct");
+ fixedLinks.Add(link);
+ correctCount++;
+ }
+ else
+ {
+ // Try to fix the link by finding the correct pin mappings
+ int actualOutputPin = -1;
+ int actualInputPin = -1;
+
+ // Check if pins are swapped
+ bool canSwap = nodeWithOutputPin.InputPins.Contains(link.OutputPinId) && nodeWithInputPin.OutputPins.Contains(link.InputPinId);
+ fixLog.Add($" Can swap pins: {canSwap}");
+
+ if (canSwap)
+ {
+ // Pins are swapped
+ actualOutputPin = link.InputPinId;
+ actualInputPin = link.OutputPinId;
+ fixedCount++;
+ fixLog.Add($" Original: Out:{link.OutputPinId} → In:{link.InputPinId}");
+ fixLog.Add($" Fixed to: Out:{actualOutputPin} → In:{actualInputPin}");
+ }
+
+ if (actualOutputPin != -1 && actualInputPin != -1)
+ {
+ // Create corrected link
+ fixedLinks.Add(new SimpleLink(link.Id, actualOutputPin, actualInputPin));
+ }
+ else
+ {
+ fixLog.Add($" Link {link.Id} could not be fixed");
+ }
+ }
+ }
+ else
+ {
+ fixLog.Add($" Link {link.Id} - nodes not found, removing link");
+ }
+ }
+
+ // Replace the links collection with fixed links
+ links.Clear();
+ links.AddRange(fixedLinks);
+
+ // Add final verification
+ fixLog.Add("");
+ fixLog.Add("Final links after fix:");
+ foreach (SimpleLink finalLink in links)
+ {
+ fixLog.Add($" Link {finalLink.Id}: Out:{finalLink.OutputPinId} → In:{finalLink.InputPinId}");
+ }
+
+ // Store the fix results for display
+ linkFixLog = fixLog;
+ linkFixSummary = $"Link Fix Results: {originalCount} original, {correctCount} already correct, {fixedCount} fixed, {fixedLinks.Count} total after fix";
+ }
+
+ private void RenderNodeEditor()
+ {
+ // Create horizontal layout: editor on left, parameters on right
+ ImGui.BeginTable("NodeEditorLayout", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV);
+ ImGui.TableSetupColumn("Editor", ImGuiTableColumnFlags.WidthStretch, 0.75f);
+ ImGui.TableSetupColumn("Parameters", ImGuiTableColumnFlags.WidthStretch, 0.25f);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ // Store editor area position and size for compass rendering
+ Vector2 editorAreaPos = ImGui.GetCursorScreenPos();
+ Vector2 editorAreaSize = ImGui.GetContentRegionAvail();
+
+ // Node editor
+ ImNodes.BeginNodeEditor();
+
+ // Set initial positions only once (skip if automatic layout is active)
+ if (!initialPositionsSet && nodes.Count > 0 && !automaticLayout)
+ {
+ foreach (SimpleNode node in nodes)
+ {
+ ImNodes.SetNodeEditorSpacePos(node.Id, node.Position);
+ }
+ initialPositionsSet = true;
+ }
+
+ RenderNodes();
+ RenderLinks();
+
+ // Sync node positions and dimensions from ImNodes back to our data structure
+ // This ensures manual node dragging works properly and keeps dimensions cached
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ SimpleNode node = nodes[i];
+ Vector2 currentImNodesPos = ImNodes.GetNodeEditorSpacePos(node.Id);
+ Vector2 currentImNodesDims = ImNodes.GetNodeDimensions(node.Id);
+
+ // Update if position has changed or dimensions are not cached yet
+ bool positionChanged = Vector2.Distance(node.Position, currentImNodesPos) > 0.1f;
+ bool dimensionsNotCached = node.Dimensions == Vector2.Zero;
+ bool dimensionsChanged = Vector2.Distance(node.Dimensions, currentImNodesDims) > 0.1f;
+
+ if (positionChanged || dimensionsNotCached || dimensionsChanged)
+ {
+ nodes[i] = node with { Position = currentImNodesPos, Dimensions = currentImNodesDims };
+ }
+ }
+
+ ImNodes.EndNodeEditor();
+
+ // Move to the parameters column
+ ImGui.TableNextColumn();
+ RenderParametersPanel();
+
+ ImGui.EndTable();
+
+ // Draw all debug visualization after everything else so it appears on top
+ if (showDebugVisualization)
+ {
+ RenderAllDebugOverlays(editorAreaPos, editorAreaSize);
+ }
+ }
+
+ private void RenderNodes()
+ {
+ // Render nodes
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ SimpleNode node = nodes[i];
+ ImNodes.BeginNode(node.Id);
+
+ // Node title
+ ImNodes.BeginNodeTitleBar();
+ ImGui.TextUnformatted(node.Name);
+ ImNodes.EndNodeTitleBar();
+
+ // Input pins
+ for (int j = 0; j < node.InputPins.Count; j++)
+ {
+ int pinId = node.InputPins[j];
+ ImNodes.BeginInputAttribute(pinId);
+ ImGui.Text($"In {j + 1}");
+ ImNodes.EndInputAttribute();
+ }
+
+ // Node content
+ ImGui.Text($"Node ID: {node.Id}");
+
+ // Output pins
+ for (int j = 0; j < node.OutputPins.Count; j++)
+ {
+ int pinId = node.OutputPins[j];
+ ImNodes.BeginOutputAttribute(pinId);
+ ImGui.Indent(40);
+ ImGui.Text($"Out {j + 1}");
+ ImNodes.EndOutputAttribute();
+ }
+
+ ImNodes.EndNode();
+ }
+ }
+
+ private void RenderLinks()
+ {
+ // Render links
+ foreach (SimpleLink link in links)
+ {
+ ImNodes.Link(link.Id, link.InputPinId, link.OutputPinId);
+ }
+ }
+
+ private void RenderParametersPanel()
+ {
+ // Debug Visualization (always available)
+ ImGui.SeparatorText("Debug Visualization:");
+ ImGui.Checkbox("Show Debug Visualization", ref showDebugVisualization);
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Show visual overlays: canvas origin, node bounding box, center of mass, and physics data (if enabled)");
+ }
+
+ // Show debug information when enabled
+ if (showDebugVisualization)
+ {
+ RenderDebugInformation();
+ }
+
+ ImGui.SeparatorText("Physics Layout:");
+
+ // Physics layout toggle
+ ImGui.Checkbox("Automatic Layout", ref automaticLayout);
+ ImGui.SameLine();
+ ImGui.TextDisabled("(?)");
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Physics simulation: node repulsion, link attraction, and center gravity");
+ }
+
+ if (automaticLayout)
+ {
+ ImGui.SameLine();
+ ImGui.TextColored(new Vector4(0.0f, 1.0f, 0.0f, 1.0f), "● ACTIVE");
+ }
+
+ // Physics parameters panel
+ if (automaticLayout)
+ {
+ ImGui.SeparatorText("Physics Parameters:");
+
+ RenderPhysicsInputs();
+ RenderPhysicsInfo();
+ }
+ else
+ {
+ ImGui.TextDisabled("Enable Automatic Layout above");
+ ImGui.TextDisabled("to show physics parameters");
+ }
+
+ // Canvas Navigation Controls
+ ImGui.SeparatorText("Canvas Navigation:");
+
+ if (ImGui.Button("Reset Canvas to Origin"))
+ {
+ // Reset canvas panning to origin (0,0)
+ ImNodes.EditorContextResetPanning(new Vector2(0.0f, 0.0f));
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Pan the canvas so origin (0,0) is visible");
+ }
+
+ if (ImGui.Button("Center Canvas on Nodes"))
+ {
+ // Calculate area-weighted center of mass of all nodes using cached dimensions
+ if (nodes.Count > 0)
+ {
+ Vector2 weightedCenterSum = Vector2.Zero;
+ float totalArea = 0.0f;
+
+ foreach (SimpleNode node in nodes)
+ {
+ // Use cached position and dimensions for efficient calculation
+ Vector2 nodePos = node.Position;
+ Vector2 nodeDims = node.Dimensions;
+
+ // Calculate area-weighted center (center of each node weighted by its area)
+ Vector2 nodeCenter = nodePos + (nodeDims * 0.5f);
+ float nodeArea = nodeDims.X * nodeDims.Y;
+ weightedCenterSum += nodeCenter * nodeArea;
+ totalArea += nodeArea;
+ }
+
+ Vector2 centerOfMass = totalArea > 0 ? weightedCenterSum / totalArea : Vector2.Zero;
+
+ // Pan canvas to center the area-weighted center of mass
+ ImNodes.EditorContextResetPanning(centerOfMass);
+ }
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Pan the canvas to center all nodes in view");
+ }
+ }
+
+ private void RenderPhysicsInputs()
+ {
+ ImGui.InputFloat("Repulsion Strength", ref repulsionStrength, 100.0f, 1000.0f, "%.0f");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("REPULSION FORCE IMPLEMENTATION:\n" +
+ "• Formula: force = repulsionStrength / ((distance²) + 1)\n" +
+ "• Applied between ALL node pairs (N² complexity)\n" +
+ "• Prevents nodes from overlapping\n" +
+ "• Higher values = stronger push-apart force\n" +
+ "• Visible as: Red circles around nodes\n" +
+ "• Default: 5000 (good balance for most layouts)");
+ }
+ else
+ {
+ ImGui.SetTooltip("How strongly nodes push each other away");
+ }
+ }
+
+ ImGui.InputFloat("Attraction Strength", ref attractionStrength, 0.01f, 0.1f, "%.3f");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("ATTRACTION FORCE IMPLEMENTATION:\n" +
+ "• Formula: force = (distance - idealLinkDistance) × attractionStrength\n" +
+ "• Applied only between CONNECTED nodes\n" +
+ "• Pulls connected nodes toward ideal distance\n" +
+ "• Spring-like behavior: stronger when farther from ideal\n" +
+ "• Visible as: Green/Red lines between connected nodes\n" +
+ "• Default: 0.5 (moderate spring strength)");
+ }
+ else
+ {
+ ImGui.SetTooltip("How strongly connected nodes pull toward each other");
+ }
+ }
+
+ ImGui.InputFloat("Center Force", ref centerForce, 0.001f, 0.01f, "%.4f");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("CENTER GRAVITY IMPLEMENTATION:\n" +
+ "• Formula: force = distance × centerForce\n" +
+ "• Applied to ALL nodes toward physics center\n" +
+ "• Prevents nodes from drifting to edges\n" +
+ "• Linear increase with distance from center\n" +
+ "• Physics center = canvas origin (0,0)\n" +
+ "• Visible as: Magenta circle at origin\n" +
+ "• Default: 0.12 (gentle centering force)");
+ }
+ else
+ {
+ ImGui.SetTooltip("How strongly nodes are pulled toward the center");
+ }
+ }
+
+ ImGui.InputFloat("Ideal Link Distance", ref idealLinkDistance, 10.0f, 50.0f, "%.0f px");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("IDEAL DISTANCE IMPLEMENTATION:\n" +
+ "• Target distance for connected nodes\n" +
+ "• Used in attraction force calculation\n" +
+ "• Links shorter than ideal: nodes pull apart\n" +
+ "• Links longer than ideal: nodes pull together\n" +
+ "• Visible as: Distance labels on connections\n" +
+ "• Green lines = close to ideal, Red lines = too far\n" +
+ "• Default: 200px (good for typical node sizes)");
+ }
+ else
+ {
+ ImGui.SetTooltip("Preferred distance between connected nodes");
+ }
+ }
+
+ ImGui.InputFloat("Damping", ref damping, 0.01f, 0.1f, "%.3f");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("VELOCITY DAMPING IMPLEMENTATION:\n" +
+ "• Formula: velocity = velocity × damping (each frame)\n" +
+ "• Applied to ALL nodes every simulation step\n" +
+ "• Simulates friction/air resistance\n" +
+ "• Higher values = nodes slow down faster\n" +
+ "• Prevents oscillation and ensures convergence\n" +
+ "• Visible as: Green arrows (velocity vectors)\n" +
+ "• Range: 0.1-0.95 (0.8 = good stability)");
+ }
+ else
+ {
+ ImGui.SetTooltip("How quickly nodes slow down (higher = more stable)");
+ }
+ }
+
+ ImGui.InputFloat("Max Velocity", ref maxVelocity, 10.0f, 100.0f, "%.0f px/s");
+ if (ImGui.IsItemHovered())
+ {
+ if (showDebugVisualization)
+ {
+ ImGui.SetTooltip("VELOCITY LIMITING IMPLEMENTATION:\n" +
+ "• Applied after force integration each frame\n" +
+ "• Prevents explosive behavior with high forces\n" +
+ "• Clamps velocity magnitude to this maximum\n" +
+ "• Maintains velocity direction, only limits speed\n" +
+ "• Essential for simulation stability\n" +
+ "• Visible as: Length of green velocity arrows\n" +
+ "• Default: 200px/s (smooth but responsive)");
+ }
+ else
+ {
+ ImGui.SetTooltip("Maximum speed limit for node movement");
+ }
+ }
+
+ if (ImGui.Button("Reset Parameters"))
+ {
+ repulsionStrength = 5000.0f;
+ attractionStrength = 0.5f;
+ centerForce = 0.12f;
+ idealLinkDistance = 200.0f;
+ damping = 0.8f;
+ maxVelocity = 200.0f;
+ }
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Restore default values");
+ }
+ }
+
+ private void RenderPhysicsInfo()
+ {
+ // Display current physics info
+ ImGui.SeparatorText("Physics Info:");
+ ImGui.Text($"Active Nodes: {nodes.Count}");
+ if (physicsCenter.HasValue)
+ {
+ Vector2 center = physicsCenter.Value;
+ ImGui.Text($"Center: ({center.X:F0}, {center.Y:F0})");
+ }
+
+ // Show total system energy (sum of all velocities)
+ float totalEnergy = 0.0f;
+ foreach (KeyValuePair kvp in nodeVelocities)
+ {
+ totalEnergy += kvp.Value.LengthSquared();
+ }
+ ImGui.Text($"System Energy: {totalEnergy:F2}");
+ }
+
+ private void HandleLinkEvents()
+ {
+ // Handle new links
+ bool isLinkCreated;
+ int startPin = 0;
+ int endPin = 0;
+
+ unsafe
+ {
+ isLinkCreated = ImNodes.IsLinkCreated(&startPin, &endPin);
+ }
+
+ if (isLinkCreated)
+ {
+ // Copy to local variables for use in lambdas
+ int startPinId = startPin;
+ int endPinId = endPin;
+
+ // Determine which pin is output and which is input
+ SimpleNode? nodeWithStartPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(startPinId) || n.InputPins.Contains(startPinId));
+ SimpleNode? nodeWithEndPin = nodes.FirstOrDefault(n => n.OutputPins.Contains(endPinId) || n.InputPins.Contains(endPinId));
+
+ if (nodeWithStartPin != null && nodeWithEndPin != null)
+ {
+ bool startIsOutput = nodeWithStartPin.OutputPins.Contains(startPinId);
+ bool endIsOutput = nodeWithEndPin.OutputPins.Contains(endPinId);
+
+ // Create link with correct pin order: (linkId, outputPinId, inputPinId)
+ if (startIsOutput && !endIsOutput)
+ {
+ // Start pin is output, end pin is input - correct order
+ links.Add(new SimpleLink(nextLinkId++, startPinId, endPinId));
+ }
+ else if (!startIsOutput && endIsOutput)
+ {
+ // Start pin is input, end pin is output - reverse order
+ links.Add(new SimpleLink(nextLinkId++, endPinId, startPinId));
+ }
+ // If both are same type (output-output or input-input), don't create the link
+ }
+ }
+
+ // Handle link destruction
+ bool isLinkDestroyed;
+ int linkId = 0;
+ int safeLinkId = 0;
+
+ unsafe
+ {
+ isLinkDestroyed = ImNodes.IsLinkDestroyed(&linkId);
+ safeLinkId = linkId; // Store the link ID safely
+ }
+
+ if (isLinkDestroyed)
+ {
+ links.RemoveAll(link => link.Id == safeLinkId);
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/ImPlotDemo.cs b/ImGuiAppDemo/Demos/ImPlotDemo.cs
new file mode 100644
index 00000000..34b2ea53
--- /dev/null
+++ b/ImGuiAppDemo/Demos/ImPlotDemo.cs
@@ -0,0 +1,146 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+using Hexa.NET.ImPlot;
+
+///
+/// Demo for ImPlot advanced plotting
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")]
+internal sealed class ImPlotDemo : IDemoTab
+{
+ private readonly List sinData = [];
+ private readonly List cosData = [];
+ private readonly List noiseData = [];
+ private readonly List plotValues = [];
+ private float plotTime;
+ private readonly Random plotRandom = new();
+ private float plotRefreshTime;
+
+ public string TabName => "ImPlot Charts";
+
+ public ImPlotDemo()
+ {
+ // Initialize plot data
+ for (int i = 0; i < 100; i++)
+ {
+ float x = i * 0.1f;
+ sinData.Add(MathF.Sin(x));
+ cosData.Add(MathF.Cos(x));
+ noiseData.Add((float)((plotRandom.NextDouble() * 2.0) - 1.0));
+ }
+ }
+
+ public void Update(float deltaTime)
+ {
+ plotTime += deltaTime;
+
+ // Update plot data for real-time demo
+ plotRefreshTime += deltaTime;
+ if (plotRefreshTime >= 0.1f) // Update every 100ms
+ {
+ plotRefreshTime = 0;
+ plotValues.Add((float)plotRandom.NextDouble());
+ if (plotValues.Count > 100) // Keep last 100 values
+ {
+ plotValues.RemoveAt(0);
+ }
+ }
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.TextWrapped("ImPlot provides advanced plotting capabilities with various chart types.");
+ ImGui.Separator();
+
+ // Plot controls
+ if (ImGui.Button("Generate New Data"))
+ {
+ sinData.Clear();
+ cosData.Clear();
+ noiseData.Clear();
+
+ for (int i = 0; i < 100; i++)
+ {
+ float x = i * 0.1f;
+ sinData.Add(MathF.Sin(x + plotTime));
+ cosData.Add(MathF.Cos(x + plotTime));
+ noiseData.Add((float)((plotRandom.NextDouble() * 2.0) - 1.0));
+ }
+ }
+
+ ImGui.Separator();
+
+ // Line plot
+ if (ImPlot.BeginPlot("Trigonometric Functions", new Vector2(-1, 200)))
+ {
+ unsafe
+ {
+ fixed (float* sinPtr = sinData.ToArray())
+ fixed (float* cosPtr = cosData.ToArray())
+ {
+ ImPlot.PlotLine("sin(x)", sinPtr, sinData.Count);
+ ImPlot.PlotLine("cos(x)", cosPtr, cosData.Count);
+ }
+ }
+ ImPlot.EndPlot();
+ }
+
+ // Scatter plot
+ if (ImPlot.BeginPlot("Noise Data (Scatter)", new Vector2(-1, 200)))
+ {
+ unsafe
+ {
+ fixed (float* noisePtr = noiseData.ToArray())
+ {
+ ImPlot.PlotScatter("Random Noise", noisePtr, noiseData.Count);
+ }
+ }
+ ImPlot.EndPlot();
+ }
+
+ // Bar chart
+ if (ImPlot.BeginPlot("Sample Bar Chart", new Vector2(-1, 200)))
+ {
+ float[] barData = [1.0f, 2.5f, 3.2f, 1.8f, 4.1f, 2.9f, 3.6f];
+ unsafe
+ {
+ fixed (float* barPtr = barData)
+ {
+ ImPlot.PlotBars("Values", barPtr, barData.Length);
+ }
+ }
+ ImPlot.EndPlot();
+ }
+
+ // Real-time plot
+ if (ImPlot.BeginPlot("Real-time Data", new Vector2(-1, 200)))
+ {
+ // Update real-time data
+ if (plotValues.Count > 0)
+ {
+ unsafe
+ {
+ fixed (float* plotPtr = plotValues.ToArray())
+ {
+ ImPlot.PlotLine("Live Data", plotPtr, plotValues.Count);
+ }
+ }
+ }
+ ImPlot.EndPlot();
+ }
+
+ ImGui.Text($"Plot Time: {plotTime:F2}");
+ ImGui.Text($"Data Points: Sin({sinData.Count}), Cos({cosData.Count}), Noise({noiseData.Count})");
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/InputHandlingDemo.cs b/ImGuiAppDemo/Demos/InputHandlingDemo.cs
new file mode 100644
index 00000000..a12be6fd
--- /dev/null
+++ b/ImGuiAppDemo/Demos/InputHandlingDemo.cs
@@ -0,0 +1,152 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using System.Text;
+using Hexa.NET.ImGui;
+
+///
+/// Demo for input handling and interaction
+///
+internal sealed class InputHandlingDemo : IDemoTab
+{
+ private readonly StringBuilder textBuffer = new(1024);
+ private bool wrapText = true;
+ private bool showModal;
+ private bool showPopup;
+ private string modalResult = "";
+ private string modalInputBuffer = "";
+
+ public string TabName => "Input & Interaction";
+
+ public InputHandlingDemo()
+ {
+ textBuffer.Append("This is a demonstration of ImGui text editing capabilities.\n");
+ textBuffer.Append("You can edit this text, and it will update in real-time.\n");
+ textBuffer.Append("ImGui supports multi-line text editing with syntax highlighting possibilities.");
+ }
+
+ public void Update(float deltaTime) =>
+ // Handle modals and popups
+ HandleModalAndPopups();
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.SeparatorText("Mouse Information:");
+ Vector2 mousePos = ImGui.GetMousePos();
+ Vector2 mouseDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Left);
+ ImGui.Text($"Mouse Position: ({mousePos.X:F1}, {mousePos.Y:F1})");
+ ImGui.Text($"Mouse Delta: ({mouseDelta.X:F1}, {mouseDelta.Y:F1})");
+ ImGui.Text($"Left Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Left) ? "DOWN" : "UP")}");
+ ImGui.Text($"Right Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Right) ? "DOWN" : "UP")}");
+
+ // Simple drag demonstration
+ ImGui.SeparatorText("Drag & Drop:");
+ ImGui.Button("Drag Source", new Vector2(100, 50));
+ ImGui.SameLine();
+ ImGui.Button("Drop Target", new Vector2(100, 50));
+ ImGui.Text("(Drag and drop functionality would require more complex implementation)");
+
+ // Text editing
+ ImGui.SeparatorText("Multi-line Text Editor:");
+ ImGui.Checkbox("Word Wrap", ref wrapText);
+ ImGuiInputTextFlags textFlags = ImGuiInputTextFlags.AllowTabInput;
+ if (!wrapText)
+ {
+ textFlags |= ImGuiInputTextFlags.NoHorizontalScroll;
+ }
+
+ string textContent = textBuffer.ToString();
+ if (ImGui.InputTextMultiline("##TextEditor", ref textContent, 1024, new Vector2(-1, 150), textFlags))
+ {
+ textBuffer.Clear();
+ textBuffer.Append(textContent);
+ }
+
+ // Popup and modal buttons
+ ImGui.SeparatorText("Popups and Modals:");
+ if (ImGui.Button("Show Modal"))
+ {
+ showModal = true;
+ modalResult = "";
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("Show Popup"))
+ {
+ showPopup = true;
+ }
+
+ if (!string.IsNullOrEmpty(modalResult))
+ {
+ ImGui.Text($"Modal Result: {modalResult}");
+ }
+
+ ImGui.EndTabItem();
+ }
+ }
+
+ private void HandleModalAndPopups()
+ {
+ // Modal dialog
+ if (showModal)
+ {
+ ImGui.OpenPopup("Demo Modal");
+ showModal = false;
+ }
+
+ if (ImGui.BeginPopupModal("Demo Modal", ref showModal))
+ {
+ ImGui.Text("This is a modal dialog.");
+ ImGui.Text("It blocks interaction with the main window.");
+ ImGui.Separator();
+
+ ImGui.InputText("Input", ref modalInputBuffer, 100);
+
+ if (ImGui.Button("OK"))
+ {
+ modalResult = $"You entered: {modalInputBuffer}";
+ ImGui.CloseCurrentPopup();
+ }
+ ImGui.SameLine();
+ if (ImGui.Button("Cancel"))
+ {
+ modalResult = "Cancelled";
+ ImGui.CloseCurrentPopup();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ // Context popup
+ if (showPopup)
+ {
+ ImGui.OpenPopup("Demo Popup");
+ showPopup = false;
+ }
+
+ if (ImGui.BeginPopup("Demo Popup"))
+ {
+ ImGui.Text("This is a popup menu");
+ if (ImGui.MenuItem("Option 1"))
+ {
+ // Handle option 1
+ }
+ if (ImGui.MenuItem("Option 2"))
+ {
+ // Handle option 2
+ }
+ ImGui.Separator();
+ if (ImGui.MenuItem("Close"))
+ {
+ // Handle close
+ }
+ ImGui.EndPopup();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/LayoutDemo.cs b/ImGuiAppDemo/Demos/LayoutDemo.cs
new file mode 100644
index 00000000..8c96b5b4
--- /dev/null
+++ b/ImGuiAppDemo/Demos/LayoutDemo.cs
@@ -0,0 +1,135 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Numerics;
+using Hexa.NET.ImGui;
+
+///
+/// Demo for layout systems and tables
+///
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "Used for dummy data purposes")]
+internal sealed class LayoutDemo : IDemoTab
+{
+ private readonly List tableData = [];
+ private bool showTableHeaders = true;
+ private bool showTableBorders = true;
+ private readonly Random random = new();
+
+ private sealed record DemoItem(int Id, string Name, string Category, float Value, bool Active);
+
+ public string TabName => "Layout & Tables";
+
+ public LayoutDemo()
+ {
+ // Initialize table data
+ for (int i = 0; i < 20; i++)
+ {
+ string[] categories = ["Category A", "Category B", "Category C"];
+ tableData.Add(new DemoItem(
+ i,
+ $"Item {i + 1}",
+ categories[i % 3],
+ (float)(random.NextDouble() * 100),
+ random.NextDouble() > 0.5
+ ));
+ }
+ }
+
+ public void Update(float deltaTime)
+ {
+ // No updates needed for layout demo
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ // Columns
+ ImGui.SeparatorText("Columns Layout:");
+ ImGui.Columns(3, "DemoColumns");
+ ImGui.Text("Column 1");
+ ImGui.NextColumn();
+ ImGui.Text("Column 2");
+ ImGui.NextColumn();
+ ImGui.Text("Column 3");
+ ImGui.NextColumn();
+
+ for (int i = 0; i < 9; i++)
+ {
+ ImGui.Text($"Item {i + 1}");
+ ImGui.NextColumn();
+ }
+
+ ImGui.Columns(1);
+
+ // Tables
+ ImGui.SeparatorText("Advanced Tables:");
+ ImGui.Checkbox("Show Headers", ref showTableHeaders);
+ ImGui.SameLine();
+ ImGui.Checkbox("Show Borders", ref showTableBorders);
+
+ ImGuiTableFlags tableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.Resizable;
+ if (showTableHeaders)
+ {
+ tableFlags |= ImGuiTableFlags.RowBg;
+ }
+ if (showTableBorders)
+ {
+ tableFlags |= ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersV;
+ }
+
+ if (ImGui.BeginTable("DemoTable", 5, tableFlags))
+ {
+ if (showTableHeaders)
+ {
+ // Test flags without width parameters
+ ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.DefaultSort);
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None);
+ ImGui.TableSetupColumn("Category", ImGuiTableColumnFlags.None);
+ ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.None);
+ ImGui.TableSetupColumn("Active", ImGuiTableColumnFlags.None);
+ ImGui.TableHeadersRow();
+ }
+
+ for (int row = 0; row < Math.Min(tableData.Count, 10); row++)
+ {
+ DemoItem item = tableData[row];
+ ImGui.TableNextRow();
+
+ ImGui.TableSetColumnIndex(0);
+ ImGui.Text(item.Id.ToString());
+
+ ImGui.TableSetColumnIndex(1);
+ ImGui.Text(item.Name);
+
+ ImGui.TableSetColumnIndex(2);
+ ImGui.Text(item.Category);
+
+ ImGui.TableSetColumnIndex(3);
+ ImGui.Text($"{item.Value:F2}");
+
+ ImGui.TableSetColumnIndex(4);
+ ImGui.Text(item.Active ? "✓" : "✗");
+ }
+
+ ImGui.EndTable();
+ }
+
+ // Child windows
+ ImGui.SeparatorText("Child Windows:");
+ if (ImGui.BeginChild("ScrollableChild", new Vector2(0, 150)))
+ {
+ for (int i = 0; i < 50; i++)
+ {
+ ImGui.Text($"Scrollable line {i + 1}");
+ }
+ }
+ ImGui.EndChild();
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/NerdFontDemo.cs b/ImGuiAppDemo/Demos/NerdFontDemo.cs
new file mode 100644
index 00000000..ef434656
--- /dev/null
+++ b/ImGuiAppDemo/Demos/NerdFontDemo.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using Hexa.NET.ImGui;
+
+///
+/// Demo for Nerd Font icon support
+///
+internal sealed class NerdFontDemo : IDemoTab
+{
+ public string TabName => "Nerd Fonts";
+
+ public void Update(float deltaTime)
+ {
+ // No updates needed for Nerd Font demo
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.TextWrapped("Nerd Font Icons (Patched Fonts)");
+ ImGui.TextWrapped("This tab demonstrates Nerd Font icons if you're using a Nerd Font (like JetBrains Mono Nerd Font, Fira Code Nerd Font, etc.).");
+
+ // Powerline symbols
+ ImGui.SeparatorText("Powerline Symbols:");
+ ImGui.Text("Basic: \uE0A0 \uE0A1 \uE0A2 \uE0B0 \uE0B1 \uE0B2 \uE0B3");
+ ImGui.Text("Extra: \uE0A3 \uE0B4 \uE0B5 \uE0B6 \uE0B7 \uE0B8 \uE0CA \uE0CC \uE0CD \uE0D0 \uE0D1 \uE0D4");
+
+ // Font Awesome icons
+ ImGui.SeparatorText("Font Awesome Icons");
+ ImGui.Text("Files & Folders: \uF07B \uF07C \uF15B \uF15C \uF016 \uF017 \uF019 \uF01A \uF093 \uF095");
+ ImGui.Text("Git & Version Control: \uF1D3 \uF1D2 \uF126 \uF127 \uF128 \uF129 \uF12A \uF12B");
+ ImGui.Text("Media & UI: \uF04B \uF04C \uF04D \uF050 \uF051 \uF048 \uF049 \uF067 \uF068 \uF00C \uF00D");
+
+ // Material Design icons
+ ImGui.SeparatorText("Material Design Icons");
+ ImGui.Text("Navigation: \uF52A \uF52B \uF544 \uF53F \uF540 \uF541 \uF542 \uF543");
+ ImGui.Text("Actions: \uF8D5 \uF8D6 \uF8D7 \uF8D8 \uF8D9 \uF8DA \uF8DB \uF8DC");
+ ImGui.Text("Content: \uF1C1 \uF1C2 \uF1C3 \uF1C4 \uF1C5 \uF1C6 \uF1C7 \uF1C8");
+
+ // Weather icons
+ ImGui.SeparatorText("Weather Icons");
+ ImGui.Text("Basic Weather: \uE30D \uE30E \uE30F \uE310 \uE311 \uE312 \uE313 \uE314");
+ ImGui.Text("Temperature: \uE315 \uE316 \uE317 \uE318 \uE319 \uE31A \uE31B \uE31C");
+ ImGui.Text("Wind & Pressure: \uE31D \uE31E \uE31F \uE320 \uE321 \uE322 \uE323 \uE324");
+
+ // Devicons
+ ImGui.SeparatorText("Developer Icons (Devicons)");
+ ImGui.Text("Languages: \uE73C \uE73D \uE73E \uE73F \uE740 \uE741 \uE742 \uE743"); // Various programming languages
+ ImGui.Text("Frameworks: \uE744 \uE745 \uE746 \uE747 \uE748 \uE749 \uE74A \uE74B");
+ ImGui.Text("Tools: \uE74C \uE74D \uE74E \uE74F \uE750 \uE751 \uE752 \uE753");
+
+ // Octicons
+ ImGui.SeparatorText("Octicons (GitHub Icons)");
+ ImGui.Text("Version Control: \uF418 \uF419 \uF41A \uF41B \uF41C \uF41D \uF41E \uF41F");
+ ImGui.Text("Issues & PRs: \uF420 \uF421 \uF422 \uF423 \uF424 \uF425 \uF426 \uF427");
+ ImGui.Text("Social: \u2665 \u26A1 \uF428 \uF429 \uF42A \uF42B \uF42C \uF42D");
+
+ // Font Logos
+ ImGui.SeparatorText("Brand Logos (Font Logos)");
+ ImGui.Text("Tech Brands: \uF300 \uF301 \uF302 \uF303 \uF304 \uF305 \uF306 \uF307");
+ ImGui.Text("More Logos: \uF308 \uF309 \uF30A \uF30B \uF30C \uF30D \uF30E \uF30F");
+
+ // Pomicons
+ ImGui.SeparatorText("Pomicons");
+ ImGui.Text("Small Icons: \uE000 \uE001 \uE002 \uE003 \uE004 \uE005 \uE006 \uE007");
+ ImGui.Text("More Icons: \uE008 \uE009 \uE00A \uE00B \uE00C \uE00D");
+
+ ImGui.Separator();
+ ImGui.TextWrapped("Note: These icons will only display correctly if you're using a Nerd Font. " +
+ "If you see question marks or boxes, switch to a Nerd Font like 'JetBrains Mono Nerd Font' or 'Fira Code Nerd Font'.");
+
+ ImGui.Separator();
+ ImGui.TextWrapped("Popular Nerd Fonts: JetBrains Mono Nerd Font, Fira Code Nerd Font, Hack Nerd Font, " +
+ "Source Code Pro Nerd Font, DejaVu Sans Mono Nerd Font, and many more at nerdfonts.com");
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/UnicodeDemo.cs b/ImGuiAppDemo/Demos/UnicodeDemo.cs
new file mode 100644
index 00000000..07f4216e
--- /dev/null
+++ b/ImGuiAppDemo/Demos/UnicodeDemo.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using Hexa.NET.ImGui;
+
+///
+/// Demo for Unicode and emoji support
+///
+internal sealed class UnicodeDemo : IDemoTab
+{
+ public string TabName => "Unicode & Emojis";
+
+ public void Update(float deltaTime)
+ {
+ // No updates needed for Unicode demo
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ ImGui.TextWrapped("Unicode and Emoji Support (Enabled by Default)");
+ ImGui.TextWrapped("ImGuiApp automatically includes support for Unicode characters and emojis. This feature works with your configured fonts.");
+
+ ImGui.SeparatorText("Basic ASCII:");
+ ImGui.Text("Hello World!");
+ ImGui.Text("Accented characters: café, naïve, résumé");
+ ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ ≥ ± × ÷ ∂ ∑ ∏ √ ∫");
+ ImGui.Text("Currency symbols: $ € £ ¥ ₹ ₿");
+ ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ⇐ ⇒ ⇑ ⇓");
+ ImGui.Text("Geometric shapes: ■ □ ▲ △ ● ○ ◆ ◇ ★ ☆");
+ ImGui.Text("Miscellaneous symbols: ♠ ♣ ♥ ♦ ☀ ☁ ☂ ☃ ♪ ♫");
+
+ ImGui.SeparatorText("Full Emoji Range Support (if font supports them)");
+ ImGui.Text("Faces: 😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 😍 😎 🤓 🧐 🤔 😴");
+ ImGui.Text("Gestures: 👍 👎 👌 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐");
+ ImGui.Text("Objects: 🚀 💻 📱 🎸 🎨 🏆 🌟 💎 ⚡ 🔥 💡 🔧 ⚙️ 🔑 💰");
+ ImGui.Text("Nature: 🌈 🌞 🌙 ⭐ 🌍 🌊 🌳 🌸 🦋 🐝 🐶 🐱 🦊 🐻 🐼");
+ ImGui.Text("Food: 🍎 🍌 🍕 🍔 🍟 🍦 🎂 ☕ 🍺 🍷 🍓 🥑 🥨 🧀 🍯");
+ ImGui.Text("Transport: 🚗 🚂 ✈️ 🚲 🚢 🚁 🚌 🏍️ 🛸 🚜 🏎️ 🚙 🚕 🚐");
+ ImGui.Text("Activities: ⚽ 🏀 🏈 ⚾ 🎾 🏐 🏉 🎱 🏓 🏸 🥊 ⛳ 🎯 🎪");
+ ImGui.Text("Weather: ☀️ ⛅ ☁️ 🌤️ ⛈️ 🌧️ ❄️ ☃️ ⛄ 🌬️ 💨 🌊 💧");
+ ImGui.Text("Symbols: ❤️ 💚 💙 💜 🖤 💛 💔 ❣️ 💕 💖 💗 💘 💝 ✨");
+ ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃");
+ ImGui.Text("Math: ± × ÷ = ≠ ≈ ≤ ≥ ∞ √ ∑ ∏ ∂ ∫ Ω π α β γ δ");
+ ImGui.Text("Geometric: ■ □ ▲ △ ● ○ ◆ ◇ ★ ☆ ♠ ♣ ♥ ♦ ▪ ▫ ◾ ◽");
+ ImGui.Text("Currency: $ € £ ¥ ₹ ₿ ¢ ₽ ₩ ₡ ₪ ₫ ₱ ₴ ₦ ₨ ₵");
+ ImGui.Text("Dingbats: ✂ ✈ ☎ ⌚ ⏰ ⏳ ⌛ ⚡ ☔ ☂ ☀ ⭐ ☁ ⛅ ❄");
+ ImGui.Text("Enclosed: ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ");
+
+ ImGui.Separator();
+ ImGui.TextWrapped("Note: Character display depends on your configured font's Unicode support. " +
+ "If characters show as question marks, your font may not include those glyphs.");
+
+ ImGui.Separator();
+ ImGui.TextWrapped("To disable Unicode support (ASCII only), set EnableUnicodeSupport = false in your ImGuiAppConfig.");
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/Demos/UtilityDemo.cs b/ImGuiAppDemo/Demos/UtilityDemo.cs
new file mode 100644
index 00000000..9f90c746
--- /dev/null
+++ b/ImGuiAppDemo/Demos/UtilityDemo.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Demo.Demos;
+
+using System.Text;
+using Hexa.NET.ImGui;
+using ktsu.ImGuiApp;
+
+///
+/// Demo for utility tools and debugging features
+///
+internal sealed class UtilityDemo : IDemoTab
+{
+ private string filePath = "";
+ private string fileContent = "";
+
+ public string TabName => "Utilities & Tools";
+
+ public void Update(float deltaTime)
+ {
+ // No additional windows managed here - all tools are now centralized in ImGuiApp
+ }
+
+ public void Render()
+ {
+ if (ImGui.BeginTabItem(TabName))
+ {
+ // File operations
+ ImGui.SeparatorText("File Operations:");
+ ImGui.InputText("File Path", ref filePath, 256);
+ ImGui.SameLine();
+ if (ImGui.Button("Load") && !string.IsNullOrEmpty(filePath))
+ {
+ try
+ {
+ fileContent = File.ReadAllText(filePath);
+ }
+ catch (Exception ex) when (ex is FileNotFoundException or UnauthorizedAccessException)
+ {
+ // Handle file read errors gracefully
+ fileContent = $"Error loading file: {ex.Message}";
+ }
+ }
+
+ if (!string.IsNullOrEmpty(fileContent))
+ {
+ ImGui.SeparatorText("File Content Preview:");
+ ImGui.TextWrapped(fileContent.Length > 500 ? fileContent[..500] + "..." : fileContent);
+ }
+
+ // System information
+ ImGui.SeparatorText("System Information:");
+ unsafe
+ {
+ byte* ptr = ImGui.GetVersion();
+ int length = 0;
+ while (ptr[length] != 0)
+ {
+ length++;
+ }
+ ImGui.Text($"ImGui Version: {Encoding.UTF8.GetString(ptr, length)}");
+ }
+ ImGui.Text($"Display Size: {ImGui.GetIO().DisplaySize}");
+
+ // Debugging tools
+ ImGui.SeparatorText("Debug Tools:");
+ if (ImGui.Button("Show ImGui Demo"))
+ {
+ ImGuiApp.ShowImGuiDemo();
+ }
+ ImGui.SameLine();
+ if (ImGui.Button("Show Style Editor"))
+ {
+ ImGuiApp.ShowImGuiStyleEditor();
+ }
+ ImGui.SameLine();
+ if (ImGui.Button("Show Metrics"))
+ {
+ ImGuiApp.ShowImGuiMetrics();
+ }
+ ImGui.SameLine();
+ if (ImGui.Button("Show Performance Monitor"))
+ {
+ ImGuiApp.ShowPerformanceMonitor();
+ }
+
+ ImGui.EndTabItem();
+ }
+ }
+}
diff --git a/ImGuiAppDemo/ImGuiAppDemo.cs b/ImGuiAppDemo/ImGuiAppDemo.cs
index 497b3139..7da390cf 100644
--- a/ImGuiAppDemo/ImGuiAppDemo.cs
+++ b/ImGuiAppDemo/ImGuiAppDemo.cs
@@ -4,98 +4,33 @@
namespace ktsu.ImGuiApp.Demo;
-using System.Numerics;
-using System.Text;
-
using Hexa.NET.ImGui;
-
using ktsu.Extensions;
using ktsu.ImGuiApp;
-using ktsu.ImGuiAppDemo.Properties;
-using ktsu.StrongPaths;
+using ktsu.ImGuiApp.Demo.Demos;
+using ktsu.Semantics;
internal static class ImGuiAppDemo
{
- private static bool showImGuiDemo;
- private static bool showStyleEditor;
- private static bool showMetrics;
private static bool showAbout;
+ private static readonly List demoTabs = [];
- // Demo state - Basic Widgets
- private static float sliderValue = 0.5f;
- private static int counter;
- private static bool checkboxState;
- private static string inputText = "Type here...";
- private static Vector3 colorPickerValue = new(0.4f, 0.7f, 0.2f);
- private static Vector4 color4Value = new(1.0f, 0.5f, 0.2f, 1.0f);
- private static readonly Random random = new();
- private static readonly List plotValues = [];
- private static float plotRefreshTime;
-
- // Advanced widget states
- private static int comboSelection;
- private static readonly string[] comboItems = ["Item 1", "Item 2", "Item 3", "Item 4"];
- private static int listboxSelection;
- private static readonly string[] listboxItems = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
- private static float dragFloat = 1.0f;
- private static int dragInt = 50;
- private static Vector3 dragVector = new(1.0f, 2.0f, 3.0f);
- private static float angle;
-
- // Table demo state
- private static readonly List tableData = [];
- private static bool showTableHeaders = true;
- private static bool showTableBorders = true;
-
- // Text rendering state
- private static readonly StringBuilder textBuffer = new(1024);
- private static bool wrapText = true;
- private static float textSpeed = 50.0f;
- private static float animationTime;
-
- // Canvas drawing state
- private static readonly List canvasPoints = [];
- private static Vector4 drawColor = new(1.0f, 1.0f, 0.0f, 1.0f);
- private static float brushSize = 5.0f;
-
- // Modal and popup states
- private static bool showModal;
- private static bool showPopup;
- private static string modalResult = "";
-
- // File operations
- private static string filePath = "";
- private static string fileContent = "";
-
- // Animation demo
- private static float bounceOffset;
- private static float pulseScale = 1.0f;
-
- // Additional UI state
- private static int radioSelection;
- private static string modalInputBuffer = "";
-
- private sealed record DemoItem(int Id, string Name, string Category, float Value, bool Active);
-
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "")]
static ImGuiAppDemo()
{
- // Initialize table data
- for (int i = 0; i < 20; i++)
- {
- string[] categories = ["Category A", "Category B", "Category C"];
- tableData.Add(new DemoItem(
- i,
- $"Item {i + 1}",
- categories[i % 3],
- (float)(random.NextDouble() * 100),
- random.NextDouble() > 0.5
- ));
- }
-
- textBuffer.Append("This is a demonstration of ImGui text editing capabilities.\n");
- textBuffer.Append("You can edit this text, and it will update in real-time.\n");
- textBuffer.Append("ImGui supports multi-line text editing with syntax highlighting possibilities.");
+ // Initialize all demo tabs
+ demoTabs.Add(new BasicWidgetsDemo());
+ demoTabs.Add(new AdvancedWidgetsDemo());
+ demoTabs.Add(new LayoutDemo());
+ demoTabs.Add(new GraphicsDemo());
+ demoTabs.Add(new DataVisualizationDemo());
+ demoTabs.Add(new InputHandlingDemo());
+ demoTabs.Add(new AnimationDemo());
+ demoTabs.Add(new UnicodeDemo());
+ demoTabs.Add(new NerdFontDemo());
+ demoTabs.Add(new ImGuizmoDemo());
+ demoTabs.Add(new ImNodesDemo());
+ demoTabs.Add(new ImPlotDemo());
+ demoTabs.Add(new UtilityDemo());
}
private static void Main() => ImGuiApp.Start(new()
@@ -108,7 +43,7 @@ private static void Main() => ImGuiApp.Start(new()
// Note: EnableUnicodeSupport = true by default, so Unicode and emojis are automatically enabled!
Fonts = new Dictionary
{
- { nameof(Resources.CARDCHAR), Resources.CARDCHAR }
+ { nameof(ktsu.ImGuiAppDemo.Properties.Resources.CARDCHAR), ktsu.ImGuiAppDemo.Properties.Resources.CARDCHAR }
},
// Example of configuring performance settings for throttled rendering
// Uses PID controller for accurate frame rate limiting instead of simple sleep-based approach
@@ -125,37 +60,20 @@ private static void Main() => ImGuiApp.Start(new()
private static void OnRender(float dt)
{
- UpdateAnimations(dt);
- RenderMainDemoWindow();
-
- // Show additional windows based on menu toggles
- if (showImGuiDemo)
- {
- ImGui.ShowDemoWindow(ref showImGuiDemo);
- }
-
- if (showStyleEditor)
+ // Update all demo tabs
+ foreach (IDemoTab demo in demoTabs)
{
- ImGui.Begin("Style Editor", ref showStyleEditor);
- ImGui.ShowStyleEditor();
- ImGui.End();
+ demo.Update(dt);
}
- if (showMetrics)
- {
- ImGui.ShowMetricsWindow(ref showMetrics);
- }
+ // Render main demo window
+ RenderMainDemoWindow();
+ // Show about window if requested
if (showAbout)
{
RenderAboutWindow();
}
-
- // Handle modals and popups
- RenderModalAndPopups();
-
- // Update plot data
- UpdatePlotData(dt);
}
private static void RenderMainDemoWindow()
@@ -163,757 +81,19 @@ private static void RenderMainDemoWindow()
// Create tabs for different demo sections
if (ImGui.BeginTabBar("DemoTabs", ImGuiTabBarFlags.None))
{
- RenderBasicWidgetsTab();
- RenderAdvancedWidgetsTab();
- RenderLayoutTab();
- RenderGraphicsTab();
- RenderDataVisualizationTab();
- RenderInputHandlingTab();
- RenderAnimationTab();
- RenderUnicodeTab();
- RenderNerdFontTab();
- RenderUtilityTab();
- ImGui.EndTabBar();
- }
- }
-
- private static void RenderBasicWidgetsTab()
- {
- if (ImGui.BeginTabItem("Basic Widgets"))
- {
- ImGui.TextWrapped("This tab demonstrates basic ImGui widgets and controls.");
- ImGui.Separator();
-
- // Buttons
- ImGui.Text("Buttons:");
- if (ImGui.Button("Regular Button"))
- {
- counter++;
- }
-
- ImGui.SameLine();
- if (ImGui.SmallButton("Small"))
- {
- counter++;
- }
-
- ImGui.SameLine();
- if (ImGui.ArrowButton("##left", ImGuiDir.Left))
- {
- counter--;
- }
-
- ImGui.SameLine();
- if (ImGui.ArrowButton("##right", ImGuiDir.Right))
- {
- counter++;
- }
-
- ImGui.SameLine();
- ImGui.Text($"Counter: {counter}");
-
- // Checkboxes and Radio buttons
- ImGui.Separator();
- ImGui.Text("Selection Controls:");
- ImGui.Checkbox("Checkbox", ref checkboxState);
-
- ImGui.RadioButton("Option 1", ref radioSelection, 0);
- ImGui.SameLine();
- ImGui.RadioButton("Option 2", ref radioSelection, 1);
- ImGui.SameLine();
- ImGui.RadioButton("Option 3", ref radioSelection, 2);
-
- // Sliders
- ImGui.Separator();
- ImGui.Text("Sliders:");
- ImGui.SliderFloat("Float Slider", ref sliderValue, 0.0f, 1.0f);
- ImGui.SliderFloat("Angle", ref angle, 0.0f, 360.0f, "%.1f deg");
- ImGui.SliderInt("Int Slider", ref dragInt, 0, 100);
-
- // Input fields
- ImGui.Separator();
- ImGui.Text("Input Fields:");
- ImGui.InputText("Text Input", ref inputText, 100);
- ImGui.InputFloat("Float Input", ref dragFloat);
- ImGui.InputFloat3("Vector3 Input", ref dragVector);
-
- // Combo boxes
- ImGui.Separator();
- ImGui.Text("Dropdowns:");
- ImGui.Combo("Combo Box", ref comboSelection, comboItems, comboItems.Length);
- ImGui.ListBox("List Box", ref listboxSelection, listboxItems, listboxItems.Length, 4);
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderAdvancedWidgetsTab()
- {
- if (ImGui.BeginTabItem("Advanced Widgets"))
- {
- // Color controls
- ImGui.Text("Color Controls:");
- ImGui.ColorEdit3("Color RGB", ref colorPickerValue);
- ImGui.ColorEdit4("Color RGBA", ref color4Value);
- ImGui.SetNextItemWidth(200.0f);
- ImGui.ColorPicker3("Color Picker", ref colorPickerValue);
-
- ImGui.Separator();
-
- // Tree view
- ImGui.Text("Tree View:");
- if (ImGui.TreeNode("Root Node"))
- {
- for (int i = 0; i < 5; i++)
- {
- string nodeName = $"Child Node {i}";
- bool nodeOpen = ImGui.TreeNode(nodeName);
-
- if (i == 2 && nodeOpen)
- {
- for (int j = 0; j < 3; j++)
- {
- if (ImGui.TreeNode($"Grandchild {j}"))
- {
- ImGui.Text($"Leaf item {j}");
- ImGui.TreePop();
- }
- }
- }
- else if (nodeOpen)
- {
- ImGui.Text($"Content of {nodeName}");
- }
-
- if (nodeOpen)
- {
- ImGui.TreePop();
- }
- }
- ImGui.TreePop();
- }
-
- ImGui.Separator();
-
- // Progress bars and loading indicators
- ImGui.Text("Progress Indicators:");
- float progress = ((float)Math.Sin(animationTime * 2.0) * 0.5f) + 0.5f;
- ImGui.ProgressBar(progress, new Vector2(-1, 0), $"{progress * 100:F1}%");
-
- // Spinner-like effect
- ImGui.Text("Loading...");
- ImGui.SameLine();
- for (int i = 0; i < 8; i++)
- {
- float rotation = (animationTime * 5.0f) + (i * MathF.PI / 4.0f);
- float alpha = (MathF.Sin(rotation) + 1.0f) * 0.5f;
- ImGui.TextColored(new Vector4(1, 1, 1, alpha), "●");
- if (i < 7)
- {
- ImGui.SameLine();
- }
- }
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderLayoutTab()
- {
- if (ImGui.BeginTabItem("Layout & Tables"))
- {
- // Columns
- ImGui.Text("Columns Layout:");
- ImGui.Columns(3, "DemoColumns");
- ImGui.Separator();
-
- ImGui.Text("Column 1");
- ImGui.NextColumn();
- ImGui.Text("Column 2");
- ImGui.NextColumn();
- ImGui.Text("Column 3");
- ImGui.NextColumn();
-
- for (int i = 0; i < 9; i++)
- {
- ImGui.Text($"Item {i + 1}");
- ImGui.NextColumn();
- }
-
- ImGui.Columns(1);
- ImGui.Separator();
-
- // Tables
- ImGui.Text("Advanced Tables:");
- ImGui.Checkbox("Show Headers", ref showTableHeaders);
- ImGui.SameLine();
- ImGui.Checkbox("Show Borders", ref showTableBorders);
-
- ImGuiTableFlags tableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.Resizable;
- if (showTableHeaders)
- {
- tableFlags |= ImGuiTableFlags.RowBg;
- }
- if (showTableBorders)
- {
- tableFlags |= ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersV;
- }
-
- if (ImGui.BeginTable("DemoTable", 5, tableFlags))
- {
- if (showTableHeaders)
- {
- // Test flags without width parameters
- ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.DefaultSort);
- ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.None);
- ImGui.TableSetupColumn("Category", ImGuiTableColumnFlags.None);
- ImGui.TableSetupColumn("Value", ImGuiTableColumnFlags.None);
- ImGui.TableSetupColumn("Active", ImGuiTableColumnFlags.None);
- ImGui.TableHeadersRow();
- }
-
- for (int row = 0; row < Math.Min(tableData.Count, 10); row++)
- {
- DemoItem item = tableData[row];
- ImGui.TableNextRow();
-
- ImGui.TableSetColumnIndex(0);
- ImGui.Text(item.Id.ToString());
-
- ImGui.TableSetColumnIndex(1);
- ImGui.Text(item.Name);
-
- ImGui.TableSetColumnIndex(2);
- ImGui.Text(item.Category);
-
- ImGui.TableSetColumnIndex(3);
- ImGui.Text($"{item.Value:F2}");
-
- ImGui.TableSetColumnIndex(4);
- ImGui.Text(item.Active ? "✓" : "✗");
- }
-
- ImGui.EndTable();
- }
-
- ImGui.Separator();
-
- // Child windows
- ImGui.Text("Child Windows:");
- if (ImGui.BeginChild("ScrollableChild", new Vector2(0, 150)))
- {
- for (int i = 0; i < 50; i++)
- {
- ImGui.Text($"Scrollable line {i + 1}");
- }
- }
- ImGui.EndChild();
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderGraphicsTab()
- {
- if (ImGui.BeginTabItem("Graphics & Drawing"))
- {
- // Image display
- AbsoluteFilePath iconPath = AppContext.BaseDirectory.As() / "icon.png".As();
- ImGuiAppTextureInfo iconTexture = ImGuiApp.GetOrLoadTexture(iconPath);
-
- ImGui.Text("Image Display:");
- ImGui.Image(iconTexture.TextureRef, new Vector2(64, 64));
- ImGui.SameLine();
- ImGui.Image(iconTexture.TextureRef, new Vector2(32, 32));
- ImGui.SameLine();
- ImGui.Image(iconTexture.TextureRef, new Vector2(16, 16));
-
- ImGui.Separator();
-
- // Custom drawing with ImDrawList
- ImGui.Text("Custom Drawing Canvas:");
- ImGui.ColorEdit4("Draw Color", ref drawColor);
- ImGui.SliderFloat("Brush Size", ref brushSize, 1.0f, 20.0f);
-
- if (ImGui.Button("Clear Canvas"))
- {
- canvasPoints.Clear();
- }
-
- Vector2 canvasPos = ImGui.GetCursorScreenPos();
- Vector2 canvasSize = new(400, 200);
-
- // Draw canvas background
- ImDrawListPtr drawList = ImGui.GetWindowDrawList();
- drawList.AddRectFilled(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.1f, 0.1f, 0.1f, 1.0f)));
- drawList.AddRect(canvasPos, canvasPos + canvasSize, ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0.5f, 0.5f, 1.0f)));
-
- // Handle mouse input for drawing
- ImGui.InvisibleButton("Canvas", canvasSize);
- if (ImGui.IsItemActive() && ImGui.IsMouseDown(ImGuiMouseButton.Left))
- {
- Vector2 mousePos = ImGui.GetMousePos() - canvasPos;
- if (mousePos.X >= 0 && mousePos.Y >= 0 && mousePos.X <= canvasSize.X && mousePos.Y <= canvasSize.Y)
- {
- canvasPoints.Add(mousePos);
- }
- }
-
- // Draw points
- uint color = ImGui.ColorConvertFloat4ToU32(drawColor);
- foreach (Vector2 point in canvasPoints)
- {
- drawList.AddCircleFilled(canvasPos + point, brushSize, color);
- }
-
- // Draw some simple shapes for demonstration
- ImGui.Separator();
- ImGui.Text("Shape Examples:");
- Vector2 shapeStart = ImGui.GetCursorScreenPos();
-
- // Simple animated circle
- float t = animationTime;
- Vector2 center = shapeStart + new Vector2(100, 50);
- float radius = 20 + (MathF.Sin(t * 2) * 5);
- drawList.AddCircle(center, radius, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 1)), 16, 2.0f);
-
- // Moving rectangle
- Vector2 rectPos = shapeStart + new Vector2(200 + (MathF.Sin(t) * 30), 30);
- drawList.AddRectFilled(rectPos, rectPos + new Vector2(40, 40), ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 0, 0.7f)));
-
- ImGui.Dummy(new Vector2(400, 100)); // Reserve space
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderDataVisualizationTab()
- {
- if (ImGui.BeginTabItem("Data Visualization"))
- {
- ImGui.Text("Real-time Data Plots:");
-
- // Line plot
- if (plotValues.Count > 0)
- {
- float[] values = [.. plotValues];
- ImGui.PlotLines("Random Values", ref values[0], values.Length, 0,
- $"Current: {values[^1]:F2}", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100));
-
- ImGui.PlotHistogram("Distribution", ref values[0], values.Length, 0,
- "Histogram", 0.0f, 1.0f, new Vector2(ImGui.GetContentRegionAvail().X, 100));
- }
-
- ImGui.Separator();
-
- // Performance note
- ImGui.Text("Performance Metrics:");
- ImGui.TextWrapped("Performance monitoring is now available in the Debug menu! Use 'Debug > Show Performance Monitor' to see real-time FPS graphs and throttling state.");
-
- ImGui.Separator();
-
- // Font demonstrations
- ImGui.Text("Custom Font Rendering:");
- using (new FontAppearance(nameof(Resources.CARDCHAR), 16))
- {
- ImGui.Text("Small custom font text");
- }
-
- using (new FontAppearance(nameof(Resources.CARDCHAR), 24))
- {
- ImGui.Text("Medium custom font text");
- }
-
- using (new FontAppearance(nameof(Resources.CARDCHAR), 32))
+ // Render all demo tabs
+ foreach (IDemoTab demo in demoTabs)
{
- ImGui.Text("Large custom font text");
+ demo.Render();
}
-
- // Text formatting examples
- ImGui.Separator();
- ImGui.Text("Text Formatting:");
- ImGui.TextColored(new Vector4(1, 0, 0, 1), "Red text");
- ImGui.TextColored(new Vector4(0, 1, 0, 1), "Green text");
- ImGui.TextColored(new Vector4(0, 0, 1, 1), "Blue text");
- ImGui.TextWrapped("This is a long line of text that should wrap to multiple lines when the window is not wide enough to contain it all on a single line.");
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderInputHandlingTab()
- {
- if (ImGui.BeginTabItem("Input & Interaction"))
- {
- ImGui.Text("Mouse Information:");
- Vector2 mousePos = ImGui.GetMousePos();
- Vector2 mouseDelta = ImGui.GetMouseDragDelta(ImGuiMouseButton.Left);
- ImGui.Text($"Mouse Position: ({mousePos.X:F1}, {mousePos.Y:F1})");
- ImGui.Text($"Mouse Delta: ({mouseDelta.X:F1}, {mouseDelta.Y:F1})");
- ImGui.Text($"Left Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Left) ? "DOWN" : "UP")}");
- ImGui.Text($"Right Button: {(ImGui.IsMouseDown(ImGuiMouseButton.Right) ? "DOWN" : "UP")}");
-
- ImGui.Separator();
-
- // Simple drag demonstration
- ImGui.Text("Drag & Drop:");
- ImGui.Button("Drag Source", new Vector2(100, 50));
- ImGui.SameLine();
- ImGui.Button("Drop Target", new Vector2(100, 50));
- ImGui.Text("(Drag and drop functionality would require more complex implementation)");
-
- ImGui.Separator();
-
- // Text editing
- ImGui.Text("Multi-line Text Editor:");
- ImGui.Checkbox("Word Wrap", ref wrapText);
- ImGuiInputTextFlags textFlags = ImGuiInputTextFlags.AllowTabInput;
- if (!wrapText)
- {
- textFlags |= ImGuiInputTextFlags.NoHorizontalScroll;
- }
-
- string textContent = textBuffer.ToString();
- if (ImGui.InputTextMultiline("##TextEditor", ref textContent, 1024, new Vector2(-1, 150), textFlags))
- {
- textBuffer.Clear();
- textBuffer.Append(textContent);
- }
-
- ImGui.Separator();
-
- // Popup and modal buttons
- ImGui.Text("Popups and Modals:");
- if (ImGui.Button("Show Modal"))
- {
- showModal = true;
- modalResult = "";
- }
-
- ImGui.SameLine();
- if (ImGui.Button("Show Popup"))
- {
- showPopup = true;
- }
-
- if (!string.IsNullOrEmpty(modalResult))
- {
- ImGui.Text($"Modal Result: {modalResult}");
- }
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderAnimationTab()
- {
- if (ImGui.BeginTabItem("Animation & Effects"))
- {
- ImGui.Text("Animation Examples:");
-
- // Simple animations
- ImGui.Text("Bouncing Animation:");
- Vector2 ballPos = ImGui.GetCursorScreenPos();
- ballPos.Y += bounceOffset;
- ImDrawListPtr drawList = ImGui.GetWindowDrawList();
- drawList.AddCircleFilled(ballPos + new Vector2(50, 50), 20, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 0, 1)));
- ImGui.Dummy(new Vector2(100, 100));
-
- // Pulsing element
- ImGui.Text("Pulse Animation:");
- Vector2 pulsePos = ImGui.GetCursorScreenPos();
- float pulseSize = 20 * pulseScale;
- drawList.AddCircleFilled(pulsePos + new Vector2(50, 50), pulseSize,
- ImGui.ColorConvertFloat4ToU32(new Vector4(0.5f, 0, 1, 0.7f)));
- ImGui.Dummy(new Vector2(100, 100));
-
- ImGui.Separator();
-
- // Animation controls
- ImGui.Text("Animation Controls:");
- ImGui.SliderFloat("Text Speed", ref textSpeed, 10.0f, 200.0f);
-
- // Animated text (simplified)
- ImGui.Text("Animated text effects:");
- for (int i = 0; i < 20; i++)
- {
- float wave = (MathF.Sin((animationTime * 3.0f) + (i * 0.5f)) * 0.5f) + 0.5f;
- ImGui.TextColored(new Vector4(wave, 1.0f - wave, 0.5f, 1.0f), i % 5 == 4 ? " " : "▓");
- if (i % 5 != 4)
- {
- ImGui.SameLine();
- }
- }
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderUtilityTab()
- {
- if (ImGui.BeginTabItem("Utilities & Tools"))
- {
- // File operations
- ImGui.Text("File Operations:");
- ImGui.InputText("File Path", ref filePath, 256);
- ImGui.SameLine();
- if (ImGui.Button("Load") && !string.IsNullOrEmpty(filePath))
- {
- try
- {
- fileContent = File.ReadAllText(filePath);
- }
- catch (Exception ex) when (ex is FileNotFoundException or UnauthorizedAccessException)
- {
- // Handle file read errors gracefully
- fileContent = $"Error loading file: {ex.Message}";
- }
- }
-
- if (!string.IsNullOrEmpty(fileContent))
- {
- ImGui.Text("File Content Preview:");
- ImGui.TextWrapped(fileContent.Length > 500 ? fileContent[..500] + "..." : fileContent);
- }
-
- ImGui.Separator();
-
- // System information
- ImGui.Text("System Information:");
- unsafe
- {
- byte* ptr = ImGui.GetVersion();
- int length = 0;
- while (ptr[length] != 0)
- {
- length++;
- }
- ImGui.Text($"ImGui Version: {Encoding.UTF8.GetString(ptr, length)}");
- }
- ImGui.Text($"Display Size: {ImGui.GetIO().DisplaySize}");
-
- ImGui.Separator();
-
- // Debugging tools
- ImGui.Text("Debug Tools:");
- if (ImGui.Button("Show ImGui Demo"))
- {
- showImGuiDemo = true;
- }
- ImGui.SameLine();
- if (ImGui.Button("Show Style Editor"))
- {
- showStyleEditor = true;
- }
- ImGui.SameLine();
- if (ImGui.Button("Show Metrics"))
- {
- showMetrics = true;
- }
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderUnicodeTab()
- {
- if (ImGui.BeginTabItem("Unicode & Emojis"))
- {
- ImGui.TextWrapped("Unicode and Emoji Support (Enabled by Default)");
- ImGui.TextWrapped("ImGuiApp automatically includes support for Unicode characters and emojis. This feature works with your configured fonts.");
- ImGui.Separator();
-
- ImGui.Text("Basic ASCII: Hello World!");
- ImGui.Text("Accented characters: café, naïve, résumé");
- ImGui.Text("Mathematical symbols: ∞ ≠ ≈ ≤ ≥ ± × ÷ ∂ ∑ ∏ √ ∫");
- ImGui.Text("Currency symbols: $ € £ ¥ ₹ ₿");
- ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ⇐ ⇒ ⇑ ⇓");
- ImGui.Text("Geometric shapes: ■ □ ▲ △ ● ○ ◆ ◇ ★ ☆");
- ImGui.Text("Miscellaneous symbols: ♠ ♣ ♥ ♦ ☀ ☁ ☂ ☃ ♪ ♫");
-
- ImGui.Separator();
- ImGui.Text("Full Emoji Range Support (if font supports them):");
- ImGui.Text("Faces: 😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 😍 😎 🤓 🧐 🤔 😴");
- ImGui.Text("Gestures: 👍 👎 👌 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐");
- ImGui.Text("Objects: 🚀 💻 📱 🎸 🎨 🏆 🌟 💎 ⚡ 🔥 💡 🔧 ⚙️ 🔑 💰");
- ImGui.Text("Nature: 🌈 🌞 🌙 ⭐ 🌍 🌊 🌳 🌸 🦋 🐝 🐶 🐱 🦊 🐻 🐼");
- ImGui.Text("Food: 🍎 🍌 🍕 🍔 🍟 🍦 🎂 ☕ 🍺 🍷 🍓 🥑 🥨 🧀 🍯");
- ImGui.Text("Transport: 🚗 🚂 ✈️ 🚲 🚢 🚁 🚌 🏍️ 🛸 🚜 🏎️ 🚙 🚕 🚐");
- ImGui.Text("Activities: ⚽ 🏀 🏈 ⚾ 🎾 🏐 🏉 🎱 🏓 🏸 🥊 ⛳ 🎯 🎪");
- ImGui.Text("Weather: ☀️ ⛅ ☁️ 🌤️ ⛈️ 🌧️ ❄️ ☃️ ⛄ 🌬️ 💨 🌊 💧");
- ImGui.Text("Symbols: ❤️ 💚 💙 💜 🖤 💛 💔 ❣️ 💕 💖 💗 💘 💝 ✨");
- ImGui.Text("Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃");
- ImGui.Text("Math: ± × ÷ = ≠ ≈ ≤ ≥ ∞ √ ∑ ∏ ∂ ∫ Ω π α β γ δ");
- ImGui.Text("Geometric: ■ □ ▲ △ ● ○ ◆ ◇ ★ ☆ ♠ ♣ ♥ ♦ ▪ ▫ ◾ ◽");
- ImGui.Text("Currency: $ € £ ¥ ₹ ₿ ¢ ₽ ₩ ₡ ₪ ₫ ₱ ₴ ₦ ₨ ₵");
- ImGui.Text("Dingbats: ✂ ✈ ☎ ⌚ ⏰ ⏳ ⌛ ⚡ ☔ ☂ ☀ ⭐ ☁ ⛅ ❄");
- ImGui.Text("Enclosed: ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ");
-
- ImGui.Separator();
- ImGui.TextWrapped("Note: Character display depends on your configured font's Unicode support. " +
- "If characters show as question marks, your font may not include those glyphs.");
-
- ImGui.Separator();
- ImGui.TextWrapped("To disable Unicode support (ASCII only), set EnableUnicodeSupport = false in your ImGuiAppConfig.");
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderNerdFontTab()
- {
- if (ImGui.BeginTabItem("Nerd Fonts"))
- {
- ImGui.TextWrapped("Nerd Font Icons (Patched Fonts)");
- ImGui.TextWrapped("This tab demonstrates Nerd Font icons if you're using a Nerd Font (like JetBrains Mono Nerd Font, Fira Code Nerd Font, etc.).");
- ImGui.Separator();
-
- // Powerline symbols
- ImGui.Text("Powerline Symbols:");
- ImGui.Text("Basic: \uE0A0 \uE0A1 \uE0A2 \uE0B0 \uE0B1 \uE0B2 \uE0B3");
- ImGui.Text("Extra: \uE0A3 \uE0B4 \uE0B5 \uE0B6 \uE0B7 \uE0B8 \uE0CA \uE0CC \uE0CD \uE0D0 \uE0D1 \uE0D4");
-
- ImGui.Separator();
-
- // Font Awesome icons
- ImGui.Text("Font Awesome Icons:");
- ImGui.Text("Files & Folders: \uF07B \uF07C \uF15B \uF15C \uF016 \uF017 \uF019 \uF01A \uF093 \uF095");
- ImGui.Text("Git & Version Control: \uF1D3 \uF1D2 \uF126 \uF127 \uF128 \uF129 \uF12A \uF12B");
- ImGui.Text("Media & UI: \uF04B \uF04C \uF04D \uF050 \uF051 \uF048 \uF049 \uF067 \uF068 \uF00C \uF00D");
-
- ImGui.Separator();
-
- // Material Design icons
- ImGui.Text("Material Design Icons:");
- ImGui.Text("Navigation: \uF52A \uF52B \uF544 \uF53F \uF540 \uF541 \uF542 \uF543");
- ImGui.Text("Actions: \uF8D5 \uF8D6 \uF8D7 \uF8D8 \uF8D9 \uF8DA \uF8DB \uF8DC");
- ImGui.Text("Content: \uF1C1 \uF1C2 \uF1C3 \uF1C4 \uF1C5 \uF1C6 \uF1C7 \uF1C8");
-
- ImGui.Separator();
-
- // Weather icons
- ImGui.Text("Weather Icons:");
- ImGui.Text("Basic Weather: \uE30D \uE30E \uE30F \uE310 \uE311 \uE312 \uE313 \uE314");
- ImGui.Text("Temperature: \uE315 \uE316 \uE317 \uE318 \uE319 \uE31A \uE31B \uE31C");
- ImGui.Text("Wind & Pressure: \uE31D \uE31E \uE31F \uE320 \uE321 \uE322 \uE323 \uE324");
-
- ImGui.Separator();
-
- // Devicons
- ImGui.Text("Developer Icons (Devicons):");
- ImGui.Text("Languages: \uE73C \uE73D \uE73E \uE73F \uE740 \uE741 \uE742 \uE743"); // Various programming languages
- ImGui.Text("Frameworks: \uE744 \uE745 \uE746 \uE747 \uE748 \uE749 \uE74A \uE74B");
- ImGui.Text("Tools: \uE74C \uE74D \uE74E \uE74F \uE750 \uE751 \uE752 \uE753");
-
- ImGui.Separator();
-
- // Octicons
- ImGui.Text("Octicons (GitHub Icons):");
- ImGui.Text("Version Control: \uF418 \uF419 \uF41A \uF41B \uF41C \uF41D \uF41E \uF41F");
- ImGui.Text("Issues & PRs: \uF420 \uF421 \uF422 \uF423 \uF424 \uF425 \uF426 \uF427");
- ImGui.Text("Social: \u2665 \u26A1 \uF428 \uF429 \uF42A \uF42B \uF42C \uF42D");
-
- ImGui.Separator();
-
- // Font Logos
- ImGui.Text("Brand Logos (Font Logos):");
- ImGui.Text("Tech Brands: \uF300 \uF301 \uF302 \uF303 \uF304 \uF305 \uF306 \uF307");
- ImGui.Text("More Logos: \uF308 \uF309 \uF30A \uF30B \uF30C \uF30D \uF30E \uF30F");
-
- ImGui.Separator();
-
- // Pomicons
- ImGui.Text("Pomicons:");
- ImGui.Text("Small Icons: \uE000 \uE001 \uE002 \uE003 \uE004 \uE005 \uE006 \uE007");
- ImGui.Text("More Icons: \uE008 \uE009 \uE00A \uE00B \uE00C \uE00D");
-
- ImGui.Separator();
- ImGui.TextWrapped("Note: These icons will only display correctly if you're using a Nerd Font. " +
- "If you see question marks or boxes, switch to a Nerd Font like 'JetBrains Mono Nerd Font' or 'Fira Code Nerd Font'.");
-
- ImGui.Separator();
- ImGui.TextWrapped("Popular Nerd Fonts: JetBrains Mono Nerd Font, Fira Code Nerd Font, Hack Nerd Font, " +
- "Source Code Pro Nerd Font, DejaVu Sans Mono Nerd Font, and many more at nerdfonts.com");
-
- ImGui.EndTabItem();
- }
- }
-
- private static void RenderModalAndPopups()
- {
- // Modal dialog
- if (showModal)
- {
- ImGui.OpenPopup("Demo Modal");
- showModal = false;
- }
-
- if (ImGui.BeginPopupModal("Demo Modal", ref showModal))
- {
- ImGui.Text("This is a modal dialog.");
- ImGui.Text("It blocks interaction with the main window.");
- ImGui.Separator();
-
- ImGui.InputText("Input", ref modalInputBuffer, 100);
-
- if (ImGui.Button("OK"))
- {
- modalResult = $"You entered: {modalInputBuffer}";
- ImGui.CloseCurrentPopup();
- }
- ImGui.SameLine();
- if (ImGui.Button("Cancel"))
- {
- modalResult = "Cancelled";
- ImGui.CloseCurrentPopup();
- }
-
- ImGui.EndPopup();
- }
-
- // Context popup
- if (showPopup)
- {
- ImGui.OpenPopup("Demo Popup");
- showPopup = false;
- }
-
- if (ImGui.BeginPopup("Demo Popup"))
- {
- ImGui.Text("This is a popup menu");
- if (ImGui.MenuItem("Option 1"))
- {
- // Handle option 1
- }
- if (ImGui.MenuItem("Option 2"))
- {
- // Handle option 2
- }
- ImGui.Separator();
- if (ImGui.MenuItem("Close"))
- {
- // Handle close
- }
- ImGui.EndPopup();
+ ImGui.EndTabBar();
}
}
- private static void UpdateAnimations(float dt)
- {
- animationTime += dt;
-
- // Bouncing animation
- bounceOffset = MathF.Abs(MathF.Sin(animationTime * 3)) * 50;
-
- // Pulse animation
- pulseScale = 0.8f + (0.4f * MathF.Sin(animationTime * 4));
- }
-
private static void RenderAboutWindow()
{
ImGui.Begin("About ImGuiApp Demo", ref showAbout);
- ImGui.Text("ImGuiApp Demo Application");
- ImGui.Separator();
+ ImGui.SeparatorText("ImGuiApp Demo Application");
ImGui.Text("This demo showcases extensive ImGui.NET features including:");
ImGui.BulletText("Basic and advanced widgets");
ImGui.BulletText("Layout systems (columns, tables, tabs)");
@@ -922,39 +102,21 @@ private static void RenderAboutWindow()
ImGui.BulletText("Input handling and interaction");
ImGui.BulletText("Animations and effects");
ImGui.BulletText("File operations and utilities");
- ImGui.Separator();
- ImGui.Text("Built with:");
+ ImGui.BulletText("3D manipulation gizmos (ImGuizmo)");
+ ImGui.BulletText("Node-based editing (ImNodes)");
+ ImGui.BulletText("Advanced plotting (ImPlot)");
+ ImGui.SeparatorText("Built with");
ImGui.BulletText("Hexa.NET.ImGui");
+ ImGui.BulletText("Hexa.NET.ImGuizmo - 3D manipulation gizmos");
+ ImGui.BulletText("Hexa.NET.ImNodes - Node editor system");
+ ImGui.BulletText("Hexa.NET.ImPlot - Advanced plotting library");
ImGui.BulletText("Silk.NET");
ImGui.BulletText("ktsu.ImGuiApp Framework");
ImGui.End();
}
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "This is a demo application")]
- private static void UpdatePlotData(float dt)
- {
- plotRefreshTime += dt;
- if (plotRefreshTime >= 0.1f) // Update every 100ms
- {
- plotRefreshTime = 0;
- plotValues.Add((float)random.NextDouble());
- if (plotValues.Count > 100) // Keep last 100 values
- {
- plotValues.RemoveAt(0);
- }
- }
- }
-
private static void OnAppMenu()
{
- if (ImGui.BeginMenu("View"))
- {
- ImGui.MenuItem("ImGui Demo", string.Empty, ref showImGuiDemo);
- ImGui.MenuItem("Style Editor", string.Empty, ref showStyleEditor);
- ImGui.MenuItem("Metrics", string.Empty, ref showMetrics);
- ImGui.EndMenu();
- }
-
if (ImGui.BeginMenu("Help"))
{
ImGui.MenuItem("About", string.Empty, ref showAbout);
diff --git a/ImGuiAppDemo/ImGuiAppDemo.csproj b/ImGuiAppDemo/ImGuiAppDemo.csproj
index d1dd6120..b4f90a7e 100644
--- a/ImGuiAppDemo/ImGuiAppDemo.csproj
+++ b/ImGuiAppDemo/ImGuiAppDemo.csproj
@@ -9,6 +9,10 @@
+
+
+
+
diff --git a/README.md b/README.md
index 607e49cf..b32f12af 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ ImGuiApp is a .NET library that provides application scaffolding for [Dear ImGui
- **Simple API**: Create ImGui applications with minimal boilerplate code
- **Full Integration**: Seamless integration with Silk.NET for OpenGL and input handling
-- **Window Management**: Automatic window state, rendering, and input handling
+- **Window Management**: Automatic window state, rendering, and input handling with startup state validation
- **Performance Optimization**: Sleep-based throttled rendering with lowest-selection logic when unfocused, idle, or not visible to maximize resource savings
- **PID Frame Limiting**: Precision frame rate control using a PID controller with comprehensive auto-tuning capabilities for highly accurate target FPS achievement
- **DPI Awareness**: Built-in support for high-DPI displays and scaling
@@ -27,6 +27,8 @@ ImGuiApp is a .NET library that provides application scaffolding for [Dear ImGui
- **Lifecycle Callbacks**: Customizable delegate callbacks for application events
- **Menu System**: Easy-to-use API for creating application menus
- **Positioning Guards**: Offscreen positioning checks to keep windows visible
+- **Rendering Precision**: Enhanced ImGui rendering with pixel-perfect alignment and sub-pixel precision controls
+- **Error Recovery**: Built-in error recovery features with configurable debugging options
- **Modern .NET**: Supports .NET 9 and newer
- **Active Development**: Open-source and actively maintained
@@ -34,7 +36,7 @@ ImGuiApp is a .NET library that provides application scaffolding for [Dear ImGui
### Prerequisites
-- .NET 9.0 or later
+- .NET 9.0 or later (currently using .NET 9.0.301)
## Installation
@@ -522,11 +524,20 @@ Check out the included demo project to see a comprehensive working example:
1. Clone or download the repository
2. Open the solution in Visual Studio (or run dotnet build)
3. Start the ImGuiAppDemo project to see a feature-rich ImGui application
-4. Explore the different tabs:
- - **Unicode & Emojis**: Test character rendering with extended Unicode support
- - **Widgets & Layout**: Comprehensive ImGui widget demonstrations
- - **Graphics & Plotting**: Custom drawing and data visualization examples
- - **Nerd Font Icons**: Browse and test various icon sets and glyphs
+4. Explore the comprehensive demo tabs:
+ - **Basic Widgets**: Essential UI elements like buttons, inputs, sliders
+ - **Advanced Widgets**: Complex controls, combo boxes, list boxes, color pickers
+ - **Layout & Tables**: Column layouts, advanced tables with sorting and resizing
+ - **Graphics & Drawing**: Custom drawing using ImDrawList for canvas-like functionality
+ - **Data Visualization**: Real-time plotting with performance metrics and graphs
+ - **Input Handling**: Mouse input information and drag-and-drop demonstrations
+ - **Animation**: Dynamic content and animated elements
+ - **Unicode & Emojis**: Extended character support with comprehensive symbol sets
+ - **Nerd Font Icons**: Icon sets and specialized glyph demonstrations
+ - **ImGuizmo**: 3D gizmo controls for object manipulation
+ - **ImNodes**: Node-based editor demonstrations
+ - **ImPlot**: Advanced plotting and charting capabilities
+ - **Utilities & Tools**: Modal dialogs, popups, file operations, and system information
5. Use the debug menu to access additional features:
- **Debug > Show Performance Monitor**: Real-time FPS graph showing PID controller performance with comprehensive auto-tuning capabilities
- **Debug > Show ImGui Demo**: Official ImGui demo window
@@ -541,6 +552,8 @@ The **Performance Monitor** includes:
Perfect for seeing both the throttling system and PID controller work in real-time!
+The demo application has been recently enhanced with comprehensive ImGui feature demonstrations, including advanced rendering precision improvements with pixel-perfect alignment, error recovery features, and extensive widget showcases across 13 different demo tabs.
+
## Contributing
Contributions are welcome! Here's how you can help:
@@ -565,7 +578,8 @@ Check the [CHANGELOG.md](CHANGELOG.md) for detailed release notes and version ch
- [Dear ImGui](https://github.com/ocornut/imgui) - The immediate mode GUI library
- [Hexa.NET.ImGui](https://github.com/HexaEngine/Hexa.NET.ImGui) - .NET bindings for Dear ImGui
-- [Silk.NET](https://github.com/dotnet/Silk.NET) - .NET bindings for OpenGL and windowing
+- [Silk.NET](https://github.com/dotnet/Silk.NET) - .NET bindings for OpenGL and windowing (includes OpenGL, Input, SDL2, and additional extensions)
+- [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) - Cross-platform image processing library
- All contributors and the .NET community for their support
## Support