Skip to content

Commit 5a57a95

Browse files
Merge pull request #623 from ktsu-dev/claude/project-thread-dod34n
[minor] Add ImGuiApp.EnableExtensions to turn on ImGui extensions after launch
2 parents 7a837a6 + 0a41dd5 commit 5a57a95

8 files changed

Lines changed: 493 additions & 15 deletions

File tree

‎CLAUDE.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ what the application does. Detection is cached per extension, so a later `Initia
235235
that an earlier call left out still finds it. Hexa's ImNodes and ImPlot bindings have no
236236
`BeginFrame`, so their `Is...Available` is answered by `CreateContext`.
237237

238+
`ImGuiApp.EnableExtensions(ImGuiAppExtensions)` turns extensions on after launch, for an
239+
application that launches with `None` and wants ImNodes the first time a node editor opens. It goes
240+
through `ImGuiExtensionManager.Enable`, which **adds** to `Enabled` where `Initialize` replaces it,
241+
so an extension already on keeps its context; it probes, hands over the ImGui context and creates
242+
the dark-styled context exactly as launch does, and is idempotent. It returns which of the requested
243+
extensions are ready (ImGuizmo found; ImNodes and ImPlot with a context). Call it on the render
244+
thread, between frames or inside `OnRender` before the extension is drawn with; it throws before
245+
launch, where `ImGuiAppConfig.Extensions` is the setting and launch would replace anything enabled
246+
earlier. One trap: ImGuizmo draws into the window its `BeginFrame` opens, and inside a frame the
247+
per-frame `BeginFrame` has already run, so `Enable` begins ImGuizmo itself when the ImGui context is
248+
`WithinFrameScope`. Without that, `Manipulate` in the same frame segfaults (verified by
249+
`EnableExtensionsTests`). ImNodes and ImPlot have no `BeginFrame` and need nothing extra. The
250+
harness shares its context the same way the controller does, so it works there unchanged;
251+
`tests/ImGui.App.Testing.Tests/EnableExtensionsTests.cs` drives it through real frames.
252+
238253
### Built-in menus
239254

240255
`ImGuiApp.RenderAppMenu` appends an Accessibility menu (UI scale) and a Debug menu (ImGui demo,

‎ImGui.App.Testing/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ application gets: ImGuizmo, ImNodes and ImPlot are detected, handed the ImGui co
101101
own contexts, and ticked once per frame. An application that draws with any of them renders under
102102
the harness rather than faulting inside native code.
103103

104+
`ImGuiAppConfig.Extensions` is honoured, and `ImGuiApp.EnableExtensions` works mid-run exactly as it
105+
does in the application: a harness started with `ImGuiAppExtensions.None` can enable ImNodes between
106+
steps, or inside `OnRender` before the first node editor draws, and draw with it in that frame.
107+
104108
`ImGuiAppConfig.EnableDocking` is honored too, and applied before the first frame, which is the only
105109
point ImGui accepts it. A configuration that sets it can therefore use
106110
`ImGuiWidgets.DrawDeferredDocked()` in a test exactly as it does in the application.

‎ImGui.App/ImGuiApp.cs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,48 @@ public static bool TryGetRenderer3D([NotNullWhen(true)] out IRenderer3D? rendere
907907
return renderer3D is not null;
908908
}
909909

910+
/// <summary>
911+
/// Turns on ImGui extensions after launch, adding them to those
912+
/// <see cref="ImGuiAppConfig.Extensions"/> already set up.
913+
/// </summary>
914+
/// <remarks>
915+
/// <para>
916+
/// This is for an application that launches with fewer extensions than it may eventually draw
917+
/// with, to keep their start-up cost off the launch path: launch with
918+
/// <see cref="ImGuiAppExtensions.None"/>, then enable <see cref="ImGuiAppExtensions.ImNodes"/>
919+
/// the first time a node editor opens. Each extension is set up exactly as it would have been at
920+
/// launch — probed, handed the ImGui context, and for ImNodes and ImPlot given a context of its
921+
/// own with the dark style — and extensions already on are left as they are.
922+
/// </para>
923+
/// <para>
924+
/// Call it on the render thread, between frames or inside <see cref="ImGuiAppConfig.OnRender"/>
925+
/// before the extension is drawn with. It is idempotent, so calling it every time a window opens
926+
/// is fine. It works the same under <c>ImGuiAppHarness</c>, which shares the ImGui context the
927+
/// same way the application does.
928+
/// </para>
929+
/// </remarks>
930+
/// <param name="extensions">The extensions to turn on.</param>
931+
/// <returns>
932+
/// Which of <paramref name="extensions"/> are ready to draw with. One asked for and missing from
933+
/// the result is not referenced by the application, and drawing with it faults in native code.
934+
/// <see cref="ImGuiExtensionManager.IsImNodesContextCreated"/> and its siblings answer the same
935+
/// question later.
936+
/// </returns>
937+
/// <exception cref="InvalidOperationException">
938+
/// No application or harness has shared its ImGui context yet. Before launch, set
939+
/// <see cref="ImGuiAppConfig.Extensions"/> instead, which launch would otherwise replace.
940+
/// </exception>
941+
public static ImGuiAppExtensions EnableExtensions(ImGuiAppExtensions extensions)
942+
{
943+
if (!ImGuiExtensionManager.IsImGuiContextShared)
944+
{
945+
throw new InvalidOperationException(
946+
"No ImGui context has been shared with the extensions yet. Call EnableExtensions after launch, or set ImGuiAppConfig.Extensions before it.");
947+
}
948+
949+
return ImGuiExtensionManager.Enable(extensions);
950+
}
951+
910952
/// <summary>
911953
/// Releases the state established by <see cref="BeginExternalFrameSession()"/>.
912954
/// </summary>

‎ImGui.App/ImGuiAppConfig.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ public class ImGuiAppConfig
296296
/// <para>
297297
/// <see cref="AutoDiscoverExtensions"/> set to false overrides this and sets up none.
298298
/// </para>
299+
/// <para>
300+
/// An extension left out here can still be turned on after launch with
301+
/// <c>ImGuiApp.EnableExtensions</c>, which adds to this set, so an application can launch with
302+
/// <see cref="ImGuiAppExtensions.None"/> and enable ImNodes the first time a node editor opens.
303+
/// </para>
299304
/// </summary>
300305
public ImGuiAppExtensions Extensions { get; init; } = ImGuiAppExtensions.All;
301306

‎ImGui.App/ImGuiExtensionManager.cs‎

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ public static class ImGuiExtensionManager
3333
private static object? nodesContext;
3434
private static object? plotContext;
3535

36-
// Track whether the ImGui context has been shared with extensions
37-
private static bool imGuiContextSet;
38-
3936
// Extensions whose assembly has been probed, so each is loaded and reflected over at most once
4037
private static ImGuiAppExtensions probed;
4138

@@ -58,8 +55,102 @@ public static class ImGuiExtensionManager
5855
public static void Initialize(ImGuiAppExtensions extensions)
5956
{
6057
Enabled = extensions & ImGuiAppExtensions.All;
58+
Probe(Enabled);
59+
}
60+
61+
/// <summary>
62+
/// Turns on the given extensions in addition to those already enabled, after launch. Unlike
63+
/// <see cref="Initialize(ImGuiAppExtensions)"/>, which replaces the enabled set, this only adds
64+
/// to it, so an extension that is already on keeps its context and its state.
65+
/// </summary>
66+
/// <remarks>
67+
/// <para>
68+
/// Each newly enabled extension is set up exactly as it is at launch: its assembly is probed if
69+
/// it has not been, it is handed the ImGui context when <see cref="SetImGuiContext(ImGuiContextPtr?)"/>
70+
/// has already run, and ImNodes and ImPlot are given a context of their own with the dark
71+
/// style. ImGuizmo, which needs a <c>BeginFrame</c> every frame, gets one at once when this is
72+
/// called inside a frame, so it can be drawn with in the same frame; <see cref="BeginFrame"/>
73+
/// covers it from the next frame on.
74+
/// </para>
75+
/// <para>
76+
/// Call it on the render thread, between frames or inside <c>OnRender</c> before the extension
77+
/// is drawn with. Enabling an extension that is already on does nothing.
78+
/// </para>
79+
/// </remarks>
80+
/// <param name="extensions">The extensions to turn on.</param>
81+
/// <returns>
82+
/// Which of <paramref name="extensions"/> are ready to draw with: ImGuizmo when its assembly was
83+
/// found, ImNodes and ImPlot when their context exists. Before the ImGui context has been shared,
84+
/// no context can exist yet, so ImNodes and ImPlot are reported when their assembly was found.
85+
/// An extension asked for and missing from the result is not referenced, and drawing with it
86+
/// faults in native code.
87+
/// </returns>
88+
public static ImGuiAppExtensions Enable(ImGuiAppExtensions extensions)
89+
{
90+
ImGuiAppExtensions requested = extensions & ImGuiAppExtensions.All;
91+
ImGuiAppExtensions added = requested & ~Enabled;
92+
93+
if (added != ImGuiAppExtensions.None)
94+
{
95+
Enabled |= added;
96+
Probe(added);
97+
98+
// Before the context is shared, launch will set these up along with everything else.
99+
if (IsImGuiContextShared)
100+
{
101+
ImGuiContextPtr context = ImGui.GetCurrentContext();
102+
SetImGuiContextFor(added, context);
103+
CreateExtensionContexts();
104+
105+
// ImGuizmo draws into a window its BeginFrame opens; inside a frame that has already
106+
// begun, BeginFrame has passed it by, so it is begun here instead.
107+
if (added.HasFlag(ImGuiAppExtensions.ImGuizmo) && context.WithinFrameScope)
108+
{
109+
BeginFrameFor(ImGuiAppExtensions.ImGuizmo);
110+
}
111+
}
112+
}
113+
114+
return Ready(requested);
115+
}
116+
117+
/// <summary>
118+
/// Gets which of the given extensions are ready to draw with. See <see cref="Enable(ImGuiAppExtensions)"/>.
119+
/// </summary>
120+
private static ImGuiAppExtensions Ready(ImGuiAppExtensions extensions)
121+
{
122+
ImGuiAppExtensions ready = ImGuiAppExtensions.None;
123+
124+
if (IsImGuizmoAvailable)
125+
{
126+
ready |= ImGuiAppExtensions.ImGuizmo;
127+
}
128+
129+
if (IsImGuiContextShared ? IsImNodesContextCreated : IsImNodesAvailable)
130+
{
131+
ready |= ImGuiAppExtensions.ImNodes;
132+
}
133+
134+
if (IsImGuiContextShared ? IsImPlotContextCreated : IsImPlotAvailable)
135+
{
136+
ready |= ImGuiAppExtensions.ImPlot;
137+
}
138+
139+
return ready & extensions;
140+
}
61141

62-
ImGuiAppExtensions toProbe = Enabled & ~probed;
142+
/// <summary>
143+
/// Gets whether <see cref="SetImGuiContext(ImGuiContextPtr?)"/> has run since the last
144+
/// <see cref="Cleanup"/>, which is what a launched application or a started harness has done.
145+
/// </summary>
146+
internal static bool IsImGuiContextShared { get; private set; }
147+
148+
/// <summary>
149+
/// Loads and reflects over each of the given extensions that has not been probed already.
150+
/// </summary>
151+
private static void Probe(ImGuiAppExtensions extensions)
152+
{
153+
ImGuiAppExtensions toProbe = extensions & ~probed;
63154
probed |= toProbe;
64155

65156
if (toProbe.HasFlag(ImGuiAppExtensions.ImGuizmo))
@@ -213,15 +304,25 @@ private static void InitializeImPlot()
213304
public static void BeginFrame()
214305
{
215306
// Guard: extension BeginFrame calls require a valid ImGui context
216-
if (!imGuiContextSet)
307+
if (!IsImGuiContextShared)
217308
{
218309
return;
219310
}
220311

312+
BeginFrameFor(Enabled);
313+
}
314+
315+
/// <summary>
316+
/// Calls BeginFrame for each of the given extensions that is enabled and has one.
317+
/// </summary>
318+
private static void BeginFrameFor(ImGuiAppExtensions extensions)
319+
{
320+
ImGuiAppExtensions toBegin = extensions & Enabled;
321+
221322
// Call ImGuizmo.BeginFrame() if available
222323
try
223324
{
224-
if (IsEnabled(ImGuiAppExtensions.ImGuizmo))
325+
if (toBegin.HasFlag(ImGuiAppExtensions.ImGuizmo))
225326
{
226327
imGuizmoBeginFrame?.Invoke(null, null);
227328
}
@@ -234,7 +335,7 @@ public static void BeginFrame()
234335
// Call ImNodes.BeginFrame() if available
235336
try
236337
{
237-
if (IsEnabled(ImGuiAppExtensions.ImNodes))
338+
if (toBegin.HasFlag(ImGuiAppExtensions.ImNodes))
238339
{
239340
imNodesBeginFrame?.Invoke(null, null);
240341
}
@@ -247,7 +348,7 @@ public static void BeginFrame()
247348
// Call ImPlot.BeginFrame() if available
248349
try
249350
{
250-
if (IsEnabled(ImGuiAppExtensions.ImPlot))
351+
if (toBegin.HasFlag(ImGuiAppExtensions.ImPlot))
251352
{
252353
imPlotBeginFrame?.Invoke(null, null);
253354
}
@@ -265,12 +366,21 @@ public static void BeginFrame()
265366
public static void SetImGuiContext(ImGuiContextPtr? context = null)
266367
{
267368
// Use current context if none provided
268-
ImGuiContextPtr contextToSet = context ?? ImGui.GetCurrentContext();
369+
SetImGuiContextFor(Enabled, context ?? ImGui.GetCurrentContext());
370+
IsImGuiContextShared = true;
371+
}
372+
373+
/// <summary>
374+
/// Hands the ImGui context to each of the given extensions that is enabled.
375+
/// </summary>
376+
private static void SetImGuiContextFor(ImGuiAppExtensions extensions, ImGuiContextPtr contextToSet)
377+
{
378+
ImGuiAppExtensions toSet = extensions & Enabled;
269379

270380
// Set context for ImGuizmo if available
271381
try
272382
{
273-
if (IsEnabled(ImGuiAppExtensions.ImGuizmo))
383+
if (toSet.HasFlag(ImGuiAppExtensions.ImGuizmo))
274384
{
275385
imGuizmoSetImGuiContext?.Invoke(null, [contextToSet]);
276386
}
@@ -283,7 +393,7 @@ public static void SetImGuiContext(ImGuiContextPtr? context = null)
283393
// Set context for ImNodes if available
284394
try
285395
{
286-
if (IsEnabled(ImGuiAppExtensions.ImNodes))
396+
if (toSet.HasFlag(ImGuiAppExtensions.ImNodes))
287397
{
288398
imNodesSetImGuiContext?.Invoke(null, [contextToSet]);
289399
}
@@ -296,7 +406,7 @@ public static void SetImGuiContext(ImGuiContextPtr? context = null)
296406
// Set context for ImPlot if available
297407
try
298408
{
299-
if (IsEnabled(ImGuiAppExtensions.ImPlot))
409+
if (toSet.HasFlag(ImGuiAppExtensions.ImPlot))
300410
{
301411
imPlotSetImGuiContext?.Invoke(null, [contextToSet]);
302412
}
@@ -305,8 +415,6 @@ public static void SetImGuiContext(ImGuiContextPtr? context = null)
305415
{
306416
DebugLogger.Log($"ImGuiExtensionManager: Error calling ImPlot.SetImGuiContext(): {ex.InnerException?.Message ?? ex.Message}");
307417
}
308-
309-
imGuiContextSet = true;
310418
}
311419

312420
/// <summary>
@@ -424,7 +532,7 @@ public static void Cleanup()
424532
}
425533
}
426534

427-
imGuiContextSet = false;
535+
IsImGuiContextShared = false;
428536
}
429537

430538
/// <summary>

‎ImGui.App/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ The main entry point for creating and managing ImGui applications.
509509
| `EmsToPx` | `float ems` | `int` | Converts a value in ems to pixels based on current font size |
510510
| `PtsToPx` | `int pts` | `int` | Converts a value in points to pixels based on current scale factor |
511511
| `UseImageBytes` | `ImagePixels image, Action<byte[]> action` | `void` | Executes an action with temporary access to image bytes using pooled memory |
512+
| `EnableExtensions` | `ImGuiAppExtensions extensions` | `ImGuiAppExtensions` | Turns on ImGuizmo, ImNodes or ImPlot after launch, adding to those `Extensions` set up; returns which of them are ready to draw with. Call on the render thread, between frames or in `OnRender` before drawing with the extension. Idempotent |
512513

513514
### `ImGuiAppConfig` Class
514515

@@ -526,6 +527,7 @@ Configuration for the ImGui application.
526527
| `Fonts` | `Dictionary<string, byte[]>` | `[]` | Font name to font data mapping |
527528
| `EnableUnicodeSupport` | `bool` | `true` | Whether to enable Unicode and emoji support |
528529
| `SaveIniSettings` | `bool` | `true` | Whether ImGui should save window settings to imgui.ini |
530+
| `Extensions` | `ImGuiAppExtensions` | `All` | Which of ImGuizmo, ImNodes and ImPlot are set up at launch. One left out costs nothing at startup and can be turned on later with `ImGuiApp.EnableExtensions` |
529531
| `PerformanceSettings` | `ImGuiAppPerformanceSettings` | `new()` | Performance settings for throttled rendering |
530532
| `OnStart` | `Action` | `() => { }` | Called when the application starts |
531533
| `FrameWrapperFactory` | `Func<ScopedAction?>` | `() => null` | Factory for creating frame wrappers |

0 commit comments

Comments
 (0)