Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zoom functionality to node editor #192

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ if(IMNODES_EXAMPLES)
else()
target_link_libraries(hello X11 Xext GL)
endif()
endif()
endif()
240 changes: 191 additions & 49 deletions imnodes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,7 @@ ImVec2 GetScreenSpacePinCoordinates(const ImNodesEditorContext& editor, const Im

bool MouseInCanvas()
{
// This flag should be true either when hovering or clicking something in the canvas.
const bool is_window_hovered_or_focused = ImGui::IsWindowHovered() || ImGui::IsWindowFocused();

return is_window_hovered_or_focused &&
GImNodes->CanvasRectScreenSpace.Contains(ImGui::GetMousePos());
return GImNodes->IsHovered;
}

void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx)
Expand Down Expand Up @@ -1049,7 +1045,7 @@ void ClickInteractionUpdate(ImNodesEditorContext& editor)
cubic_bezier.P2,
cubic_bezier.P3,
GImNodes->Style.Colors[ImNodesCol_Link],
GImNodes->Style.LinkThickness,
GImNodes->Style.LinkThickness / editor.ZoomScale,
cubic_bezier.NumSegments);

const bool link_creation_on_snap =
Expand Down Expand Up @@ -1348,6 +1344,44 @@ void DrawGrid(ImNodesEditorContext& editor, const ImVec2& canvas_size)
}
}

inline void AppendDrawData(ImDrawList* src, ImVec2 origin, float scale)
{
ImDrawList* dl = ImGui::GetWindowDrawList();
const int vtx_start = dl->VtxBuffer.size();
const int idx_start = dl->IdxBuffer.size();
dl->VtxBuffer.resize(dl->VtxBuffer.size() + src->VtxBuffer.size());
dl->IdxBuffer.resize(dl->IdxBuffer.size() + src->IdxBuffer.size());
dl->CmdBuffer.reserve(dl->CmdBuffer.size() + src->CmdBuffer.size());
dl->_VtxWritePtr = dl->VtxBuffer.Data + vtx_start;
dl->_IdxWritePtr = dl->IdxBuffer.Data + idx_start;
const ImDrawVert* vtx_read = src->VtxBuffer.Data;
const ImDrawIdx* idx_read = src->IdxBuffer.Data;
for (int i = 0, c = src->VtxBuffer.size(); i < c; ++i)
{
dl->_VtxWritePtr[i].uv = vtx_read[i].uv;
dl->_VtxWritePtr[i].col = vtx_read[i].col;
dl->_VtxWritePtr[i].pos = vtx_read[i].pos * scale + origin;
}
for (int i = 0, c = src->IdxBuffer.size(); i < c; ++i)
{
dl->_IdxWritePtr[i] = idx_read[i] + (ImDrawIdx)vtx_start;
}
for (int i = 0, c = src->CmdBuffer.size(); i < c; ++i)
{
ImDrawCmd cmd = src->CmdBuffer[i];
cmd.IdxOffset += idx_start;
cmd.ClipRect.x = cmd.ClipRect.x * scale + origin.x;
cmd.ClipRect.y = cmd.ClipRect.y * scale + origin.y;
cmd.ClipRect.z = cmd.ClipRect.z * scale + origin.x;
cmd.ClipRect.w = cmd.ClipRect.w * scale + origin.y;
dl->CmdBuffer.push_back(cmd);
}

dl->_VtxCurrentIdx += src->VtxBuffer.size();
dl->_VtxWritePtr = dl->VtxBuffer.Data + dl->VtxBuffer.size();
dl->_IdxWritePtr = dl->IdxBuffer.Data + dl->IdxBuffer.size();
}

struct QuadOffsets
{
ImVec2 TopLeft, BottomLeft, BottomRight, TopRight;
Expand Down Expand Up @@ -1623,7 +1657,7 @@ void DrawLink(ImNodesEditorContext& editor, const int link_idx)
cubic_bezier.P2,
cubic_bezier.P3,
link_color,
GImNodes->Style.LinkThickness,
GImNodes->Style.LinkThickness / editor.ZoomScale,
cubic_bezier.NumSegments);
}

