Alex Davies
Alex Davies

Reputation: 57

Strange rendering issues with manual DrawList modification in ImGUI

I'm attempting to create a simple node canvas system using ImGUI.NET loosely based on https://github.com/thedmd/imgui-node-editor/blob/master/imgui_canvas.cpp. I believe that this question is still applicable to general ImGUI though, as it's essentially just a wrapper around the C++ library.

This works by grabbing all of the commands and vertices added by ImGUI in between calls to Canvas.Begin() and Canvas.End() and transforming them from the local space of the canvas into screen space. This essentially means that within the block between Canvas.Begin() and Canvas.End() , all screen positions are effectively positions within the canvas.

The issue is that for some reason, if I set the elements to render at (0, 0), they refuse to render unless the parent window's y coordinate is negative, and there's also some strange masking behaviour going on as well.

I'm demonstrating it in this video: https://files.catbox.moe/uxex96.mp4

Please find my code below:

private Vector2 _widgetPos;
private Vector2 _widgetSize;

private ImDrawListPtr _drawList;

private Vector2 _offset;
private float _scale = 1f;

private int _vtxBufferStart;
private int _cmdBufferStart;

private const float GridStep = 64f;

public void Begin()
{
    _widgetPos = ImGui.GetCursorScreenPos();
    _widgetSize = ImGui.GetContentRegionAvail();
    
    _drawList = ImGui.GetWindowDrawList();

    // Draw Grid (Grid jumps a bit when scaling - TODO: fix this)
    var localMin = WidgetToLocal(Vector2.Zero);
    var localMax = WidgetToLocal(_widgetSize);
    var gridColour = ImGui.ColorConvertFloat4ToU32(new Vector4(220, 220, 220, 50) / 255);
    for (float x = localMin.X % GridStep; x < localMax.X - localMin.X; x += GridStep)
        _drawList.AddLine(LocalToScreen(new Vector2(localMax.X - x, localMin.Y)),
            LocalToScreen(new Vector2(localMax.X - x, localMax.Y)), gridColour);
    for (float y = localMin.Y % GridStep; y < localMax.Y - localMin.Y; y += GridStep)
        _drawList.AddLine(LocalToScreen(new Vector2(localMin.X, localMax.Y - y)),
            LocalToScreen(new Vector2(localMax.X, localMax.Y - y)), gridColour);

    // Clip to control
    _drawList.PushClipRect(ScreenToLocal(_widgetPos), ScreenToLocal(_widgetPos + _widgetSize), false);

    // Any UI drawn past this point will be in canvas space
    _vtxBufferStart = _drawList.VtxBuffer.Size;
    _cmdBufferStart = _drawList.CmdBuffer.Size;
    
    // Start Drawing from (0, 0) in the canvas
    ImGui.SetCursorScreenPos(Vector2.Zero);
}

public Vector2 ScreenToLocal(Vector2 screen) => WidgetToLocal(screen - _widgetPos);
public Vector2 LocalToScreen(Vector2 local) => LocalToWidget(local) + _widgetPos;

public Vector2 LocalToWidget(Vector2 local) => (local + _offset) * _scale;
public Vector2 WidgetToLocal(Vector2 widget) => widget / _scale - _offset;

public void End()
{
    // Any UI drawn past this point is in screen space
    var vtxBufferEnd = _drawList.VtxBuffer.Size;
    var cmdBufferEnd = _drawList.CmdBuffer.Size;
    
    for (int idx = _vtxBufferStart; idx < vtxBufferEnd; idx++) // Update vertices
    {
        var vtx = _drawList.VtxBuffer[idx];
        vtx.pos = LocalToScreen(vtx.pos);
    }

    for (int idx = _cmdBufferStart; idx < cmdBufferEnd; idx++) // Update clipping
    {
        var cmd = _drawList.CmdBuffer[idx];
        var (min, max) = Util.SplitVector4(cmd.ClipRect);
        cmd.ClipRect = Util.MergeVector2s(LocalToScreen(min), LocalToScreen(max));
    }
    
    _drawList.PopClipRect(); // We are done with clipping now

    // Zooming
    var io = ImGui.GetIO();
    _scale += io.MouseWheel * _scale * 0.1f;
    
    // Draw Invisible Button to capture click and focus events
    ImGui.SetCursorScreenPos(_widgetPos);
    ImGui.InvisibleButton("Canvas", _widgetSize);
    bool isHovered = ImGui.IsItemHovered();
    bool isClicked = ImGui.IsItemActive();
    if (isClicked)
    {
        _offset += WidgetToLocal(io.MousePos) - WidgetToLocal(io.MousePosPrev);
    }
}

The canvas is then used as follows:

ImGui.Begin("StoryGraph", ref _open);

_canvas.Begin();

ImGui.Button("Example Button!");

_canvas.End();

ImGui.End();

I'm fairly sure the issue isn't with my clipping rect's bounds. I've tried the following to diagnose that and none of them have worked:

None of them have made a difference to the main issue of elements just disappearing. Drawing the button in the position it would ordinarily appear (ImGui.SetCursorScreenPos(_widgetPos) instead of ImGui.SetCursorScreenPos(Vector2.Zero)) makes the button appear, but then the positioning is incorrect, as the canvas transformation is then applied on top of the already screen-space position.

I would also be happy to take an alternative solution for a canvas in ImGUI, provided I can zoom and pan around an infinite canvas that I can draw normal ImGUI elements on to (with capability for mouse events).

Upvotes: 2

Views: 176

Answers (0)

Related Questions