Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This is the **ktsu ImGui Suite**, a collection of .NET libraries for building De
- **ImGui.Color** (`ktsu.ImGui.Color`) - Bridge between `ktsu.Semantics.Color` and ImGui. Colors are held as the semantic `Color` (linear) and `Srgb` types and converted only at the ImGui seam: `ColorImGuiExtensions` (`ToImColor`/`FromImColor`, `ToImGuiVector4`, `ToImGuiU32`) and `SrgbImGuiExtensions` (`Srgb` → `ImColor`/`ImGuiVector4`/`ImU32`, packed directly with no linear round-trip). The `ImColor` and `Srgb` `ToImGuiU32` apply the global style alpha like `ImGui.GetColorU32`; the linear `Color.ToImGuiU32` is a pure pack matching `ColorConvertFloat4ToU32`. `ImColor` extension operations: adjustments (lighten/darken, saturate/desaturate, hue offset, grayscale, invert, alpha), analysis (relative luminance, contrast ratio, perceptual distance), and contrast heuristics (`MostReadableTextColor`, `AdjustForSufficientContrast`). All color math delegates to `ktsu.Semantics.Color`. (There is no `ImColor` factory class — construct via `Color`/`Srgb` and convert.)
- **ImGui.Styler** (`ktsu.ImGui.Styler`) - Theming system with 50+ built-in themes, scoped styling, Button.Alignment, Text.Color semantic colors, Indent utilities, Alignment helpers, theme-aware color palette (`Palette`, e.g. `Palette.Basic.Red`, `Palette.Semantic.Error`), and interactive theme browser. Color construction and manipulation live in `ImGui.Color`.
- **NodeGraph** (`ktsu.NodeGraph`) - UI-agnostic attribute-based node graph metadata: `[Node]`, `[InputPin]`, `[OutputPin]`, `[NodeExecute]`, `[NodeBehavior]`, pin type utilities
- **ImGui.NodeEditor** (`ktsu.ImGui.NodeEditor`) - ImNodes-based visual node editor with `NodeEditorEngine`, `AttributeBasedNodeFactory`, physics-based layout, `NodeEditorRenderer`, `NodeEditorInputHandler`. `PhysicsSettingsPanel.Draw(ref PhysicsSettings)` draws every layout setting grouped by force and captioned, and `DrawDiagnostics(engine)` the live energy and settled state, so a consuming application gets the whole tuning surface rather than reimplementing a subset of it. ImNodes has no zoom of its own, so `NodeEditorRenderer.Zoom` supplies one and `FitToView` centres a graph and picks the zoom it fits at; the engine's positions and sizes stay at their own scale throughout, since that is the space the layout's lengths are measured in
- **ImGui.NodeEditor** (`ktsu.ImGui.NodeEditor`) - ImNodes-based visual node editor with `NodeEditorEngine`, `AttributeBasedNodeFactory`, physics-based layout, `NodeEditorRenderer`, `NodeEditorInputHandler`. `PhysicsSettingsPanel.Draw(ref PhysicsSettings)` draws every layout setting grouped by force and captioned, and `DrawDiagnostics(engine)` the live energy and settled state, so a consuming application gets the whole tuning surface rather than reimplementing a subset of it. ImNodes has no zoom of its own, so `NodeEditorRenderer.Zoom` supplies one and `FitToView` centres a graph and picks the zoom it fits at; the engine's positions and sizes stay at their own scale throughout, since that is the space the layout's lengths are measured in. Hovering is answered by the renderer: `HighlightLinksOnNodeHover` (on) colours the links meeting the hovered node, `HighlightDownstreamOnNodeHover` (off) also colours everything that node's value reaches, and `DrawHoveredLinkOnTop` (on) redraws the hovered link over the nodes ImNodes drew on top of it. See [Hover highlighting](#hover-highlighting) below. How many links a pin accepts is the pin's own business: `Pin.AllowsMultipleConnections` defaults to many for an output and one for an input, `[InputPin(AllowMultipleConnections = true)]` / `[OutputPin(AllowMultipleConnections = false)]` override it through the factory, and `NodeEditorEngine.SetPinAllowsMultipleConnections` sets it directly. `GetOutgoingLinks`, `GetIncomingLinks`, `GetDownstream` and `GetUpstream` walk the graph
- **ImGui.Markdown** (`ktsu.ImGui.Markdown`) - CommonMark markdown renderer built on Markdig (pipe tables, task lists, autolinks), layered on `ImGui.Color` only, with no dependency on `ImGui.App`. Static `ImGuiMarkdown.Render(string, MarkdownConfig?)` parses with an internal source-keyed cache; `MarkdownDocument` parses once for hot render paths. `MarkdownConfig` exposes `FontResolver`, `OnLinkClicked`, `ImageResolver`, `HeadingScales`, `WrapWidth`, `ListIndentPixels`, `ParagraphSpacingPixels`, and `LinkColor`. Heading sizes derive from the live font size, so DPI and `ImGuiApp.GlobalScale` are respected automatically. Bold/italic use real glyphs when the host app registers named font variants via `FontResolver`, otherwise faux styling (faux-bold double-draw, faux-italic renders upright). Fenced and indented code blocks go to `MarkdownConfig.CodeBlockRenderer` (`Action<string?, string>?` — the fence's info string and the block text) when one is supplied, which takes over drawing *and* reserving the block's layout space; `ImGui.SyntaxHighlighting` plugs into it, and neither library references the other. v1 has no built-in code-block syntax highlighting, no async remote image download, and renders HTML as escaped text.
- **SyntaxHighlighting** (`ktsu.SyntaxHighlighting`) - Renderer-agnostic tokenizing: no ImGui, no graphics API, no third-party parser, so it can move to its own repository unchanged. `SyntaxHighlighter.Highlight(code, language, tabWidth)` returns the classified `HighlightedLine`/`HighlightedToken` runs; `SyntaxHighlighter.HighlightCached` goes through a bounded cache keyed by source, language and tab width; `HighlightedCode` tokenizes once for hot render paths. Languages are data (`LanguageDefinition`: line/block comment, string, keyword, type, constant, operator, identifier and embedded-language rules) held in `LanguageRegistry`, which resolves names and aliases case-insensitively and falls back to plain text for unknown names rather than throwing. Fifteen built-ins in `BuiltInLanguages`: text, csharp, c, cpp, javascript, typescript, python, json, yaml, xml, html, css, sql, shell, lua. Two tokenizers back them — the general `CodeTokenizer`, and `MarkupTokenizer` for definitions with `IsMarkup` (XML/HTML), which classify structurally rather than by keyword. `SyntaxTheme` holds one `ktsu.Semantics.Color.Color` per `TokenKind`, with `Dark`/`Light` built in and `Background`/`Plain`/`LineNumber` left unset for the host to fill. Comments and strings are searched for an embedded language; see [Embedded languages](#embedded-languages) below. Highlighting is lexical.
- **ImGui.SyntaxHighlighting** (`ktsu.ImGui.SyntaxHighlighting`) - The Dear ImGui drawing layer over `ktsu.SyntaxHighlighting`, layered on `ImGui.Color` only, with no dependency on `ImGui.App`. Static `ImGuiSyntaxHighlighting.Render(code, language, SyntaxHighlightConfig?)` tokenizes through the shared cache and draws; `Render(HighlightedCode, config)` draws pre-tokenized code, and `HighlightedCodeExtensions` re-adds `code.Render(config)` as an extension since the tokenized type itself knows nothing about ImGui. `Highlight` forwards to `SyntaxHighlighter.Highlight`. Leaving `SyntaxHighlightConfig.Theme` null picks between `SyntaxTheme.Dark`/`Light` per frame from the window background's luminance, and unset `Background`/`Plain`/`LineNumber` come from `FrameBg`/`Text`/`TextDisabled`. Code is never wrapped, and there is no scrolling, selection or editing. `ImGui.Markdown`'s `CodeBlockRenderer` plugs into this, and neither library references the other.
Expand All @@ -53,6 +53,10 @@ This is the **ktsu ImGui Suite**, a collection of .NET libraries for building De
in memory so the decoders can be driven over their whole feature matrix without binary fixtures; the
JPEG cases, which need a real encoder, are small base64 constants in `JpegDecoderTests`.
- `tests/NodeGraph.Tests/` - Node graph attribute and type utility tests
- `tests/ImGui.NodeEditor.Tests/` - Engine, factory and rendering tests for the node editor. The
engine and factory ones need no context; `NodeRenderingTests`, `ZoomTests` and
`HoverHighlightTests` drive real frames through `ImGuiAppHarness`, since zoom and hover are made
of what the renderer writes into ImNodes and reads back out, and neither exists without drawing
- `tests/<Demo>.UITests/` - One headless UI test project per example, driving the demo's real
`BuildConfig()` through `ImGuiAppHarness`: `ImGuiAppDemo.UITests`, `ImGuiWidgetsDemo.UITests`,
`ImGuiStylerDemo.UITests`, `ImGuiPopupsDemo.UITests`, `ImGuiMarkdownDemo.UITests`,
Expand Down Expand Up @@ -405,6 +409,28 @@ The node graph system follows a clean separation of concerns:
is why `FitToView` moves the nodes. Zoom is applied on the way into ImNodes and undone on the way
back out, so nothing zoomed ever reaches the engine.

### Hover highlighting

ImNodes only answers what the pointer is over once the editor has ended, so `NodeEditorRenderer`
asks after `EndNodeEditor` and highlights on the next frame. A frame's lag on a pointer that has to
rest on a node to mean anything is not one anybody sees, and it is what lets the colours be pushed
per node and per link: ImNodes copies a link's three colours, and a node's outline colour and border
thickness, out of the style as each is submitted, so `PushColorStyle` around one submission colours
that one thing.

Two consequences worth knowing:

- **A hovered link is drawn twice.** ImNodes puts every link in a channel below the nodes and offers
no way to lift one out, so `DrawHoveredLinkOnTop` draws the same cubic bezier again — between the
same two pin positions, with control points a quarter of the straight-line distance to either
side, which is the curve ImNodes itself draws — on the **foreground draw list**, clipped to the
editor. It has to be the foreground list: the editor is a child window, and a child window's
drawing is composited over its parent's, so an overlay on the window draw list ends up underneath
the nodes. `RenderDebugOverlays` draws there for the same reason.
- **ImNodes resolves a hovered node before a hovered link.** The part of a link that passes behind a
node cannot be pointed at, only the part in the open, which is why the overlay is about following
a link you have already caught hold of rather than about grabbing one.

### Key Technical Details

- **PID frame limiter** with auto-tuning (Coarse/Fine/Precision phases)
Expand Down
35 changes: 33 additions & 2 deletions ImGui.NodeEditor/AttributeBasedNodeFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,14 @@ public Node CreateNode(Type nodeType, Vector2 position)
.OrderBy(p => p.Order)
.Select(p => p.DisplayName)];

return engine.CreateNode(
Node node = engine.CreateNode(
position,
definition.DisplayName,
inputPinNames,
outputPinNames);

ApplyConnectionCapacities(definition, node);
return node;
}

/// <summary>
Expand All @@ -154,11 +157,39 @@ public Node CreateMethodNode(MethodInfo method, Vector2 position)
.OrderBy(p => p.Order)
.Select(p => p.DisplayName)];