Expand Down Expand Up @@ -1680,6 +1714,11 @@ void EndPinAttribute()

void Initialize(ImNodesContext* context)
{
context->NodeEditorImgCtx = ImGui::CreateContext(ImGui::GetIO().Fonts);
context->NodeEditorImgCtx->IO.IniFilename = nullptr;
context->OriginalImgCtx = nullptr;

context->CanvasOriginalOrigin = ImVec2(0.0f, 0.0f);
context->CanvasOriginScreenSpace = ImVec2(0.0f, 0.0f);
context->CanvasRectScreenSpace = ImRect(ImVec2(0.f, 0.f), ImVec2(0.f, 0.f));
context->CurrentScope = ImNodesScope_None;
Expand All @@ -1696,7 +1735,11 @@ void Initialize(ImNodesContext* context)
StyleColorsDark(&context->Style);
}

void Shutdown(ImNodesContext* ctx) { EditorContextFree(ctx->DefaultEditorCtx); }
void Shutdown(ImNodesContext* ctx)
{
EditorContextFree(ctx->DefaultEditorCtx);
ImGui::DestroyContext(ctx->NodeEditorImgCtx);
}

// [SECTION] minimap

Expand All @@ -1717,8 +1760,8 @@ static inline bool IsMiniMapHovered()
static inline void CalcMiniMapLayout()
{
ImNodesEditorContext& editor = EditorContextGet();
const ImVec2 offset = GImNodes->Style.MiniMapOffset;
const ImVec2 border = GImNodes->Style.MiniMapPadding;
const ImVec2 offset = GImNodes->Style.MiniMapOffset / editor.ZoomScale;
const ImVec2 border = GImNodes->Style.MiniMapPadding / editor.ZoomScale;
const ImRect editor_rect = GImNodes->CanvasRectScreenSpace;

// Compute the size of the mini-map area
Expand Down Expand Up @@ -1814,7 +1857,7 @@ static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx)
node_rect.Min, node_rect.Max, mini_map_node_background, mini_map_node_rounding);

GImNodes->CanvasDrawList->AddRect(
node_rect.Min, node_rect.Max, mini_map_node_outline, mini_map_node_rounding);
node_rect.Min, node_rect.Max, mini_map_node_outline, mini_map_node_rounding, 0, 1 / editor.ZoomScale);
}

static void MiniMapDrawLink(ImNodesEditorContext& editor, const int link_idx)
Expand Down Expand Up @@ -1854,7 +1897,7 @@ static void MiniMapDrawLink(ImNodesEditorContext& editor, const int link_idx)
cubic_bezier.P2,
cubic_bezier.P3,
link_color,
GImNodes->Style.LinkThickness * editor.MiniMapScaling,
GImNodes->Style.LinkThickness * editor.MiniMapScaling / editor.ZoomScale,
cubic_bezier.NumSegments);
}

Expand Down Expand Up @@ -1885,7 +1928,12 @@ static void MiniMapUpdate()
mini_map_rect.Min, mini_map_rect.Max, mini_map_background);

GImNodes->CanvasDrawList->AddRect(
mini_map_rect.Min, mini_map_rect.Max, GImNodes->Style.Colors[ImNodesCol_MiniMapOutline]);
mini_map_rect.Min,
mini_map_rect.Max,
GImNodes->Style.Colors[ImNodesCol_MiniMapOutline],
0,
0,
1 / editor.ZoomScale);

// Clip draw list items to mini-map rect (after drawing background/outline)
GImNodes->CanvasDrawList->PushClipRect(
Expand Down Expand Up @@ -1915,7 +1963,8 @@ static void MiniMapUpdate()
const ImRect rect = ScreenSpaceToMiniMapSpace(editor, GImNodes->CanvasRectScreenSpace);

GImNodes->CanvasDrawList->AddRectFilled(rect.Min, rect.Max, canvas_color);
GImNodes->CanvasDrawList->AddRect(rect.Min, rect.Max, outline_color);
GImNodes->CanvasDrawList->AddRect(
rect.Min, rect.Max, outline_color, 0, 0, 1 / editor.ZoomScale);
}

