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