return engine.CreateNode(
Node node = engine.CreateNode(
position,
definition.DisplayName,
inputPinNames,
outputPinNames);

ApplyConnectionCapacities(definition, node);
return node;
}

/// <summary>
/// Carry each declared pin's connection capacity onto the pin the engine just created.
/// </summary>
/// <param name="definition">The definition the node was created from.</param>
/// <param name="node">The created node.</param>
/// <remarks>
/// The engine creates pins from names alone, so without this an
/// <c>[OutputPin(AllowMultipleConnections = false)]</c> would be declared and then ignored. The
/// pins are matched by the order they were created in, which is the order the names were passed
/// in.
/// </remarks>
private void ApplyConnectionCapacities(NodeDefinition definition, Node node)
{
ApplyConnectionCapacities([.. definition.InputPins.OrderBy(p => p.Order)], node.InputPins);
ApplyConnectionCapacities([.. definition.OutputPins.OrderBy(p => p.Order)], node.OutputPins);
}

private void ApplyConnectionCapacities(List<PinDefinition> definitions, List<Pin> pins)
{
for (int i = 0; i < definitions.Count && i < pins.Count; i++)
{
engine.SetPinAllowsMultipleConnections(pins[i].Id, definitions[i].AllowMultipleConnections);
}
}

/// <summary>
Expand Down
27 changes: 26 additions & 1 deletion ImGui.NodeEditor/DomainModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,42 @@ int InputPinId
/// <summary>
/// Represents a pin on a node
/// </summary>
/// <param name="Id">The pin's identifier, unique across every pin in the graph.</param>
/// <param name="Direction">Whether the pin takes a connection in or sends one out.</param>
/// <param name="Name">The pin's name.</param>
/// <param name="DisplayName">What to show instead of <paramref name="Name"/>, when they differ.</param>
/// <param name="AllowMultipleConnections">
/// How many links may meet this pin, or null to take the default for its direction. See
/// <see cref="AllowsMultipleConnections"/>.
/// </param>
public record Pin(
int Id,
PinDirection Direction,
string Name,
string? DisplayName = null
string? DisplayName = null,
bool? AllowMultipleConnections = null
)
{
/// <summary>
/// Gets the name to display in the UI, preferring DisplayName over Name
/// </summary>
public string EffectiveDisplayName => DisplayName ?? Name;

/// <summary>
/// Whether more than one link may meet this pin.
/// </summary>
/// <remarks>
/// An output pin fans out by default: one value can feed as many consumers as want it, and
/// refusing the second link would mean inserting a node whose only job is to duplicate the
/// first. An input pin takes one link by default, because a pin fed from two places has no
/// answer to which value it holds.
/// <para>
/// Either default is overridden by giving <see cref="AllowMultipleConnections"/> a value, which
/// is how a declared <c>[InputPin(AllowMultipleConnections = true)]</c> or
/// <c>[OutputPin(AllowMultipleConnections = false)]</c> reaches the graph.
/// </para>
/// </remarks>
public bool AllowsMultipleConnections => AllowMultipleConnections ?? (Direction == PinDirection.Output);
};

/// <summary>
Expand Down
Loading