// Have to pop mini-map clip rect
Expand Down Expand Up @@ -2058,6 +2107,8 @@ void EditorContextMoveToNode(const int node_id)
editor.Panning.y = -node.Origin.y;
}

ImGuiContext* GetNodeEditorImGuiContext() { return GImNodes->NodeEditorImgCtx; }

void SetImGuiContext(ImGuiContext* ctx) { ImGui::SetCurrentContext(ctx); }

ImNodesIO& GetIO() { return GImNodes->Io; }
Expand Down Expand Up @@ -2220,37 +2271,73 @@ void BeginNodeEditor()

GImNodes->ImNodesUIState = ImNodesUIState_None;

GImNodes->MousePos = ImGui::GetIO().MousePos;
GImNodes->LeftMouseClicked = ImGui::IsMouseClicked(0);
GImNodes->LeftMouseReleased = ImGui::IsMouseReleased(0);
GImNodes->LeftMouseDragging = ImGui::IsMouseDragging(0, 0.0f);
GImNodes->AltMouseClicked =
(GImNodes->Io.EmulateThreeButtonMouse.Modifier != NULL &&
*GImNodes->Io.EmulateThreeButtonMouse.Modifier && GImNodes->LeftMouseClicked) ||
ImGui::IsMouseClicked(GImNodes->Io.AltMouseButton);
GImNodes->AltMouseDragging =
(GImNodes->Io.EmulateThreeButtonMouse.Modifier != NULL && GImNodes->LeftMouseDragging &&
(*GImNodes->Io.EmulateThreeButtonMouse.Modifier)) ||
ImGui::IsMouseDragging(GImNodes->Io.AltMouseButton, 0.0f);
GImNodes->AltMouseScrollDelta = ImGui::GetIO().MouseWheel;
GImNodes->MultipleSelectModifier =
(GImNodes->Io.MultipleSelectModifier.Modifier != NULL
? *GImNodes->Io.MultipleSelectModifier.Modifier
: ImGui::GetIO().KeyCtrl);

GImNodes->ActiveAttribute = false;
GImNodes->IsHovered = false;

ImGui::BeginGroup();
{
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1.f, 1.f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, GImNodes->Style.Colors[ImNodesCol_GridBackground]);
ImGui::BeginChild(
"scrolling_region",
ImVec2(0.f, 0.f),
true,
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoScrollWithMouse);
// Setup zoom context
ImVec2 canvas_size = ImGui::GetContentRegionAvail();
GImNodes->CanvasOriginalOrigin = ImGui::GetCursorScreenPos();
GImNodes->OriginalImgCtx = ImGui::GetCurrentContext();

// Copy config settings in IO from main context, avoiding input fields
memcpy(
(void*)&GImNodes->NodeEditorImgCtx->IO,
(void*)&GImNodes->OriginalImgCtx->IO,
offsetof(ImGuiIO, SetPlatformImeDataFn) +
sizeof(GImNodes->OriginalImgCtx->IO.SetPlatformImeDataFn));

GImNodes->NodeEditorImgCtx->IO.BackendPlatformUserData = nullptr;
GImNodes->NodeEditorImgCtx->IO.BackendRendererUserData = nullptr;
GImNodes->NodeEditorImgCtx->IO.IniFilename = nullptr;
GImNodes->NodeEditorImgCtx->IO.ConfigInputTrickleEventQueue = false;
GImNodes->NodeEditorImgCtx->IO.DisplaySize = ImMax(canvas_size / editor.ZoomScale, ImVec2(0, 0));
GImNodes->NodeEditorImgCtx->Style = GImNodes->OriginalImgCtx->Style;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scaled(Zoom) context does not need viewport option, nor docking.
So, we have to disable (only if a user enabled these flags).
Otherwise assertion failur in ImGui::ErrorCheckNewFrameSanityChecks()

