Skip to content

[minor] Give NodeEditorRenderer a hook for host content in a node body - #443

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/nodeeditor-441-node-body-hook
Sep 23, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/nodeeditor-441-node-body-hook

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #441

What was wrong

RenderNode drew a fixed node body — a title bar, one ImGui.Text per input pin, one right-aligned ImGui.Text per output pin — with no callback anywhere on the type. A host wanting a value editable on the node face had two options and both were bad: fork the renderer, giving up zoom, hover highlighting, the measured position and dimension feedback and the pin offset publishing, all of which live in that class; or draw with raw ImNodes and keep NodeEditorEngine for topology and physics alone, which loses the same things.

ImNodes itself has no such restriction — anything submitted between BeginNode and EndNode is drawn in the node and sized into it.

The change

public Action<Node>? DrawNodeBody { get; set; }

Invoked inside each node, after its pins and before EndNode. Null draws nothing extra, so a renderer nobody configures draws exactly the node it always drew.

The placement is the load-bearing part. PublishPinOffsets measures each pin from a row RecordPinRow collects as that pin is submitted, and the engine uses those offsets to measure a link between the points it is actually drawn between. Running the callback after every pin row is recorded means nothing a host draws can move the point a link is drawn to. Moving the call above the pins makes DrawNodeBody_LeavesThePinPositionsWhereTheyWere fail, which is what that test is for.

The issue also sketches a per-pin DrawPinBody. That one is left alone deliberately: whether an input pin should offer an inline editor depends on whether it is connected, which is a question about the graph rather than about drawing, and the issue says it needs the most thought. This PR is the per-node half, which stands on its own.

Two things the doc comment and README state rather than enforce: the ID stack inside the callback is already the node's, since ImNodes pushes the node's ID as it begins it; and an exception thrown out of the callback escapes before EndNode and leaves the frame unusable, so a host that can fail should catch its own failures.

Tests

Three in NodeRenderingTests, which drives real frames through ImGuiAppHarness:

  • DrawNodeBody_ReachesEveryNodeAndDrawsWithoutError — called once per node, a pinless node included, with ImGui.GetCurrentContext().ErrorCountCurrentFrame at 0
  • DrawNodeBody_ContentIsSizedIntoTheNodeAndSettles — the node grows to hold the body and then holds still, rather than growing every frame
  • DrawNodeBody_LeavesThePinPositionsWhereTheyWere — the published pin screen positions do not move when a body is drawn

Verified by reverting rather than assuming: removing the call site fails the first two (Expected:<3>. Actual:<0>, and the node not widening); moving the call above the pins fails the third ("A body drawn under the pins moved an output pin").

tests/ImGui.NodeEditor.Tests 101/101 pass on this branch, on .NET 10.0.401 / Linux.

Also adds a NodeEditorRenderer README row and a short usage section.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w


Generated by Claude Code

RenderNode drew a fixed node body — a title bar, one line per input pin,
one right-aligned line per output pin — and offered no way in. A host
wanting a value editable on the node face had to either fork the renderer,
giving up zoom, hover highlighting, the measured position and dimension
feedback and the pin offset publishing, or drive ImNodes directly and keep
the engine for topology alone. Both cost the same things.

DrawNodeBody is called inside each node, after its pins and before
EndNode. ImNodes draws and sizes whatever it submits, which is the usual
idiom for putting a widget on a node.

It runs after the pins deliberately: PublishPinOffsets measures from rows
RecordPinRow collects as each pin is submitted, so content drawn later
cannot move the point a link is drawn to. Moving the call above the pins
makes the new pin-position test fail, which is the guard on that.

Fixes #441

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w
SonarCloud's scan of the branch raised MSTEST0068 on the new test:
CollectionAssert.AreEquivalent where Assert.AreSequenceEqual is the
MSTest-native form. The quality gate passed regardless, but the warning
is this branch's own and the sequence form is the stronger assertion.

The renderer walks engine.Nodes, which holds them in creation order, so
the order is deterministic and worth pinning rather than discarding: an
equivalent-set assertion would have passed had the hook run the nodes in
some other order, which would itself be a change worth noticing.

101/101 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6bYWX7oFj1YTAAw9M516w
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NodeEditorRenderer has no hook for host content inside a node body

2 participants