GImNodes->NodeEditorImgCtx->IO.ConfigFlags -= ImGuiConfigFlags_ViewportsEnable;
GImNodes->NodeEditorImgCtx->IO.ConfigFlags -= ImGuiConfigFlags_DockingEnable;

// Nav (tabbing) needs to be disabled otherwise it doubles up with the main context
// not sure how to get this working correctly
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoMove;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need ImGuiWindowFlags_NoBackground for docking feature


// Button to capture mouse events and hover test
ImGui::BeginChild("canvas_no_drag", canvas_size, 0, windowFlags);

if (ImGui::IsWindowHovered())
{
GImNodes->IsHovered = true;
}
else
{
windowFlags |= ImGuiWindowFlags_NoInputs;
GImNodes->NodeEditorImgCtx->IO.ConfigFlags |= ImGuiConfigFlags_NoMouse;
}

// Copy IO events
GImNodes->NodeEditorImgCtx->InputEventsQueue = GImNodes->OriginalImgCtx->InputEventsTrail;
for (ImGuiInputEvent& e : GImNodes->NodeEditorImgCtx->InputEventsQueue)
{
if (e.Type == ImGuiInputEventType_MousePos)
{
e.MousePos.PosX =
(e.MousePos.PosX - GImNodes->CanvasOriginalOrigin.x) / editor.ZoomScale;
e.MousePos.PosY =
(e.MousePos.PosY - GImNodes->CanvasOriginalOrigin.y) / editor.ZoomScale;
}
}

ImGui::SetCurrentContext(GImNodes->NodeEditorImgCtx);
ImGui::NewFrame();

ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
ImGui::PushStyleColor(ImGuiCol_WindowBg, GImNodes->Style.Colors[ImNodesCol_GridBackground]);
ImGui::Begin("editor_canvas", nullptr, windowFlags);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();

GImNodes->CanvasOriginScreenSpace = ImGui::GetCursorScreenPos();

// NOTE: we have to fetch the canvas draw list *after* we call
Expand All @@ -2259,16 +2346,35 @@ void BeginNodeEditor()
DrawListSet(ImGui::GetWindowDrawList());

{
const ImVec2 canvas_size = ImGui::GetWindowSize();
const ImVec2 window_size = ImGui::GetWindowSize();
GImNodes->CanvasRectScreenSpace = ImRect(
EditorSpaceToScreenSpace(ImVec2(0.f, 0.f)), EditorSpaceToScreenSpace(canvas_size));
EditorSpaceToScreenSpace(ImVec2(0.f, 0.f)), EditorSpaceToScreenSpace(window_size));

if (GImNodes->Style.Flags & ImNodesStyleFlags_GridLines)
{
DrawGrid(editor, canvas_size);
DrawGrid(editor, window_size);
}
}
}

// Cache inputs
GImNodes->MousePos = ImGui::GetIO().MousePos;
GImNodes->LeftMouseClicked = ImGui::IsMouseClicked(0);
GImNodes->LeftMouseReleased = ImGui::IsMouseReleased(0);
GImNodes->LeftMouseDragging = ImGui::IsMouseDragging(0, 0.0f);
GImNodes->AltMouseClicked =
(GImNodes->Io.EmulateThreeButtonMouse.Modifier != NULL &&
*GImNodes->Io.EmulateThreeButtonMouse.Modifier && GImNodes->LeftMouseClicked) ||
ImGui::IsMouseClicked(GImNodes->Io.AltMouseButton);
GImNodes->AltMouseDragging =
(GImNodes->Io.EmulateThreeButtonMouse.Modifier != NULL && GImNodes->LeftMouseDragging &&
(*GImNodes->Io.EmulateThreeButtonMouse.Modifier)) ||
ImGui::IsMouseDragging(GImNodes->Io.AltMouseButton, 0.0f);
GImNodes->AltMouseScrollDelta = ImGui::GetIO().MouseWheel;
GImNodes->MultipleSelectModifier =
(GImNodes->Io.MultipleSelectModifier.Modifier != NULL
? *GImNodes->Io.MultipleSelectModifier.Modifier
: ImGui::GetIO().KeyCtrl);
}

void EndNodeEditor()
Expand Down Expand Up @@ -2413,12 +2519,30 @@ void EndNodeEditor()
// Finally, merge the draw channels
GImNodes->CanvasDrawList->ChannelsMerge();

// pop style
ImGui::EndChild(); // end scrolling region
ImGui::PopStyleColor(); // pop child window background color
ImGui::PopStyleVar(); // pop window padding
ImGui::PopStyleVar(); // pop frame padding
GImNodes->OriginalImgCtx->WantTextInputNextFrame = ImMax(
GImNodes->OriginalImgCtx->WantTextInputNextFrame,
GImNodes->NodeEditorImgCtx->WantTextInputNextFrame);

if (MouseInCanvas())
{
GImNodes->OriginalImgCtx->MouseCursor = GImNodes->NodeEditorImgCtx->MouseCursor;
}

// End frame for zoom context
ImGui::End();
ImGui::Render();

ImDrawData* draw_data = ImGui::GetDrawData();

ImGui::SetCurrentContext(GImNodes->OriginalImgCtx);
GImNodes->OriginalImgCtx = nullptr;

ImGui::EndChild();
ImGui::EndGroup();

// Copy draw data over to original context
for (int i = 0; i < draw_data->CmdListsCount; ++i)
AppendDrawData(draw_data->CmdLists[i], GImNodes->CanvasOriginalOrigin, editor.ZoomScale);
}

void MiniMap(
Expand Down Expand Up @@ -2792,6 +2916,24 @@ void SnapNodeToGrid(int node_id)
node.Origin = SnapOriginToGrid(node.Origin);
}

float EditorContextGetZoom() { return EditorContextGet().ZoomScale; }

void EditorContextSetZoom(float zoom_scale, ImVec2 zoom_centering_pos)
{
IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None);

ImNodesEditorContext& editor = EditorContextGet();
const float new_zoom = ImMax(0.1f, ImMin(10.0f, zoom_scale));

zoom_centering_pos -= GImNodes->CanvasOriginalOrigin;
editor.Panning += zoom_centering_pos / new_zoom - zoom_centering_pos / editor.ZoomScale;

// Fix mouse position
GImNodes->NodeEditorImgCtx->IO.MousePos *= editor.ZoomScale / new_zoom;

editor.ZoomScale = new_zoom;
}

bool IsEditorHovered() { return MouseInCanvas(); }

bool IsNodeHovered(int* const node_id)
Expand Down
8 changes: 8 additions & 0 deletions imnodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ ImVec2 EditorContextGetPanning();
void EditorContextResetPanning(const ImVec2& pos);
void EditorContextMoveToNode(const int node_id);

// Get the separate ImGui context that ImNodes uses for zoom functionality
// this is the active context when between BeginNodeEditor and EndNodeEditor
ImGuiContext* GetNodeEditorImGuiContext();

ImNodesIO& GetIO();

// Returns the global style struct. See the struct declaration for default values.
Expand All @@ -278,6 +282,10 @@ void MiniMap(
const ImNodesMiniMapNodeHoveringCallback node_hovering_callback = NULL,
const ImNodesMiniMapNodeHoveringCallbackUserData node_hovering_callback_data = NULL);

// Editor zoom controls
float EditorContextGetZoom();
void EditorContextSetZoom(float zoom_scale, ImVec2 zoom_center);

// Use PushColorStyle and PopColorStyle to modify ImNodesStyle::Colors mid-frame.
void PushColorStyle(ImNodesCol item, unsigned int color);
void PopColorStyle();
Expand Down
Loading
Loading