diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 7fb9dcc..53da87f 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -6,7 +6,8 @@ "${workspaceFolder}/**", "${workspaceFolder}/Inc", "${workspaceFolder}/Lib/imgui-1.92.4-docking", - "${workspaceFolder}/Lib/imgui-1.92.4-docking/backends" + "${workspaceFolder}/Lib/imgui-1.92.4-docking/backends", + "${workspaceFolder}/Lib/imnodes-master-b2ec254" ], "defines": [ "UNICODE", diff --git a/Inc/UI_Layout.hpp b/Inc/UI_Layout.hpp index e6738f1..df31bb8 100644 --- a/Inc/UI_Layout.hpp +++ b/Inc/UI_Layout.hpp @@ -2,6 +2,7 @@ #define __UI_LAYOUT__ #include "imgui.h" +#include "imnodes.h" // UI布局 void UI_Layout(); diff --git a/Lib/imnodes-master-b2ec254/.clang-format b/Lib/imnodes-master-b2ec254/.clang-format new file mode 100644 index 0000000..eec9c23 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/.clang-format @@ -0,0 +1,39 @@ +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: 'AlwaysBreak' +AlignConsecutiveDeclarations: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AllowShortFunctionsOnASingleLine: 'true' +AlwaysBreakBeforeMultilineStrings: 'true' +AlwaysBreakTemplateDeclarations: 'true' +BinPackArguments: 'false' +BinPackParameters: 'false' +BraceWrapping: { + AfterCaseLabel: 'true' + AfterClass: 'true' + AfterControlStatement: 'true' + AfterEnum: 'true' + AfterFunction: 'true' + AfterNamespace: 'true' + AfterStruct: 'true' + AfterUnion: 'true' + BeforeCatch: 'true' + BeforeElse: 'true' + IndentBraces: 'false' +} +BreakBeforeBraces: Custom +ColumnLimit: 100 +Cpp11BracedListStyle: 'true' +DerivePointerAlignment: 'false' +IndentCaseLabels: 'false' +IndentWidth: 4 +PenaltyExcessCharacter: 100000000 +PenaltyReturnTypeOnItsOwnLine: 100000000 +PointerAlignment: Left +PointerBindsToType: 'true' +SortIncludes: 'false' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeParens: ControlStatements +TabWidth: 8 +UseTab: Never +... diff --git a/Lib/imnodes-master-b2ec254/.clang-tidy b/Lib/imnodes-master-b2ec254/.clang-tidy new file mode 100644 index 0000000..1762ff7 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/.clang-tidy @@ -0,0 +1,33 @@ +# Apply this style by doing +# +# clang-tidy -fix-errors -config= +# +# Running the command without -fix-errors will generate warnings about each +# style violation but won't change them. + +Checks: '-*,readability-identifier-naming' +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.MemberCase + value: CamelCase + - key: readability-identifier-naming.MethodCase + value: CamelCase + - key: readability-identifier-naming.NamespaceCase + value: CamelCase + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberCase + value: CamelCase + - key: readability-identifier-naming.PrivateMemberPrefix + value: '_' + - key: readability-identifier-naming.StaticConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case diff --git a/Lib/imnodes-master-b2ec254/.github/workflows/build.yaml b/Lib/imnodes-master-b2ec254/.github/workflows/build.yaml new file mode 100644 index 0000000..794e88e --- /dev/null +++ b/Lib/imnodes-master-b2ec254/.github/workflows/build.yaml @@ -0,0 +1,24 @@ +name: Build +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + platform: [macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: 'recursive' + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: ${{ github.workspace}}/vcpkg + runVcpkgInstall: true + - name: CMake generate + run: cmake -B build-release/ -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace}}/vcpkg/scripts/buildsystems/vcpkg.cmake + - name: CMake build + run: cmake --build build-release/ -j diff --git a/Lib/imnodes-master-b2ec254/.gitignore b/Lib/imnodes-master-b2ec254/.gitignore new file mode 100644 index 0000000..c4778ad --- /dev/null +++ b/Lib/imnodes-master-b2ec254/.gitignore @@ -0,0 +1,529 @@ +### Local build ### +build*/ + +### ImGui ### +**.ini + +# Created by https://www.toptal.com/developers/gitignore/api/sublimetext +# Edit at https://www.toptal.com/developers/gitignore?templates=sublimetext + +### SublimeText ### +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +*.sublime-project + +# SFTP configuration file +sftp-config.json +sftp-config-alt*.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + +# End of https://www.toptal.com/developers/gitignore/api/sublimetext + +# Created by https://www.gitignore.io/api/c,c++ + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### C++ ### +# Prerequisites + +# Compiled Object files +*.slo + +# Precompiled Headers + +# Compiled Dynamic libraries + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai + +# Executables + + +# End of https://www.gitignore.io/api/c,c++ + +# Created by https://www.gitignore.io/api/visualstudiocode + +### VisualStudioCode ### +.vscode/* + + +# End of https://www.gitignore.io/api/visualstudiocode + +# Created by https://www.gitignore.io/api/premake-gmake + +### premake-gmake ### +Makefile +*.make +obj/ + + +# End of https://www.gitignore.io/api/premake-gmake + + +# Created by https://www.gitignore.io/api/visualstudio +# Edit at https://www.gitignore.io/?templates=visualstudio + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# End of https://www.gitignore.io/api/visualstudio + +# Created by https://www.toptal.com/developers/gitignore/api/vcpkg +# Edit at https://www.toptal.com/developers/gitignore?templates=vcpkg + +### vcpkg ### +# Vcpkg + +## Manifest Mode +vcpkg_installed/ + +# End of https://www.toptal.com/developers/gitignore/api/vcpkg + + +# Created by https://www.toptal.com/developers/gitignore/api/cmake +# Edit at https://www.toptal.com/developers/gitignore?templates=cmake + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +# External projects +*-prefix/ + +# End of https://www.toptal.com/developers/gitignore/api/cmake diff --git a/Lib/imnodes-master-b2ec254/.gitmodules b/Lib/imnodes-master-b2ec254/.gitmodules new file mode 100644 index 0000000..a0a57f3 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vcpkg"] + path = vcpkg + url = https://github.com/microsoft/vcpkg.git diff --git a/Lib/imnodes-master-b2ec254/CMakeLists.txt b/Lib/imnodes-master-b2ec254/CMakeLists.txt new file mode 100644 index 0000000..c4c8d50 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/CMakeLists.txt @@ -0,0 +1,107 @@ +cmake_minimum_required(VERSION 3.15) + +project(imnodes) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED True) + + +# determine whether this is a standalone project or included by other projects +if (NOT DEFINED IMNODES_STANDALONE_PROJECT) + if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(IMNODES_STANDALONE_PROJECT ON) + else() + set(IMNODES_STANDALONE_PROJECT OFF) + endif () +endif() + +# cmake options +option(IMNODES_EXAMPLES "Build examples" ${IMNODES_STANDALONE_PROJECT}) + +# allow custom imgui target name since this can vary because imgui doesn't natively include a CMakeLists.txt +if(NOT DEFINED IMNODES_IMGUI_TARGET_NAME) + find_package(imgui CONFIG REQUIRED) + set(IMNODES_IMGUI_TARGET_NAME imgui::imgui) +endif() + + +if(MSVC) + add_compile_definitions(SDL_MAIN_HANDLED) + add_compile_options(/WX) + # replace existing /W to avoid warning + if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") + string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + else() + add_compile_options(/W4) + endif() +else() + add_compile_options(-Wall -Wextra -Wpedantic -Werror) +endif() + +# Imnodes + +add_library(imnodes) +target_sources(imnodes PRIVATE + imnodes.h + imnodes_internal.h + imnodes.cpp) +target_include_directories(imnodes PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(imnodes PUBLIC ${IMNODES_IMGUI_TARGET_NAME}) + +# Example projects +if(IMNODES_EXAMPLES) + + find_package(SDL2 CONFIG REQUIRED) + + add_executable(colornode + ${CMAKE_CURRENT_SOURCE_DIR}/imnodes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/color_node_editor.cpp) + target_link_libraries(colornode imnodes SDL2::SDL2) + if (APPLE) + target_link_libraries(colornode "-framework OpenGL") + elseif(MSVC) + target_link_libraries(colornode "opengl32") + else() + target_link_libraries(colornode X11 Xext GL) + endif() + + add_executable(multieditor + ${CMAKE_CURRENT_SOURCE_DIR}/imnodes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/multi_editor.cpp) + target_link_libraries(multieditor imnodes SDL2::SDL2) + if (APPLE) + target_link_libraries(multieditor "-framework OpenGL") + elseif(MSVC) + target_link_libraries(multieditor "opengl32") + else() + target_link_libraries(multieditor X11 Xext GL) + endif() + + add_executable(saveload + ${CMAKE_CURRENT_SOURCE_DIR}/imnodes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/save_load.cpp) + target_link_libraries(saveload imnodes SDL2::SDL2) + if (APPLE) + target_link_libraries(saveload "-framework OpenGL") + elseif(MSVC) + target_link_libraries(saveload "opengl32") + else() + target_link_libraries(saveload X11 Xext GL) + endif() + + add_executable(hello + ${CMAKE_CURRENT_SOURCE_DIR}/imnodes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/example/hello.cpp) + target_link_libraries(hello imnodes SDL2::SDL2) + if (APPLE) + target_link_libraries(hello "-framework OpenGL") + elseif(MSVC) + target_link_libraries(hello "opengl32") + else() + target_link_libraries(hello X11 Xext GL) + endif() +endif() diff --git a/Lib/imnodes-master-b2ec254/LICENSE.md b/Lib/imnodes-master-b2ec254/LICENSE.md new file mode 100644 index 0000000..ac8539e --- /dev/null +++ b/Lib/imnodes-master-b2ec254/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Johann Muszynski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Lib/imnodes-master-b2ec254/README.md b/Lib/imnodes-master-b2ec254/README.md new file mode 100644 index 0000000..1fa2a7d --- /dev/null +++ b/Lib/imnodes-master-b2ec254/README.md @@ -0,0 +1,264 @@ +

imnodes

+ +

A small, dependency-free node editor extension for dear imgui.

+ +

+ +

+ +[![Build Status](https://github.com/nelarius/imnodes/workflows/Build/badge.svg)](https://github.com/nelarius/imnodes/actions?workflow=Build) + +Imnodes aims to provide a simple, immediate-mode interface for creating a node editor within an ImGui window. Imnodes provides simple, customizable building blocks that a user needs to build their node editor. + +Features: + +* Create nodes, links, and pins in an immediate-mode style. The user controls all the state. +* Nest ImGui widgets inside nodes +* Simple distribution, just copy-paste `imnodes.h`, `imnodes_internal.h`, and `imnodes.cpp` into your project along side ImGui. + +## Examples + +This repository includes a few example files, under `example/`. They are intended as simple examples giving you an idea of what you can build with imnodes. + +If you need to build the examples, you can use the provided CMake script to do so. + +```bash +# Initialize the vcpkg submodule +$ git submodule update --init +# Run the generation step and build +$ cmake -B build-release/ -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake +$ cmake --build build-release -- -j +``` + +Note that this has not been tested on Linux and is likely to fail on the platform. + +## A brief tour + +Here is a small overview of how the extension is used. For more information on example usage, scroll to the bottom of the README. + +Before anything can be done, the library must be initialized. This can be done at the same time as `dear imgui` initialization. + +```cpp +ImGui::CreateContext(); +ImNodes::CreateContext(); + +// elsewhere in the code... +ImNodes::DestroyContext(); +ImGui::DestroyContext(); +``` + +The node editor is a workspace which contains nodes. The node editor must be instantiated within a window, like any other UI element. + +```cpp +ImGui::Begin("node editor"); + +ImNodes::BeginNodeEditor(); +ImNodes::EndNodeEditor(); + +ImGui::End(); +``` + +Now you should have a workspace with a grid visible in the window. An empty node can now be instantiated: + +```cpp +const int hardcoded_node_id = 1; + +ImNodes::BeginNodeEditor(); + +ImNodes::BeginNode(hardcoded_node_id); +ImGui::Dummy(ImVec2(80.0f, 45.0f)); +ImNodes::EndNode(); + +ImNodes::EndNodeEditor(); +``` + +Nodes, like windows in `dear imgui` must be uniquely identified. But we can't use the node titles for identification, because it should be possible to have many nodes of the same name in the workspace. Instead, you just use integers for identification. + +Attributes are the UI content of the node. An attribute will have a pin (the little circle) on either side of the node. There are two types of attributes: input, and output attributes. Input attribute pins are on the left side of the node, and output attribute pins are on the right. Like nodes, pins must be uniquely identified. + +```cpp +ImNodes::BeginNode(hardcoded_node_id); + +const int output_attr_id = 2; +ImNodes::BeginOutputAttribute(output_attr_id); +// in between Begin|EndAttribute calls, you can call ImGui +// UI functions +ImGui::Text("output pin"); +ImNodes::EndOutputAttribute(); + +ImNodes::EndNode(); +``` + +The extension doesn't really care what is in the attribute. It just renders the pin for the attribute, and allows the user to create links between pins. + +A title bar can be added to the node using `BeginNodeTitleBar` and `EndNodeTitleBar`. Like attributes, you place your title bar's content between the function calls. Note that these functions have to be called before adding attributes or other `dear imgui` UI elements to the node, since the node's layout is built in order, top-to-bottom. + +```cpp +ImNodes::BeginNode(hardcoded_node_id); + +ImNodes::BeginNodeTitleBar(); +ImGui::TextUnformatted("output node"); +ImNodes::EndNodeTitleBar(); + +// pins and other node UI content omitted... + +ImNodes::EndNode(); +``` + +The user has to render their own links between nodes as well. A link is a curve which connects two attributes. A link is just a pair of attribute ids. And like nodes and attributes, links too have to be identified by unique integer values: + +```cpp +std::vector> links; +// elsewhere in the code... +for (int i = 0; i < links.size(); ++i) +{ + const std::pair p = links[i]; + // in this case, we just use the array index of the link + // as the unique identifier + ImNodes::Link(i, p.first, p.second); +} +``` + +After `EndNodeEditor` has been called, you can check if a link was created during the frame with the function call `IsLinkCreated`: + +```cpp +int start_attr, end_attr; +if (ImNodes::IsLinkCreated(&start_attr, &end_attr)) +{ + links.push_back(std::make_pair(start_attr, end_attr)); +} +``` + +In addition to checking for new links, you can also check whether UI elements are being hovered over by the mouse cursor: + +```cpp +int node_id; +if (ImNodes::IsNodeHovered(&node_id)) +{ + node_hovered = node_id; +} +``` + +You can also check to see if any node has been selected. Nodes can be clicked on, or they can be selected by clicking and dragging the box selector over them. + +```cpp +// Note that since many nodes can be selected at once, we first need to query the number of +// selected nodes before getting them. +const int num_selected_nodes = ImNodes::NumSelectedNodes(); +if (num_selected_nodes > 0) +{ + std::vector selected_nodes; + selected_nodes.resize(num_selected_nodes); + ImNodes::GetSelectedNodes(selected_nodes.data()); +} +``` + +See `imnodes.h` for more UI event-related functions. + +Like `dear imgui`, the style of the UI can be changed. You can set the color style of individual nodes, pins, and links mid-frame by calling `ImNodes::PushColorStyle` and `ImNodes::PopColorStyle`. + +```cpp +// set the titlebar color of an individual node +ImNodes::PushColorStyle( + ImNodesCol_TitleBar, IM_COL32(11, 109, 191, 255)); +ImNodes::PushColorStyle( + ImNodesCol_TitleBarSelected, IM_COL32(81, 148, 204, 255)); + +ImNodes::BeginNode(hardcoded_node_id); +// node internals here... +ImNodes::EndNode(); + +ImNodes::PopColorStyle(); +ImNodes::PopColorStyle(); +``` + +If the style is not being set mid-frame, `ImNodes::GetStyle` can be called instead, and the values can be set into the style array directly. + +```cpp +// set the titlebar color for all nodes +ImNodesStyle& style = ImNodes::GetStyle(); +style.colors[ImNodesCol_TitleBar] = IM_COL32(232, 27, 86, 255); +style.colors[ImNodesCol_TitleBarSelected] = IM_COL32(241, 108, 146, 255); +``` + +To handle quicker navigation of large graphs you can use an interactive mini-map overlay. The mini-map can be zoomed and scrolled. Editor nodes will track the panning of the mini-map accordingly. + +```cpp +ImGui::Begin("node editor"); + +ImNodes::BeginNodeEditor(); + +// add nodes... + +// must be called right before EndNodeEditor +ImNodes::MiniMap(); +ImNodes::EndNodeEditor(); + +ImGui::End(); +``` + +The relative sizing and corner location of the mini-map in the editor space can be specified like so: + +```cpp +// MiniMap is a square region with a side length that is 20% the largest editor canvas dimension +// See ImNodesMiniMapLocation_ for other corner locations +ImNodes::MiniMap(0.2f, ImNodesMiniMapLocation_TopRight); +``` + +The mini-map also supports limited node hovering customization through a user-defined callback. +```cpp +// User callback +void mini_map_node_hovering_callback(int node_id, void* user_data) +{ + ImGui::SetTooltip("This is node %d", node_id); +} + +// Later on... +ImNodes::MiniMap(0.2f, ImNodesMiniMapLocation_TopRight, mini_map_node_hovering_callback, custom_user_data); + +// 'custom_user_data' can be used to supply extra information needed for drawing within the callback +``` + +## Customizing ImNodes + +ImNodes can be customized by providing an `imnodes_config.h` header and specifying defining `IMNODES_USER_CONFIG=imnodes_config.h` when compiling. + +It is currently possible to override the type of the minimap hovering callback function. This is useful when generating bindings for another language. + +Here's an example imnodes_config.h, which generates a pybind wrapper for the callback. +```cpp +#pragma once + +#include + +namespace pybind11 { + +inline bool PyWrapper_Check(PyObject *o) { return true; } + +class wrapper : public object { +public: + PYBIND11_OBJECT_DEFAULT(wrapper, object, PyWrapper_Check) + wrapper(void* x) { m_ptr = (PyObject*)x; } + explicit operator bool() const { return m_ptr != nullptr && m_ptr != Py_None; } +}; + +} //namespace pybind11 + +namespace py = pybind11; + +#define ImNodesMiniMapNodeHoveringCallback py::wrapper + +#define ImNodesMiniMapNodeHoveringCallbackUserData py::wrapper +``` + +## Known issues + +* `ImGui::Separator()` spans the current window span. As a result, using a separator inside a node will result in the separator spilling out of the node into the node editor grid. + +## Further information + +See the `examples/` directory to see library usage in greater detail. + +* simple.cpp is a simple hello-world style program which displays two nodes +* save_load.cpp is enables you to add and remove nodes and links, and serializes/deserializes them, so that the program state is retained between restarting the program +* color_node_editor.cpp is a more complete example, which shows how a simple node editor is implemented with a graph. diff --git a/Lib/imnodes-master-b2ec254/example/color_node_editor.cpp b/Lib/imnodes-master-b2ec254/example/color_node_editor.cpp new file mode 100644 index 0000000..549bd81 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/color_node_editor.cpp @@ -0,0 +1,715 @@ +#include "node_editor.h" +#include "graph.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace example +{ +namespace +{ +enum class NodeType +{ + add, + multiply, + output, + sine, + time, + value +}; + +struct Node +{ + NodeType type; + float value; + + explicit Node(const NodeType t) : type(t), value(0.f) {} + + Node(const NodeType t, const float v) : type(t), value(v) {} +}; + +template +T clamp(T x, T a, T b) +{ + return std::min(b, std::max(x, a)); +} + +static float current_time_seconds = 0.f; +static bool emulate_three_button_mouse = false; + +ImU32 evaluate(const Graph& graph, const int root_node) +{ + std::stack postorder; + dfs_traverse( + graph, root_node, [&postorder](const int node_id) -> void { postorder.push(node_id); }); + + std::stack value_stack; + while (!postorder.empty()) + { + const int id = postorder.top(); + postorder.pop(); + const Node node = graph.node(id); + + switch (node.type) + { + case NodeType::add: + { + const float rhs = value_stack.top(); + value_stack.pop(); + const float lhs = value_stack.top(); + value_stack.pop(); + value_stack.push(lhs + rhs); + } + break; + case NodeType::multiply: + { + const float rhs = value_stack.top(); + value_stack.pop(); + const float lhs = value_stack.top(); + value_stack.pop(); + value_stack.push(rhs * lhs); + } + break; + case NodeType::sine: + { + const float x = value_stack.top(); + value_stack.pop(); + const float res = std::abs(std::sin(x)); + value_stack.push(res); + } + break; + case NodeType::time: + { + value_stack.push(current_time_seconds); + } + break; + case NodeType::value: + { + // If the edge does not have an edge connecting to another node, then just use the value + // at this node. It means the node's input pin has not been connected to anything and + // the value comes from the node's UI. + if (graph.num_edges_from_node(id) == 0ull) + { + value_stack.push(node.value); + } + } + break; + default: + break; + } + } + + // The final output node isn't evaluated in the loop -- instead we just pop + // the three values which should be in the stack. + assert(value_stack.size() == 3ull); + const int b = static_cast(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f); + value_stack.pop(); + const int g = static_cast(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f); + value_stack.pop(); + const int r = static_cast(255.f * clamp(value_stack.top(), 0.f, 1.f) + 0.5f); + value_stack.pop(); + + return IM_COL32(r, g, b, 255); +} + +class ColorNodeEditor +{ +public: + ColorNodeEditor() + : graph_(), nodes_(), root_node_id_(-1), + minimap_location_(ImNodesMiniMapLocation_BottomRight) + { + } + + void show() + { + // Update timer context + current_time_seconds = 0.001f * SDL_GetTicks(); + + auto flags = ImGuiWindowFlags_MenuBar; + + // The node editor window + ImGui::Begin("color node editor", NULL, flags); + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Mini-map")) + { + const char* names[] = { + "Top Left", + "Top Right", + "Bottom Left", + "Bottom Right", + }; + int locations[] = { + ImNodesMiniMapLocation_TopLeft, + ImNodesMiniMapLocation_TopRight, + ImNodesMiniMapLocation_BottomLeft, + ImNodesMiniMapLocation_BottomRight, + }; + + for (int i = 0; i < 4; i++) + { + bool selected = minimap_location_ == locations[i]; + if (ImGui::MenuItem(names[i], NULL, &selected)) + minimap_location_ = locations[i]; + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Style")) + { + if (ImGui::MenuItem("Classic")) + { + ImGui::StyleColorsClassic(); + ImNodes::StyleColorsClassic(); + } + if (ImGui::MenuItem("Dark")) + { + ImGui::StyleColorsDark(); + ImNodes::StyleColorsDark(); + } + if (ImGui::MenuItem("Light")) + { + ImGui::StyleColorsLight(); + ImNodes::StyleColorsLight(); + } + ImGui::EndMenu(); + } + + ImGui::EndMenuBar(); + } + + ImGui::TextUnformatted("Edit the color of the output color window using nodes."); + ImGui::Columns(2); + ImGui::TextUnformatted("A -- add node"); + ImGui::TextUnformatted("X -- delete selected node or link"); + ImGui::NextColumn(); + if (ImGui::Checkbox("emulate_three_button_mouse", &emulate_three_button_mouse)) + { + ImNodes::GetIO().EmulateThreeButtonMouse.Modifier = + emulate_three_button_mouse ? &ImGui::GetIO().KeyAlt : NULL; + } + ImGui::Columns(1); + + ImNodes::BeginNodeEditor(); + + // Handle new nodes + // These are driven by the user, so we place this code before rendering the nodes + { + const bool open_popup = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + ImNodes::IsEditorHovered() && ImGui::IsKeyReleased(ImGuiKey_A); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.f, 8.f)); + if (!ImGui::IsAnyItemHovered() && open_popup) + { + ImGui::OpenPopup("add node"); + } + + if (ImGui::BeginPopup("add node")) + { + const ImVec2 click_pos = ImGui::GetMousePosOnOpeningCurrentPopup(); + + if (ImGui::MenuItem("add")) + { + const Node value(NodeType::value, 0.f); + const Node op(NodeType::add); + + UiNode ui_node; + ui_node.type = UiNodeType::add; + ui_node.ui.add.lhs = graph_.insert_node(value); + ui_node.ui.add.rhs = graph_.insert_node(value); + ui_node.id = graph_.insert_node(op); + + graph_.insert_edge(ui_node.id, ui_node.ui.add.lhs); + graph_.insert_edge(ui_node.id, ui_node.ui.add.rhs); + + nodes_.push_back(ui_node); + ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos); + } + + if (ImGui::MenuItem("multiply")) + { + const Node value(NodeType::value, 0.f); + const Node op(NodeType::multiply); + + UiNode ui_node; + ui_node.type = UiNodeType::multiply; + ui_node.ui.multiply.lhs = graph_.insert_node(value); + ui_node.ui.multiply.rhs = graph_.insert_node(value); + ui_node.id = graph_.insert_node(op); + + graph_.insert_edge(ui_node.id, ui_node.ui.multiply.lhs); + graph_.insert_edge(ui_node.id, ui_node.ui.multiply.rhs); + + nodes_.push_back(ui_node); + ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos); + } + + if (ImGui::MenuItem("output") && root_node_id_ == -1) + { + const Node value(NodeType::value, 0.f); + const Node out(NodeType::output); + + UiNode ui_node; + ui_node.type = UiNodeType::output; + ui_node.ui.output.r = graph_.insert_node(value); + ui_node.ui.output.g = graph_.insert_node(value); + ui_node.ui.output.b = graph_.insert_node(value); + ui_node.id = graph_.insert_node(out); + + graph_.insert_edge(ui_node.id, ui_node.ui.output.r); + graph_.insert_edge(ui_node.id, ui_node.ui.output.g); + graph_.insert_edge(ui_node.id, ui_node.ui.output.b); + + nodes_.push_back(ui_node); + ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos); + root_node_id_ = ui_node.id; + } + + if (ImGui::MenuItem("sine")) + { + const Node value(NodeType::value, 0.f); + const Node op(NodeType::sine); + + UiNode ui_node; + ui_node.type = UiNodeType::sine; + ui_node.ui.sine.input = graph_.insert_node(value); + ui_node.id = graph_.insert_node(op); + + graph_.insert_edge(ui_node.id, ui_node.ui.sine.input); + + nodes_.push_back(ui_node); + ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos); + } + + if (ImGui::MenuItem("time")) + { + UiNode ui_node; + ui_node.type = UiNodeType::time; + ui_node.id = graph_.insert_node(Node(NodeType::time)); + + nodes_.push_back(ui_node); + ImNodes::SetNodeScreenSpacePos(ui_node.id, click_pos); + } + + ImGui::EndPopup(); + } + ImGui::PopStyleVar(); + } + + for (const UiNode& node : nodes_) + { + switch (node.type) + { + case UiNodeType::add: + { + const float node_width = 100.f; + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("add"); + ImNodes::EndNodeTitleBar(); + { + ImNodes::BeginInputAttribute(node.ui.add.lhs); + const float label_width = ImGui::CalcTextSize("left").x; + ImGui::TextUnformatted("left"); + if (graph_.num_edges_from_node(node.ui.add.lhs) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat("##hidelabel", &graph_.node(node.ui.add.lhs).value, 0.01f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + { + ImNodes::BeginInputAttribute(node.ui.add.rhs); + const float label_width = ImGui::CalcTextSize("right").x; + ImGui::TextUnformatted("right"); + if (graph_.num_edges_from_node(node.ui.add.rhs) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat("##hidelabel", &graph_.node(node.ui.add.rhs).value, 0.01f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + ImGui::Spacing(); + + { + ImNodes::BeginOutputAttribute(node.id); + const float label_width = ImGui::CalcTextSize("result").x; + ImGui::Indent(node_width - label_width); + ImGui::TextUnformatted("result"); + ImNodes::EndOutputAttribute(); + } + + ImNodes::EndNode(); + } + break; + case UiNodeType::multiply: + { + const float node_width = 100.0f; + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("multiply"); + ImNodes::EndNodeTitleBar(); + + { + ImNodes::BeginInputAttribute(node.ui.multiply.lhs); + const float label_width = ImGui::CalcTextSize("left").x; + ImGui::TextUnformatted("left"); + if (graph_.num_edges_from_node(node.ui.multiply.lhs) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", &graph_.node(node.ui.multiply.lhs).value, 0.01f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + { + ImNodes::BeginInputAttribute(node.ui.multiply.rhs); + const float label_width = ImGui::CalcTextSize("right").x; + ImGui::TextUnformatted("right"); + if (graph_.num_edges_from_node(node.ui.multiply.rhs) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", &graph_.node(node.ui.multiply.rhs).value, 0.01f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + ImGui::Spacing(); + + { + ImNodes::BeginOutputAttribute(node.id); + const float label_width = ImGui::CalcTextSize("result").x; + ImGui::Indent(node_width - label_width); + ImGui::TextUnformatted("result"); + ImNodes::EndOutputAttribute(); + } + + ImNodes::EndNode(); + } + break; + case UiNodeType::output: + { + const float node_width = 100.0f; + ImNodes::PushColorStyle(ImNodesCol_TitleBar, IM_COL32(11, 109, 191, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarHovered, IM_COL32(45, 126, 194, 255)); + ImNodes::PushColorStyle(ImNodesCol_TitleBarSelected, IM_COL32(81, 148, 204, 255)); + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("output"); + ImNodes::EndNodeTitleBar(); + + ImGui::Dummy(ImVec2(node_width, 0.f)); + { + ImNodes::BeginInputAttribute(node.ui.output.r); + const float label_width = ImGui::CalcTextSize("r").x; + ImGui::TextUnformatted("r"); + if (graph_.num_edges_from_node(node.ui.output.r) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", &graph_.node(node.ui.output.r).value, 0.01f, 0.f, 1.0f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + ImGui::Spacing(); + + { + ImNodes::BeginInputAttribute(node.ui.output.g); + const float label_width = ImGui::CalcTextSize("g").x; + ImGui::TextUnformatted("g"); + if (graph_.num_edges_from_node(node.ui.output.g) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", &graph_.node(node.ui.output.g).value, 0.01f, 0.f, 1.f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + ImGui::Spacing(); + + { + ImNodes::BeginInputAttribute(node.ui.output.b); + const float label_width = ImGui::CalcTextSize("b").x; + ImGui::TextUnformatted("b"); + if (graph_.num_edges_from_node(node.ui.output.b) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", &graph_.node(node.ui.output.b).value, 0.01f, 0.f, 1.0f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + ImNodes::EndNode(); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + ImNodes::PopColorStyle(); + } + break; + case UiNodeType::sine: + { + const float node_width = 100.0f; + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("sine"); + ImNodes::EndNodeTitleBar(); + + { + ImNodes::BeginInputAttribute(node.ui.sine.input); + const float label_width = ImGui::CalcTextSize("number").x; + ImGui::TextUnformatted("number"); + if (graph_.num_edges_from_node(node.ui.sine.input) == 0ull) + { + ImGui::SameLine(); + ImGui::PushItemWidth(node_width - label_width); + ImGui::DragFloat( + "##hidelabel", + &graph_.node(node.ui.sine.input).value, + 0.01f, + 0.f, + 1.0f); + ImGui::PopItemWidth(); + } + ImNodes::EndInputAttribute(); + } + + ImGui::Spacing(); + + { + ImNodes::BeginOutputAttribute(node.id); + const float label_width = ImGui::CalcTextSize("output").x; + ImGui::Indent(node_width - label_width); + ImGui::TextUnformatted("output"); + ImNodes::EndOutputAttribute(); + } + + ImNodes::EndNode(); + } + break; + case UiNodeType::time: + { + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("time"); + ImNodes::EndNodeTitleBar(); + + ImNodes::BeginOutputAttribute(node.id); + ImGui::Text("output"); + ImNodes::EndOutputAttribute(); + + ImNodes::EndNode(); + } + break; + } + } + + for (const auto& edge : graph_.edges()) + { + // If edge doesn't start at value, then it's an internal edge, i.e. + // an edge which links a node's operation to its input. We don't + // want to render node internals with visible links. + if (graph_.node(edge.from).type != NodeType::value) + continue; + + ImNodes::Link(edge.id, edge.from, edge.to); + } + + ImNodes::MiniMap(0.2f, minimap_location_); + ImNodes::EndNodeEditor(); + + // Handle new links + // These are driven by Imnodes, so we place the code after EndNodeEditor(). + + { + int start_attr, end_attr; + if (ImNodes::IsLinkCreated(&start_attr, &end_attr)) + { + const NodeType start_type = graph_.node(start_attr).type; + const NodeType end_type = graph_.node(end_attr).type; + + const bool valid_link = start_type != end_type; + if (valid_link) + { + // Ensure the edge is always directed from the value to + // whatever produces the value + if (start_type != NodeType::value) + { + std::swap(start_attr, end_attr); + } + graph_.insert_edge(start_attr, end_attr); + } + } + } + + // Handle deleted links + + { + int link_id; + if (ImNodes::IsLinkDestroyed(&link_id)) + { + graph_.erase_edge(link_id); + } + } + + { + const int num_selected = ImNodes::NumSelectedLinks(); + if (num_selected > 0 && ImGui::IsKeyReleased(ImGuiKey_X)) + { + static std::vector selected_links; + selected_links.resize(static_cast(num_selected)); + ImNodes::GetSelectedLinks(selected_links.data()); + for (const int edge_id : selected_links) + { + graph_.erase_edge(edge_id); + } + } + } + + { + const int num_selected = ImNodes::NumSelectedNodes(); + if (num_selected > 0 && ImGui::IsKeyReleased(ImGuiKey_X)) + { + static std::vector selected_nodes; + selected_nodes.resize(static_cast(num_selected)); + ImNodes::GetSelectedNodes(selected_nodes.data()); + for (const int node_id : selected_nodes) + { + graph_.erase_node(node_id); + auto iter = std::find_if( + nodes_.begin(), nodes_.end(), [node_id](const UiNode& node) -> bool { + return node.id == node_id; + }); + // Erase any additional internal nodes + switch (iter->type) + { + case UiNodeType::add: + graph_.erase_node(iter->ui.add.lhs); + graph_.erase_node(iter->ui.add.rhs); + break; + case UiNodeType::multiply: + graph_.erase_node(iter->ui.multiply.lhs); + graph_.erase_node(iter->ui.multiply.rhs); + break; + case UiNodeType::output: + graph_.erase_node(iter->ui.output.r); + graph_.erase_node(iter->ui.output.g); + graph_.erase_node(iter->ui.output.b); + root_node_id_ = -1; + break; + case UiNodeType::sine: + graph_.erase_node(iter->ui.sine.input); + break; + default: + break; + } + nodes_.erase(iter); + } + } + } + + ImGui::End(); + + // The color output window + + const ImU32 color = + root_node_id_ != -1 ? evaluate(graph_, root_node_id_) : IM_COL32(255, 20, 147, 255); + ImGui::PushStyleColor(ImGuiCol_WindowBg, color); + ImGui::Begin("output color"); + ImGui::End(); + ImGui::PopStyleColor(); + } + +private: + enum class UiNodeType + { + add, + multiply, + output, + sine, + time, + }; + + struct UiNode + { + UiNodeType type; + // The identifying id of the ui node. For add, multiply, sine, and time + // this is the "operation" node id. The additional input nodes are + // stored in the structs. + int id; + + union + { + struct + { + int lhs, rhs; + } add; + + struct + { + int lhs, rhs; + } multiply; + + struct + { + int r, g, b; + } output; + + struct + { + int input; + } sine; + } ui; + }; + + Graph graph_; + std::vector nodes_; + int root_node_id_; + ImNodesMiniMapLocation minimap_location_; +}; + +static ColorNodeEditor color_editor; +} // namespace + +void NodeEditorInitialize() +{ + ImNodesIO& io = ImNodes::GetIO(); + io.LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; +} + +void NodeEditorShow() { color_editor.show(); } + +void NodeEditorShutdown() {} +} // namespace example diff --git a/Lib/imnodes-master-b2ec254/example/graph.h b/Lib/imnodes-master-b2ec254/example/graph.h new file mode 100644 index 0000000..35efdea --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/graph.h @@ -0,0 +1,357 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace example +{ +template +struct Span +{ + using iterator = ElementType*; + + template + Span(Container& c) : begin_(c.data()), end_(begin_ + c.size()) + { + } + + iterator begin() const { return begin_; } + iterator end() const { return end_; } + +private: + iterator begin_; + iterator end_; +}; + +template +class IdMap +{ +public: + using iterator = typename std::vector::iterator; + using const_iterator = typename std::vector::const_iterator; + + // Iterators + + const_iterator begin() const { return elements_.begin(); } + const_iterator end() const { return elements_.end(); } + + // Element access + + Span elements() const { return elements_; } + + // Capacity + + bool empty() const { return sorted_ids_.empty(); } + size_t size() const { return sorted_ids_.size(); } + + // Modifiers + + std::pair insert(int id, const ElementType& element); + std::pair insert(int id, ElementType&& element); + size_t erase(int id); + void clear(); + + // Lookup + + iterator find(int id); + const_iterator find(int id) const; + bool contains(int id) const; + +private: + std::vector elements_; + std::vector sorted_ids_; +}; + +template +std::pair::iterator, bool> IdMap::insert( + const int id, + const ElementType& element) +{ + auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id); + + if (lower_bound != sorted_ids_.end() && id == *lower_bound) + { + return std::make_pair( + std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)), false); + } + + auto insert_element_at = + std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)); + + sorted_ids_.insert(lower_bound, id); + return std::make_pair(elements_.insert(insert_element_at, element), true); +} + +template +std::pair::iterator, bool> IdMap::insert( + const int id, + ElementType&& element) +{ + auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id); + + if (lower_bound != sorted_ids_.end() && id == *lower_bound) + { + return std::make_pair( + std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)), false); + } + + auto insert_element_at = + std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)); + + sorted_ids_.insert(lower_bound, id); + return std::make_pair(elements_.insert(insert_element_at, std::move(element)), true); +} + +template +size_t IdMap::erase(const int id) +{ + auto lower_bound = std::lower_bound(sorted_ids_.begin(), sorted_ids_.end(), id); + + if (lower_bound == sorted_ids_.end() || id != *lower_bound) + { + return 0ull; + } + + auto erase_element_at = + std::next(elements_.begin(), std::distance(sorted_ids_.begin(), lower_bound)); + + sorted_ids_.erase(lower_bound); + elements_.erase(erase_element_at); + + return 1ull; +} + +template +void IdMap::clear() +{ + elements_.clear(); + sorted_ids_.clear(); +} + +template +typename IdMap::iterator IdMap::find(const int id) +{ + const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id); + return (lower_bound == sorted_ids_.cend() || *lower_bound != id) + ? elements_.end() + : std::next(elements_.begin(), std::distance(sorted_ids_.cbegin(), lower_bound)); +} + +template +typename IdMap::const_iterator IdMap::find(const int id) const +{ + const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id); + return (lower_bound == sorted_ids_.cend() || *lower_bound != id) + ? elements_.cend() + : std::next(elements_.cbegin(), std::distance(sorted_ids_.cbegin(), lower_bound)); +} + +template +bool IdMap::contains(const int id) const +{ + const auto lower_bound = std::lower_bound(sorted_ids_.cbegin(), sorted_ids_.cend(), id); + + if (lower_bound == sorted_ids_.cend()) + { + return false; + } + + return *lower_bound == id; +} + +// a very simple directional graph +template +class Graph +{ +public: + Graph() : current_id_(0), nodes_(), edges_from_node_(), node_neighbors_(), edges_() {} + + struct Edge + { + int id; + int from, to; + + Edge() = default; + Edge(const int id, const int f, const int t) : id(id), from(f), to(t) {} + + inline int opposite(const int n) const { return n == from ? to : from; } + inline bool contains(const int n) const { return n == from || n == to; } + }; + + // Element access + + NodeType& node(int node_id); + const NodeType& node(int node_id) const; + Span neighbors(int node_id) const; + Span edges() const; + + // Capacity + + size_t num_edges_from_node(int node_id) const; + + // Modifiers + + int insert_node(const NodeType& node); + void erase_node(int node_id); + + int insert_edge(int from, int to); + void erase_edge(int edge_id); + +private: + int current_id_; + // These contains map to the node id + IdMap nodes_; + IdMap edges_from_node_; + IdMap> node_neighbors_; + + // This container maps to the edge id + IdMap edges_; +}; + +template +NodeType& Graph::node(const int id) +{ + return const_cast(static_cast(this)->node(id)); +} + +template +const NodeType& Graph::node(const int id) const +{ + const auto iter = nodes_.find(id); + assert(iter != nodes_.end()); + return *iter; +} + +template +Span Graph::neighbors(int node_id) const +{ + const auto iter = node_neighbors_.find(node_id); + assert(iter != node_neighbors_.end()); + return *iter; +} + +template +Span::Edge> Graph::edges() const +{ + return edges_.elements(); +} + +template +size_t Graph::num_edges_from_node(const int id) const +{ + auto iter = edges_from_node_.find(id); + assert(iter != edges_from_node_.end()); + return *iter; +} + +template +int Graph::insert_node(const NodeType& node) +{ + const int id = current_id_++; + assert(!nodes_.contains(id)); + nodes_.insert(id, node); + edges_from_node_.insert(id, 0); + node_neighbors_.insert(id, std::vector()); + return id; +} + +template +void Graph::erase_node(const int id) +{ + + // first, remove any potential dangling edges + { + static std::vector edges_to_erase; + + for (const Edge& edge : edges_.elements()) + { + if (edge.contains(id)) + { + edges_to_erase.push_back(edge.id); + } + } + + for (const int edge_id : edges_to_erase) + { + erase_edge(edge_id); + } + + edges_to_erase.clear(); + } + + nodes_.erase(id); + edges_from_node_.erase(id); + node_neighbors_.erase(id); +} + +template +int Graph::insert_edge(const int from, const int to) +{ + const int id = current_id_++; + assert(!edges_.contains(id)); + assert(nodes_.contains(from)); + assert(nodes_.contains(to)); + edges_.insert(id, Edge(id, from, to)); + + // update neighbor count + assert(edges_from_node_.contains(from)); + *edges_from_node_.find(from) += 1; + // update neighbor list + assert(node_neighbors_.contains(from)); + node_neighbors_.find(from)->push_back(to); + + return id; +} + +template +void Graph::erase_edge(const int edge_id) +{ + // This is a bit lazy, we find the pointer here, but we refind it when we erase the edge based + // on id key. + assert(edges_.contains(edge_id)); + const Edge& edge = *edges_.find(edge_id); + + // update neighbor count + assert(edges_from_node_.contains(edge.from)); + int& edge_count = *edges_from_node_.find(edge.from); + assert(edge_count > 0); + edge_count -= 1; + + // update neighbor list + { + assert(node_neighbors_.contains(edge.from)); + auto neighbors = node_neighbors_.find(edge.from); + auto iter = std::find(neighbors->begin(), neighbors->end(), edge.to); + assert(iter != neighbors->end()); + neighbors->erase(iter); + } + + edges_.erase(edge_id); +} + +template +void dfs_traverse(const Graph& graph, const int start_node, Visitor visitor) +{ + std::stack stack; + + stack.push(start_node); + + while (!stack.empty()) + { + const int current_node = stack.top(); + stack.pop(); + + visitor(current_node); + + for (const int neighbor : graph.neighbors(current_node)) + { + stack.push(neighbor); + } + } +} +} // namespace example diff --git a/Lib/imnodes-master-b2ec254/example/hello.cpp b/Lib/imnodes-master-b2ec254/example/hello.cpp new file mode 100644 index 0000000..7a8619b --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/hello.cpp @@ -0,0 +1,48 @@ +#include "node_editor.h" +#include +#include + +namespace example +{ +namespace +{ +class HelloWorldNodeEditor +{ +public: + void show() + { + ImGui::Begin("simple node editor"); + + ImNodes::BeginNodeEditor(); + ImNodes::BeginNode(1); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("simple node :)"); + ImNodes::EndNodeTitleBar(); + + ImNodes::BeginInputAttribute(2); + ImGui::Text("input"); + ImNodes::EndInputAttribute(); + + ImNodes::BeginOutputAttribute(3); + ImGui::Indent(40); + ImGui::Text("output"); + ImNodes::EndOutputAttribute(); + + ImNodes::EndNode(); + ImNodes::EndNodeEditor(); + + ImGui::End(); + } +}; + +static HelloWorldNodeEditor editor; +} // namespace + +void NodeEditorInitialize() { ImNodes::SetNodeGridSpacePos(1, ImVec2(200.0f, 200.0f)); } + +void NodeEditorShow() { editor.show(); } + +void NodeEditorShutdown() {} + +} // namespace example diff --git a/Lib/imnodes-master-b2ec254/example/main.cpp b/Lib/imnodes-master-b2ec254/example/main.cpp new file mode 100644 index 0000000..fd08780 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/main.cpp @@ -0,0 +1,133 @@ +#include "node_editor.h" + +#include +#include +#include +#include +#include +#if defined(IMGUI_IMPL_OPENGL_ES2) +#include +#else +#include +#endif + +#include + +int main(int, char**) +{ + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) + { + printf("Error: %s\n", SDL_GetError()); + return -1; + } + + // Decide GL+GLSL versions +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GL ES 2.0 + GLSL 100 + const char* glsl_version = "#version 100"; + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#elif defined(__APPLE__) + // GL 3.2 Core + GLSL 150 + const char* glsl_version = "#version 150"; + SDL_GL_SetAttribute( + SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); +#else + // GL 3.0 + GLSL 130 + const char* glsl_version = "#version 130"; + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); +#endif + + // Create window with graphics context + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_WindowFlags window_flags = + (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + SDL_Window* window = SDL_CreateWindow( + "Dear ImGui SDL2+OpenGL3 example", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + 1280, + 720, + window_flags); + SDL_GLContext gl_context = SDL_GL_CreateContext(window); + SDL_GL_MakeCurrent(window, gl_context); + SDL_GL_SetSwapInterval(1); // Enable vsync + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + (void)io; + + ImNodes::CreateContext(); + example::NodeEditorInitialize(); + + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + // ImGui::StyleColorsClassic(); + ImNodes::StyleColorsDark(); + + // Setup Platform/Renderer backends + ImGui_ImplSDL2_InitForOpenGL(window, gl_context); + ImGui_ImplOpenGL3_Init(glsl_version); + + ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + + // Main loop + bool done = false; + while (!done) + { + SDL_Event event; + while (SDL_PollEvent(&event)) + { + ImGui_ImplSDL2_ProcessEvent(&event); + if (event.type == SDL_QUIT) + done = true; + if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_CLOSE && + event.window.windowID == SDL_GetWindowID(window)) + done = true; + } + + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(); + ImGui::NewFrame(); + + example::NodeEditorShow(); + + // Rendering + ImGui::Render(); + glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); + glClearColor( + clear_color.x * clear_color.w, + clear_color.y * clear_color.w, + clear_color.z * clear_color.w, + clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + SDL_GL_SwapWindow(window); + } + + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL2_Shutdown(); + example::NodeEditorShutdown(); + ImNodes::DestroyContext(); + ImGui::DestroyContext(); + + SDL_GL_DeleteContext(gl_context); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} diff --git a/Lib/imnodes-master-b2ec254/example/multi_editor.cpp b/Lib/imnodes-master-b2ec254/example/multi_editor.cpp new file mode 100644 index 0000000..cb9fb1d --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/multi_editor.cpp @@ -0,0 +1,142 @@ +#include "node_editor.h" +#include +#include +#include + +#include +#include + +namespace example +{ +namespace +{ +struct Node +{ + int id; + float value; + + Node(const int i, const float v) : id(i), value(v) {} +}; + +struct Link +{ + int id; + int start_attr, end_attr; +}; + +struct Editor +{ + ImNodesEditorContext* context = nullptr; + std::vector nodes; + std::vector links; + int current_id = 0; +}; + +void show_editor(const char* editor_name, Editor& editor) +{ + ImNodes::EditorContextSet(editor.context); + + ImGui::Begin(editor_name); + ImGui::TextUnformatted("A -- add node"); + + ImNodes::BeginNodeEditor(); + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + ImNodes::IsEditorHovered() && ImGui::IsKeyReleased(ImGuiKey_A)) + { + const int node_id = ++editor.current_id; + ImNodes::SetNodeScreenSpacePos(node_id, ImGui::GetMousePos()); + ImNodes::SnapNodeToGrid(node_id); + editor.nodes.push_back(Node(node_id, 0.f)); + } + + for (Node& node : editor.nodes) + { + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("node"); + ImNodes::EndNodeTitleBar(); + + ImNodes::BeginInputAttribute(node.id << 8); + ImGui::TextUnformatted("input"); + ImNodes::EndInputAttribute(); + + ImNodes::BeginStaticAttribute(node.id << 16); + ImGui::PushItemWidth(120.0f); + ImGui::DragFloat("value", &node.value, 0.01f); + ImGui::PopItemWidth(); + ImNodes::EndStaticAttribute(); + + ImNodes::BeginOutputAttribute(node.id << 24); + const float text_width = ImGui::CalcTextSize("output").x; + ImGui::Indent(120.f + ImGui::CalcTextSize("value").x - text_width); + ImGui::TextUnformatted("output"); + ImNodes::EndOutputAttribute(); + + ImNodes::EndNode(); + } + + for (const Link& link : editor.links) + { + ImNodes::Link(link.id, link.start_attr, link.end_attr); + } + + ImNodes::EndNodeEditor(); + + { + Link link; + if (ImNodes::IsLinkCreated(&link.start_attr, &link.end_attr)) + { + link.id = ++editor.current_id; + editor.links.push_back(link); + } + } + + { + int link_id; + if (ImNodes::IsLinkDestroyed(&link_id)) + { + auto iter = std::find_if( + editor.links.begin(), editor.links.end(), [link_id](const Link& link) -> bool { + return link.id == link_id; + }); + assert(iter != editor.links.end()); + editor.links.erase(iter); + } + } + + ImGui::End(); +} + +Editor editor1; +Editor editor2; +} // namespace + +void NodeEditorInitialize() +{ + editor1.context = ImNodes::EditorContextCreate(); + editor2.context = ImNodes::EditorContextCreate(); + ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick); + + ImNodesIO& io = ImNodes::GetIO(); + io.LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; + io.MultipleSelectModifier.Modifier = &ImGui::GetIO().KeyCtrl; + + ImNodesStyle& style = ImNodes::GetStyle(); + style.Flags |= ImNodesStyleFlags_GridLinesPrimary | ImNodesStyleFlags_GridSnapping; +} + +void NodeEditorShow() +{ + show_editor("editor1", editor1); + show_editor("editor2", editor2); +} + +void NodeEditorShutdown() +{ + ImNodes::PopAttributeFlag(); + ImNodes::EditorContextFree(editor1.context); + ImNodes::EditorContextFree(editor2.context); +} +} // namespace example diff --git a/Lib/imnodes-master-b2ec254/example/node_editor.h b/Lib/imnodes-master-b2ec254/example/node_editor.h new file mode 100644 index 0000000..80328a4 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/node_editor.h @@ -0,0 +1,8 @@ +#pragma once + +namespace example +{ +void NodeEditorInitialize(); +void NodeEditorShow(); +void NodeEditorShutdown(); +} // namespace example \ No newline at end of file diff --git a/Lib/imnodes-master-b2ec254/example/save_load.cpp b/Lib/imnodes-master-b2ec254/example/save_load.cpp new file mode 100644 index 0000000..01f3293 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/example/save_load.cpp @@ -0,0 +1,206 @@ +#include "node_editor.h" + +#include +#include +#include + +#include +#include +#include +#include // for std::streamsize +#include +#include + +namespace example +{ +namespace +{ +struct Node +{ + int id; + float value; + + Node() = default; + + Node(const int i, const float v) : id(i), value(v) {} +}; + +struct Link +{ + int id; + int start_attr, end_attr; +}; + +class SaveLoadEditor +{ +public: + SaveLoadEditor() : nodes_(), links_(), current_id_(0) {} + + void show() + { + ImGui::Begin("Save & load example"); + ImGui::TextUnformatted("A -- add node"); + ImGui::TextUnformatted( + "Close the executable and rerun it -- your nodes should be exactly " + "where you left them!"); + + ImNodes::BeginNodeEditor(); + + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + ImNodes::IsEditorHovered() && ImGui::IsKeyReleased(ImGuiKey_A)) + { + const int node_id = ++current_id_; + ImNodes::SetNodeScreenSpacePos(node_id, ImGui::GetMousePos()); + nodes_.push_back(Node(node_id, 0.f)); + } + + for (Node& node : nodes_) + { + ImNodes::BeginNode(node.id); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("node"); + ImNodes::EndNodeTitleBar(); + + ImNodes::BeginInputAttribute(node.id << 8); + ImGui::TextUnformatted("input"); + ImNodes::EndInputAttribute(); + + ImNodes::BeginStaticAttribute(node.id << 16); + ImGui::PushItemWidth(120.f); + ImGui::DragFloat("value", &node.value, 0.01f); + ImGui::PopItemWidth(); + ImNodes::EndStaticAttribute(); + + ImNodes::BeginOutputAttribute(node.id << 24); + const float text_width = ImGui::CalcTextSize("output").x; + ImGui::Indent(120.f + ImGui::CalcTextSize("value").x - text_width); + ImGui::TextUnformatted("output"); + ImNodes::EndOutputAttribute(); + + ImNodes::EndNode(); + } + + for (const Link& link : links_) + { + ImNodes::Link(link.id, link.start_attr, link.end_attr); + } + + ImNodes::EndNodeEditor(); + + { + Link link; + if (ImNodes::IsLinkCreated(&link.start_attr, &link.end_attr)) + { + link.id = ++current_id_; + links_.push_back(link); + } + } + + { + int link_id; + if (ImNodes::IsLinkDestroyed(&link_id)) + { + auto iter = + std::find_if(links_.begin(), links_.end(), [link_id](const Link& link) -> bool { + return link.id == link_id; + }); + assert(iter != links_.end()); + links_.erase(iter); + } + } + + ImGui::End(); + } + + void save() + { + // Save the internal imnodes state + ImNodes::SaveCurrentEditorStateToIniFile("save_load.ini"); + + // Dump our editor state as bytes into a file + + std::fstream fout( + "save_load.bytes", std::ios_base::out | std::ios_base::binary | std::ios_base::trunc); + + // copy the node vector to file + const size_t num_nodes = nodes_.size(); + fout.write( + reinterpret_cast(&num_nodes), + static_cast(sizeof(size_t))); + fout.write( + reinterpret_cast(nodes_.data()), + static_cast(sizeof(Node) * num_nodes)); + + // copy the link vector to file + const size_t num_links = links_.size(); + fout.write( + reinterpret_cast(&num_links), + static_cast(sizeof(size_t))); + fout.write( + reinterpret_cast(links_.data()), + static_cast(sizeof(Link) * num_links)); + + // copy the current_id to file + fout.write( + reinterpret_cast(¤t_id_), static_cast(sizeof(int))); + } + + void load() + { + // Load the internal imnodes state + ImNodes::LoadCurrentEditorStateFromIniFile("save_load.ini"); + + // Load our editor state into memory + + std::fstream fin("save_load.bytes", std::ios_base::in | std::ios_base::binary); + + if (!fin.is_open()) + { + return; + } + + // copy nodes into memory + size_t num_nodes; + fin.read(reinterpret_cast(&num_nodes), static_cast(sizeof(size_t))); + nodes_.resize(num_nodes); + fin.read( + reinterpret_cast(nodes_.data()), + static_cast(sizeof(Node) * num_nodes)); + + // copy links into memory + size_t num_links; + fin.read(reinterpret_cast(&num_links), static_cast(sizeof(size_t))); + links_.resize(num_links); + fin.read( + reinterpret_cast(links_.data()), + static_cast(sizeof(Link) * num_links)); + + // copy current_id into memory + fin.read(reinterpret_cast(¤t_id_), static_cast(sizeof(int))); + } + +private: + std::vector nodes_; + std::vector links_; + int current_id_; +}; + +static SaveLoadEditor editor; +} // namespace + +void NodeEditorInitialize() +{ + ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; + ImNodes::PushAttributeFlag(ImNodesAttributeFlags_EnableLinkDetachWithDragClick); + editor.load(); +} + +void NodeEditorShow() { editor.show(); } + +void NodeEditorShutdown() +{ + ImNodes::PopAttributeFlag(); + editor.save(); +} +} // namespace example diff --git a/Lib/imnodes-master-b2ec254/img/imnodes.gif b/Lib/imnodes-master-b2ec254/img/imnodes.gif new file mode 100644 index 0000000..6163688 Binary files /dev/null and b/Lib/imnodes-master-b2ec254/img/imnodes.gif differ diff --git a/Lib/imnodes-master-b2ec254/imnodes.cpp b/Lib/imnodes-master-b2ec254/imnodes.cpp new file mode 100644 index 0000000..e2d9e5e --- /dev/null +++ b/Lib/imnodes-master-b2ec254/imnodes.cpp @@ -0,0 +1,3265 @@ +// the structure of this file: +// +// [SECTION] bezier curve helpers +// [SECTION] draw list helper +// [SECTION] ui state logic +// [SECTION] render helpers +// [SECTION] API implementation + +#include "imnodes_internal.h" + +// Check minimum ImGui version +#define MINIMUM_COMPATIBLE_IMGUI_VERSION 17400 +#if IMGUI_VERSION_NUM < MINIMUM_COMPATIBLE_IMGUI_VERSION +#error "Minimum ImGui version requirement not met -- please use a newer version!" +#endif + +#include +#include +#include +#include +#include // for fwrite, ssprintf, sscanf +#include +#include // strlen, strncmp + +// Use secure CRT function variants to avoid MSVC compiler errors +#ifdef _MSC_VER +#define sscanf sscanf_s +#endif + +ImNodesContext* GImNodes = NULL; + +namespace IMNODES_NAMESPACE +{ +namespace +{ +// [SECTION] bezier curve helpers + +struct CubicBezier +{ + ImVec2 P0, P1, P2, P3; + int NumSegments; +}; + +inline ImVec2 EvalCubicBezier( + const float t, + const ImVec2& P0, + const ImVec2& P1, + const ImVec2& P2, + const ImVec2& P3) +{ + // B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3 + + const float u = 1.0f - t; + const float b0 = u * u * u; + const float b1 = 3 * u * u * t; + const float b2 = 3 * u * t * t; + const float b3 = t * t * t; + return ImVec2( + b0 * P0.x + b1 * P1.x + b2 * P2.x + b3 * P3.x, + b0 * P0.y + b1 * P1.y + b2 * P2.y + b3 * P3.y); +} + +// Calculates the closest point along each bezier curve segment. +ImVec2 GetClosestPointOnCubicBezier(const int num_segments, const ImVec2& p, const CubicBezier& cb) +{ + IM_ASSERT(num_segments > 0); + ImVec2 p_last = cb.P0; + ImVec2 p_closest; + float p_closest_dist = FLT_MAX; + float t_step = 1.0f / (float)num_segments; + for (int i = 1; i <= num_segments; ++i) + { + ImVec2 p_current = EvalCubicBezier(t_step * i, cb.P0, cb.P1, cb.P2, cb.P3); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist = ImLengthSqr(p - p_line); + if (dist < p_closest_dist) + { + p_closest = p_line; + p_closest_dist = dist; + } + p_last = p_current; + } + return p_closest; +} + +inline float GetDistanceToCubicBezier( + const ImVec2& pos, + const CubicBezier& cubic_bezier, + const int num_segments) +{ + const ImVec2 point_on_curve = GetClosestPointOnCubicBezier(num_segments, pos, cubic_bezier); + + const ImVec2 to_curve = point_on_curve - pos; + return ImSqrt(ImLengthSqr(to_curve)); +} + +inline ImRect GetContainingRectForCubicBezier(const CubicBezier& cb) +{ + const ImVec2 min = ImVec2(ImMin(cb.P0.x, cb.P3.x), ImMin(cb.P0.y, cb.P3.y)); + const ImVec2 max = ImVec2(ImMax(cb.P0.x, cb.P3.x), ImMax(cb.P0.y, cb.P3.y)); + + const float hover_distance = GImNodes->Style.LinkHoverDistance; + + ImRect rect(min, max); + rect.Add(cb.P1); + rect.Add(cb.P2); + rect.Expand(ImVec2(hover_distance, hover_distance)); + + return rect; +} + +inline CubicBezier GetCubicBezier( + ImVec2 start, + ImVec2 end, + const ImNodesAttributeType start_type, + const float line_segments_per_length) +{ + IM_ASSERT( + (start_type == ImNodesAttributeType_Input) || (start_type == ImNodesAttributeType_Output)); + if (start_type == ImNodesAttributeType_Input) + { + ImSwap(start, end); + } + + const float link_length = ImSqrt(ImLengthSqr(end - start)); + const ImVec2 offset = ImVec2(0.25f * link_length, 0.f); + CubicBezier cubic_bezier; + cubic_bezier.P0 = start; + cubic_bezier.P1 = start + offset; + cubic_bezier.P2 = end - offset; + cubic_bezier.P3 = end; + cubic_bezier.NumSegments = ImMax(static_cast(link_length * line_segments_per_length), 1); + return cubic_bezier; +} + +inline float EvalImplicitLineEq(const ImVec2& p1, const ImVec2& p2, const ImVec2& p) +{ + return (p2.y - p1.y) * p.x + (p1.x - p2.x) * p.y + (p2.x * p1.y - p1.x * p2.y); +} + +inline int Sign(float val) { return int(val > 0.0f) - int(val < 0.0f); } + +inline bool RectangleOverlapsLineSegment(const ImRect& rect, const ImVec2& p1, const ImVec2& p2) +{ + // Trivial case: rectangle contains an endpoint + if (rect.Contains(p1) || rect.Contains(p2)) + { + return true; + } + + // Flip rectangle if necessary + ImRect flip_rect = rect; + + if (flip_rect.Min.x > flip_rect.Max.x) + { + ImSwap(flip_rect.Min.x, flip_rect.Max.x); + } + + if (flip_rect.Min.y > flip_rect.Max.y) + { + ImSwap(flip_rect.Min.y, flip_rect.Max.y); + } + + // Trivial case: line segment lies to one particular side of rectangle + if ((p1.x < flip_rect.Min.x && p2.x < flip_rect.Min.x) || + (p1.x > flip_rect.Max.x && p2.x > flip_rect.Max.x) || + (p1.y < flip_rect.Min.y && p2.y < flip_rect.Min.y) || + (p1.y > flip_rect.Max.y && p2.y > flip_rect.Max.y)) + { + return false; + } + + const int corner_signs[4] = { + Sign(EvalImplicitLineEq(p1, p2, flip_rect.Min)), + Sign(EvalImplicitLineEq(p1, p2, ImVec2(flip_rect.Max.x, flip_rect.Min.y))), + Sign(EvalImplicitLineEq(p1, p2, ImVec2(flip_rect.Min.x, flip_rect.Max.y))), + Sign(EvalImplicitLineEq(p1, p2, flip_rect.Max))}; + + int sum = 0; + int sum_abs = 0; + + for (int i = 0; i < 4; ++i) + { + sum += corner_signs[i]; + sum_abs += abs(corner_signs[i]); + } + + // At least one corner of rectangle lies on a different side of line segment + return abs(sum) != sum_abs; +} + +inline bool RectangleOverlapsBezier(const ImRect& rectangle, const CubicBezier& cubic_bezier) +{ + ImVec2 current = + EvalCubicBezier(0.f, cubic_bezier.P0, cubic_bezier.P1, cubic_bezier.P2, cubic_bezier.P3); + const float dt = 1.0f / cubic_bezier.NumSegments; + for (int s = 0; s < cubic_bezier.NumSegments; ++s) + { + ImVec2 next = EvalCubicBezier( + static_cast((s + 1) * dt), + cubic_bezier.P0, + cubic_bezier.P1, + cubic_bezier.P2, + cubic_bezier.P3); + if (RectangleOverlapsLineSegment(rectangle, current, next)) + { + return true; + } + current = next; + } + return false; +} + +inline bool RectangleOverlapsLink( + const ImRect& rectangle, + const ImVec2& start, + const ImVec2& end, + const ImNodesAttributeType start_type) +{ + // First level: simple rejection test via rectangle overlap: + + ImRect lrect = ImRect(start, end); + if (lrect.Min.x > lrect.Max.x) + { + ImSwap(lrect.Min.x, lrect.Max.x); + } + + if (lrect.Min.y > lrect.Max.y) + { + ImSwap(lrect.Min.y, lrect.Max.y); + } + + if (rectangle.Overlaps(lrect)) + { + // First, check if either one or both endpoinds are trivially contained + // in the rectangle + + if (rectangle.Contains(start) || rectangle.Contains(end)) + { + return true; + } + + // Second level of refinement: do a more expensive test against the + // link + + const CubicBezier cubic_bezier = + GetCubicBezier(start, end, start_type, GImNodes->Style.LinkLineSegmentsPerLength); + return RectangleOverlapsBezier(rectangle, cubic_bezier); + } + + return false; +} + +// [SECTION] coordinate space conversion helpers + +inline ImVec2 ScreenSpaceToGridSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return v - GImNodes->CanvasOriginScreenSpace - editor.Panning; +} + +inline ImRect ScreenSpaceToGridSpace(const ImNodesEditorContext& editor, const ImRect& r) +{ + return ImRect(ScreenSpaceToGridSpace(editor, r.Min), ScreenSpaceToGridSpace(editor, r.Max)); +} + +inline ImVec2 GridSpaceToScreenSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return v + GImNodes->CanvasOriginScreenSpace + editor.Panning; +} + +inline ImVec2 GridSpaceToEditorSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return v + editor.Panning; +} + +inline ImVec2 EditorSpaceToGridSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return v - editor.Panning; +} + +inline ImVec2 EditorSpaceToScreenSpace(const ImVec2& v) +{ + return GImNodes->CanvasOriginScreenSpace + v; +} + +inline ImVec2 MiniMapSpaceToGridSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return (v - editor.MiniMapContentScreenSpace.Min) / editor.MiniMapScaling + + editor.GridContentBounds.Min; +} + +inline ImVec2 ScreenSpaceToMiniMapSpace(const ImNodesEditorContext& editor, const ImVec2& v) +{ + return (ScreenSpaceToGridSpace(editor, v) - editor.GridContentBounds.Min) * + editor.MiniMapScaling + + editor.MiniMapContentScreenSpace.Min; +} + +inline ImRect ScreenSpaceToMiniMapSpace(const ImNodesEditorContext& editor, const ImRect& r) +{ + return ImRect( + ScreenSpaceToMiniMapSpace(editor, r.Min), ScreenSpaceToMiniMapSpace(editor, r.Max)); +} + +// [SECTION] draw list helper + +void ImDrawListGrowChannels(ImDrawList* draw_list, const int num_channels) +{ + ImDrawListSplitter& splitter = draw_list->_Splitter; + + if (splitter._Count == 1) + { + splitter.Split(draw_list, num_channels + 1); + return; + } + + // NOTE: this logic has been lifted from ImDrawListSplitter::Split with slight modifications + // to allow nested splits. The main modification is that we only create new ImDrawChannel + // instances after splitter._Count, instead of over the whole splitter._Channels array like + // the regular ImDrawListSplitter::Split method does. + + const int old_channel_capacity = splitter._Channels.Size; + // NOTE: _Channels is not resized down, and therefore _Count <= _Channels.size()! + const int old_channel_count = splitter._Count; + const int requested_channel_count = old_channel_count + num_channels; + if (old_channel_capacity < old_channel_count + num_channels) + { + splitter._Channels.resize(requested_channel_count); + } + + splitter._Count = requested_channel_count; + + for (int i = old_channel_count; i < requested_channel_count; ++i) + { + ImDrawChannel& channel = splitter._Channels[i]; + + // If we're inside the old capacity region of the array, we need to reuse the existing + // memory of the command and index buffers. + if (i < old_channel_capacity) + { + channel._CmdBuffer.resize(0); + channel._IdxBuffer.resize(0); + } + // Else, we need to construct new draw channels. + else + { + IM_PLACEMENT_NEW(&channel) ImDrawChannel(); + } + + { + ImDrawCmd draw_cmd; + draw_cmd.ClipRect = draw_list->_ClipRectStack.back(); +#if IMGUI_VERSION_NUM < 19200 + draw_cmd.TextureId = draw_list->_TextureIdStack.back(); +#else + draw_cmd.TexRef = draw_list->_TextureStack.back(); +#endif + channel._CmdBuffer.push_back(draw_cmd); + } + } +} + +void ImDrawListSplitterSwapChannels( + ImDrawListSplitter& splitter, + const int lhs_idx, + const int rhs_idx) +{ + if (lhs_idx == rhs_idx) + { + return; + } + + IM_ASSERT(lhs_idx >= 0 && lhs_idx < splitter._Count); + IM_ASSERT(rhs_idx >= 0 && rhs_idx < splitter._Count); + + ImDrawChannel& lhs_channel = splitter._Channels[lhs_idx]; + ImDrawChannel& rhs_channel = splitter._Channels[rhs_idx]; + lhs_channel._CmdBuffer.swap(rhs_channel._CmdBuffer); + lhs_channel._IdxBuffer.swap(rhs_channel._IdxBuffer); + + const int current_channel = splitter._Current; + + if (current_channel == lhs_idx) + { + splitter._Current = rhs_idx; + } + else if (current_channel == rhs_idx) + { + splitter._Current = lhs_idx; + } +} + +void DrawListSet(ImDrawList* window_draw_list) +{ + GImNodes->CanvasDrawList = window_draw_list; + GImNodes->NodeIdxToSubmissionIdx.Clear(); + GImNodes->NodeIdxSubmissionOrder.clear(); +} + +// The draw list channels are structured as follows. First we have our base channel, the canvas grid +// on which we render the grid lines in BeginNodeEditor(). The base channel is the reason +// draw_list_submission_idx_to_background_channel_idx offsets the index by one. Each BeginNode() +// call appends two new draw channels, for the node background and foreground. The node foreground +// is the channel into which the node's ImGui content is rendered. Finally, in EndNodeEditor() we +// append one last draw channel for rendering the selection box and the incomplete link on top of +// everything else. +// +// +----------+----------+----------+----------+----------+----------+ +// | | | | | | | +// |canvas |node |node |... |... |click | +// |grid |background|foreground| | |interaction +// | | | | | | | +// +----------+----------+----------+----------+----------+----------+ +// | | +// | submission idx | +// | | +// ----------------------- + +void DrawListAddNode(const int node_idx) +{ + GImNodes->NodeIdxToSubmissionIdx.SetInt( + static_cast(node_idx), GImNodes->NodeIdxSubmissionOrder.Size); + GImNodes->NodeIdxSubmissionOrder.push_back(node_idx); + ImDrawListGrowChannels(GImNodes->CanvasDrawList, 2); +} + +void DrawListAppendClickInteractionChannel() +{ + // NOTE: don't use this function outside of EndNodeEditor. Using this before all nodes have been + // added will screw up the node draw order. + ImDrawListGrowChannels(GImNodes->CanvasDrawList, 1); +} + +int DrawListSubmissionIdxToBackgroundChannelIdx(const int submission_idx) +{ + // NOTE: the first channel is the canvas background, i.e. the grid + return 1 + 2 * submission_idx; +} + +int DrawListSubmissionIdxToForegroundChannelIdx(const int submission_idx) +{ + return DrawListSubmissionIdxToBackgroundChannelIdx(submission_idx) + 1; +} + +void DrawListActivateClickInteractionChannel() +{ + GImNodes->CanvasDrawList->_Splitter.SetCurrentChannel( + GImNodes->CanvasDrawList, GImNodes->CanvasDrawList->_Splitter._Count - 1); +} + +void DrawListActivateCurrentNodeForeground() +{ + const int foreground_channel_idx = + DrawListSubmissionIdxToForegroundChannelIdx(GImNodes->NodeIdxSubmissionOrder.Size - 1); + GImNodes->CanvasDrawList->_Splitter.SetCurrentChannel( + GImNodes->CanvasDrawList, foreground_channel_idx); +} + +void DrawListActivateNodeBackground(const int node_idx) +{ + const int submission_idx = + GImNodes->NodeIdxToSubmissionIdx.GetInt(static_cast(node_idx), -1); + // There is a discrepancy in the submitted node count and the rendered node count! Did you call + // one of the following functions + // * EditorContextMoveToNode + // * SetNodeScreenSpacePos + // * SetNodeGridSpacePos + // * SetNodeDraggable + // after the BeginNode/EndNode function calls? + IM_ASSERT(submission_idx != -1); + const int background_channel_idx = DrawListSubmissionIdxToBackgroundChannelIdx(submission_idx); + GImNodes->CanvasDrawList->_Splitter.SetCurrentChannel( + GImNodes->CanvasDrawList, background_channel_idx); +} + +void DrawListSwapSubmissionIndices(const int lhs_idx, const int rhs_idx) +{ + IM_ASSERT(lhs_idx != rhs_idx); + + const int lhs_foreground_channel_idx = DrawListSubmissionIdxToForegroundChannelIdx(lhs_idx); + const int lhs_background_channel_idx = DrawListSubmissionIdxToBackgroundChannelIdx(lhs_idx); + const int rhs_foreground_channel_idx = DrawListSubmissionIdxToForegroundChannelIdx(rhs_idx); + const int rhs_background_channel_idx = DrawListSubmissionIdxToBackgroundChannelIdx(rhs_idx); + + ImDrawListSplitterSwapChannels( + GImNodes->CanvasDrawList->_Splitter, + lhs_background_channel_idx, + rhs_background_channel_idx); + ImDrawListSplitterSwapChannels( + GImNodes->CanvasDrawList->_Splitter, + lhs_foreground_channel_idx, + rhs_foreground_channel_idx); +} + +void DrawListSortChannelsByDepth(const ImVector& node_idx_depth_order) +{ + if (GImNodes->NodeIdxToSubmissionIdx.Data.Size < 2) + { + return; + } + + IM_ASSERT(node_idx_depth_order.Size == GImNodes->NodeIdxSubmissionOrder.Size); + + int start_idx = node_idx_depth_order.Size - 1; + + while (node_idx_depth_order[start_idx] == GImNodes->NodeIdxSubmissionOrder[start_idx]) + { + if (--start_idx == 0) + { + // early out if submission order and depth order are the same + return; + } + } + + // TODO: this is an O(N^2) algorithm. It might be worthwhile revisiting this to see if the time + // complexity can be reduced. + + for (int depth_idx = start_idx; depth_idx > 0; --depth_idx) + { + const int node_idx = node_idx_depth_order[depth_idx]; + + // Find the current index of the node_idx in the submission order array + int submission_idx = -1; + for (int i = 0; i < GImNodes->NodeIdxSubmissionOrder.Size; ++i) + { + if (GImNodes->NodeIdxSubmissionOrder[i] == node_idx) + { + submission_idx = i; + break; + } + } + IM_ASSERT(submission_idx >= 0); + + if (submission_idx == depth_idx) + { + continue; + } + + for (int j = submission_idx; j < depth_idx; ++j) + { + DrawListSwapSubmissionIndices(j, j + 1); + ImSwap(GImNodes->NodeIdxSubmissionOrder[j], GImNodes->NodeIdxSubmissionOrder[j + 1]); + } + } +} + +// [SECTION] ui state logic + +ImVec2 GetScreenSpacePinCoordinates( + const ImRect& node_rect, + const ImRect& attribute_rect, + const ImNodesAttributeType type) +{ + IM_ASSERT(type == ImNodesAttributeType_Input || type == ImNodesAttributeType_Output); + const float x = type == ImNodesAttributeType_Input + ? (node_rect.Min.x - GImNodes->Style.PinOffset) + : (node_rect.Max.x + GImNodes->Style.PinOffset); + return ImVec2(x, 0.5f * (attribute_rect.Min.y + attribute_rect.Max.y)); +} + +ImVec2 GetScreenSpacePinCoordinates(const ImNodesEditorContext& editor, const ImPinData& pin) +{ + const ImRect& parent_node_rect = editor.Nodes.Pool[pin.ParentNodeIdx].Rect; + return GetScreenSpacePinCoordinates(parent_node_rect, pin.AttributeRect, pin.Type); +} + +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()); +} + +void BeginNodeSelection(ImNodesEditorContext& editor, const int node_idx) +{ + // Don't start selecting a node if we are e.g. already creating and dragging + // a new link! New link creation can happen when the mouse is clicked over + // a node, but within the hover radius of a pin. + if (editor.ClickInteraction.Type != ImNodesClickInteractionType_None) + { + return; + } + + editor.ClickInteraction.Type = ImNodesClickInteractionType_Node; + // If the node is not already contained in the selection, then we want only + // the interaction node to be selected, effective immediately. + // + // If the multiple selection modifier is active, we want to add this node + // to the current list of selected nodes. + // + // Otherwise, we want to allow for the possibility of multiple nodes to be + // moved at once. + if (!editor.SelectedNodeIndices.contains(node_idx)) + { + editor.SelectedLinkIndices.clear(); + if (!GImNodes->MultipleSelectModifier) + { + editor.SelectedNodeIndices.clear(); + } + editor.SelectedNodeIndices.push_back(node_idx); + + // Ensure that individually selected nodes get rendered on top + ImVector& depth_stack = editor.NodeDepthOrder; + const int* const elem = depth_stack.find(node_idx); + IM_ASSERT(elem != depth_stack.end()); + depth_stack.erase(elem); + depth_stack.push_back(node_idx); + } + // Deselect a previously-selected node + else if (GImNodes->MultipleSelectModifier) + { + const int* const node_ptr = editor.SelectedNodeIndices.find(node_idx); + editor.SelectedNodeIndices.erase(node_ptr); + + // Don't allow dragging after deselecting + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + + // To support snapping of multiple nodes, we need to store the offset of + // each node in the selection to the origin of the dragged node. + const ImVec2 ref_origin = editor.Nodes.Pool[node_idx].Origin; + editor.PrimaryNodeOffset = + ref_origin + GImNodes->CanvasOriginScreenSpace + editor.Panning - GImNodes->MousePos; + + editor.SelectedNodeOffsets.clear(); + for (int idx = 0; idx < editor.SelectedNodeIndices.Size; idx++) + { + const int node = editor.SelectedNodeIndices[idx]; + const ImVec2 node_origin = editor.Nodes.Pool[node].Origin - ref_origin; + editor.SelectedNodeOffsets.push_back(node_origin); + } +} + +void BeginLinkSelection(ImNodesEditorContext& editor, const int link_idx) +{ + editor.ClickInteraction.Type = ImNodesClickInteractionType_Link; + // When a link is selected, clear all other selections, and insert the link + // as the sole selection. + editor.SelectedNodeIndices.clear(); + editor.SelectedLinkIndices.clear(); + editor.SelectedLinkIndices.push_back(link_idx); +} + +void BeginLinkDetach(ImNodesEditorContext& editor, const int link_idx, const int detach_pin_idx) +{ + const ImLinkData& link = editor.Links.Pool[link_idx]; + ImClickInteractionState& state = editor.ClickInteraction; + state.Type = ImNodesClickInteractionType_LinkCreation; + state.LinkCreation.EndPinIdx.Reset(); + state.LinkCreation.StartPinIdx = + detach_pin_idx == link.StartPinIdx ? link.EndPinIdx : link.StartPinIdx; + GImNodes->DeletedLinkIdx = link_idx; +} + +void BeginLinkCreation(ImNodesEditorContext& editor, const int hovered_pin_idx) +{ + editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; + editor.ClickInteraction.LinkCreation.StartPinIdx = hovered_pin_idx; + editor.ClickInteraction.LinkCreation.EndPinIdx.Reset(); + editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_Standard; + GImNodes->ImNodesUIState |= ImNodesUIState_LinkStarted; +} + +void BeginLinkInteraction( + ImNodesEditorContext& editor, + const int link_idx, + const ImOptionalIndex pin_idx = ImOptionalIndex()) +{ + // Check if we are clicking the link with the modifier pressed. + // This will in a link detach via clicking. + + const bool modifier_pressed = GImNodes->Io.LinkDetachWithModifierClick.Modifier == NULL + ? false + : *GImNodes->Io.LinkDetachWithModifierClick.Modifier; + + if (modifier_pressed) + { + const ImLinkData& link = editor.Links.Pool[link_idx]; + const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; + const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + const ImVec2& mouse_pos = GImNodes->MousePos; + const float dist_to_start = ImLengthSqr(start_pin.Pos - mouse_pos); + const float dist_to_end = ImLengthSqr(end_pin.Pos - mouse_pos); + const int closest_pin_idx = dist_to_start < dist_to_end ? link.StartPinIdx : link.EndPinIdx; + + editor.ClickInteraction.Type = ImNodesClickInteractionType_LinkCreation; + BeginLinkDetach(editor, link_idx, closest_pin_idx); + editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; + } + else + { + if (pin_idx.HasValue()) + { + const int hovered_pin_flags = editor.Pins.Pool[pin_idx.Value()].Flags; + + // Check the 'click and drag to detach' case. + if (hovered_pin_flags & ImNodesAttributeFlags_EnableLinkDetachWithDragClick) + { + BeginLinkDetach(editor, link_idx, pin_idx.Value()); + editor.ClickInteraction.LinkCreation.Type = ImNodesLinkCreationType_FromDetach; + } + else + { + BeginLinkCreation(editor, pin_idx.Value()); + } + } + else + { + BeginLinkSelection(editor, link_idx); + } + } +} + +static inline bool IsMiniMapHovered(); + +void BeginCanvasInteraction(ImNodesEditorContext& editor) +{ + const bool any_ui_element_hovered = + GImNodes->HoveredNodeIdx.HasValue() || GImNodes->HoveredLinkIdx.HasValue() || + GImNodes->HoveredPinIdx.HasValue() || ImGui::IsAnyItemHovered(); + + const bool mouse_not_in_canvas = !MouseInCanvas(); + + if (editor.ClickInteraction.Type != ImNodesClickInteractionType_None || + any_ui_element_hovered || mouse_not_in_canvas) + { + return; + } + + const bool started_panning = GImNodes->AltMouseClicked; + + if (started_panning) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_Panning; + } + else if (GImNodes->LeftMouseClicked) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_BoxSelection; + editor.ClickInteraction.BoxSelector.Rect.Min = + ScreenSpaceToGridSpace(editor, GImNodes->MousePos); + } +} + +void BoxSelectorUpdateSelection(ImNodesEditorContext& editor, ImRect box_rect) +{ + // Invert box selector coordinates as needed + + if (box_rect.Min.x > box_rect.Max.x) + { + ImSwap(box_rect.Min.x, box_rect.Max.x); + } + + if (box_rect.Min.y > box_rect.Max.y) + { + ImSwap(box_rect.Min.y, box_rect.Max.y); + } + + // Update node selection + + editor.SelectedNodeIndices.clear(); + + // Test for overlap against node rectangles + + for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) + { + if (editor.Nodes.InUse[node_idx]) + { + ImNodeData& node = editor.Nodes.Pool[node_idx]; + if (box_rect.Overlaps(node.Rect)) + { + editor.SelectedNodeIndices.push_back(node_idx); + } + } + } + + // Update link selection + + editor.SelectedLinkIndices.clear(); + + // Test for overlap against links + + for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) + { + if (editor.Links.InUse[link_idx]) + { + const ImLinkData& link = editor.Links.Pool[link_idx]; + + const ImPinData& pin_start = editor.Pins.Pool[link.StartPinIdx]; + const ImPinData& pin_end = editor.Pins.Pool[link.EndPinIdx]; + const ImRect& node_start_rect = editor.Nodes.Pool[pin_start.ParentNodeIdx].Rect; + const ImRect& node_end_rect = editor.Nodes.Pool[pin_end.ParentNodeIdx].Rect; + + const ImVec2 start = GetScreenSpacePinCoordinates( + node_start_rect, pin_start.AttributeRect, pin_start.Type); + const ImVec2 end = + GetScreenSpacePinCoordinates(node_end_rect, pin_end.AttributeRect, pin_end.Type); + + // Test + if (RectangleOverlapsLink(box_rect, start, end, pin_start.Type)) + { + editor.SelectedLinkIndices.push_back(link_idx); + } + } + } +} + +ImVec2 SnapOriginToGrid(ImVec2 origin) +{ + if (GImNodes->Style.Flags & ImNodesStyleFlags_GridSnapping) + { + const float spacing = GImNodes->Style.GridSpacing; + const float spacing2 = spacing * 0.5f; + + // Snap the origin to the nearest grid point in any direction + float modx = fmodf(fabsf(origin.x) + spacing2, spacing) - spacing2; + float mody = fmodf(fabsf(origin.y) + spacing2, spacing) - spacing2; + origin.x += (origin.x < 0.f) ? modx : -modx; + origin.y += (origin.y < 0.f) ? mody : -mody; + } + + return origin; +} + +void TranslateSelectedNodes(ImNodesEditorContext& editor) +{ + if (GImNodes->LeftMouseDragging) + { + // If we have grid snap enabled, don't start moving nodes until we've moved the mouse + // slightly + const bool shouldTranslate = (GImNodes->Style.Flags & ImNodesStyleFlags_GridSnapping) + ? ImGui::GetIO().MouseDragMaxDistanceSqr[0] > 5.0 + : true; + + const ImVec2 origin = SnapOriginToGrid( + GImNodes->MousePos - GImNodes->CanvasOriginScreenSpace - editor.Panning + + editor.PrimaryNodeOffset); + for (int i = 0; i < editor.SelectedNodeIndices.size(); ++i) + { + const ImVec2 node_rel = editor.SelectedNodeOffsets[i]; + const int node_idx = editor.SelectedNodeIndices[i]; + ImNodeData& node = editor.Nodes.Pool[node_idx]; + if (node.Draggable && shouldTranslate) + { + node.Origin = origin + node_rel + editor.AutoPanningDelta; + } + } + } +} + +struct LinkPredicate +{ + bool operator()(const ImLinkData& lhs, const ImLinkData& rhs) const + { + // Do a unique compare by sorting the pins' addresses. + // This catches duplicate links, whether they are in the + // same direction or not. + // Sorting by pin index should have the uniqueness guarantees as sorting + // by id -- each unique id will get one slot in the link pool array. + + int lhs_start = lhs.StartPinIdx; + int lhs_end = lhs.EndPinIdx; + int rhs_start = rhs.StartPinIdx; + int rhs_end = rhs.EndPinIdx; + + if (lhs_start > lhs_end) + { + ImSwap(lhs_start, lhs_end); + } + + if (rhs_start > rhs_end) + { + ImSwap(rhs_start, rhs_end); + } + + return lhs_start == rhs_start && lhs_end == rhs_end; + } +}; + +ImOptionalIndex FindDuplicateLink( + const ImNodesEditorContext& editor, + const int start_pin_idx, + const int end_pin_idx) +{ + ImLinkData test_link(0); + test_link.StartPinIdx = start_pin_idx; + test_link.EndPinIdx = end_pin_idx; + for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) + { + const ImLinkData& link = editor.Links.Pool[link_idx]; + if (LinkPredicate()(test_link, link) && editor.Links.InUse[link_idx]) + { + return ImOptionalIndex(link_idx); + } + } + + return ImOptionalIndex(); +} + +bool ShouldLinkSnapToPin( + const ImNodesEditorContext& editor, + const ImPinData& start_pin, + const int hovered_pin_idx, + const ImOptionalIndex duplicate_link) +{ + const ImPinData& end_pin = editor.Pins.Pool[hovered_pin_idx]; + + // The end pin must be in a different node + if (start_pin.ParentNodeIdx == end_pin.ParentNodeIdx) + { + return false; + } + + // The end pin must be of a different type + if (start_pin.Type == end_pin.Type) + { + return false; + } + + // The link to be created must not be a duplicate, unless it is the link which was created on + // snap. In that case we want to snap, since we want it to appear visually as if the created + // link remains snapped to the pin. + if (duplicate_link.HasValue() && !(duplicate_link == GImNodes->SnapLinkIdx)) + { + return false; + } + + return true; +} + +void ClickInteractionUpdate(ImNodesEditorContext& editor) +{ + switch (editor.ClickInteraction.Type) + { + case ImNodesClickInteractionType_BoxSelection: + { + editor.ClickInteraction.BoxSelector.Rect.Max = + ScreenSpaceToGridSpace(editor, GImNodes->MousePos); + + ImRect box_rect = editor.ClickInteraction.BoxSelector.Rect; + box_rect.Min = GridSpaceToScreenSpace(editor, box_rect.Min); + box_rect.Max = GridSpaceToScreenSpace(editor, box_rect.Max); + + BoxSelectorUpdateSelection(editor, box_rect); + + const ImU32 box_selector_color = GImNodes->Style.Colors[ImNodesCol_BoxSelector]; + const ImU32 box_selector_outline = GImNodes->Style.Colors[ImNodesCol_BoxSelectorOutline]; + GImNodes->CanvasDrawList->AddRectFilled(box_rect.Min, box_rect.Max, box_selector_color); + GImNodes->CanvasDrawList->AddRect(box_rect.Min, box_rect.Max, box_selector_outline); + + if (GImNodes->LeftMouseReleased) + { + ImVector& depth_stack = editor.NodeDepthOrder; + const ImVector& selected_idxs = editor.SelectedNodeIndices; + + // Bump the selected node indices, in order, to the top of the depth stack. + // NOTE: this algorithm has worst case time complexity of O(N^2), if the node selection + // is ~ N (due to selected_idxs.contains()). + + if ((selected_idxs.Size > 0) && (selected_idxs.Size < depth_stack.Size)) + { + int num_moved = 0; // The number of indices moved. Stop after selected_idxs.Size + for (int i = 0; i < depth_stack.Size - selected_idxs.Size; ++i) + { + for (int node_idx = depth_stack[i]; selected_idxs.contains(node_idx); + node_idx = depth_stack[i]) + { + depth_stack.erase(depth_stack.begin() + static_cast(i)); + depth_stack.push_back(node_idx); + ++num_moved; + } + + if (num_moved == selected_idxs.Size) + { + break; + } + } + } + + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + } + break; + case ImNodesClickInteractionType_Node: + { + TranslateSelectedNodes(editor); + + if (GImNodes->LeftMouseReleased) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + } + break; + case ImNodesClickInteractionType_Link: + { + if (GImNodes->LeftMouseReleased) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + } + break; + case ImNodesClickInteractionType_LinkCreation: + { + const ImPinData& start_pin = + editor.Pins.Pool[editor.ClickInteraction.LinkCreation.StartPinIdx]; + + const ImOptionalIndex maybe_duplicate_link_idx = + GImNodes->HoveredPinIdx.HasValue() + ? FindDuplicateLink( + editor, + editor.ClickInteraction.LinkCreation.StartPinIdx, + GImNodes->HoveredPinIdx.Value()) + : ImOptionalIndex(); + + const bool should_snap = + GImNodes->HoveredPinIdx.HasValue() && + ShouldLinkSnapToPin( + editor, start_pin, GImNodes->HoveredPinIdx.Value(), maybe_duplicate_link_idx); + + // If we created on snap and the hovered pin is empty or changed, then we need signal that + // the link's state has changed. + const bool snapping_pin_changed = + editor.ClickInteraction.LinkCreation.EndPinIdx.HasValue() && + !(GImNodes->HoveredPinIdx == editor.ClickInteraction.LinkCreation.EndPinIdx); + + // Detach the link that was created by this link event if it's no longer in snap range + if (snapping_pin_changed && GImNodes->SnapLinkIdx.HasValue()) + { + BeginLinkDetach( + editor, + GImNodes->SnapLinkIdx.Value(), + editor.ClickInteraction.LinkCreation.EndPinIdx.Value()); + } + + const ImVec2 start_pos = GetScreenSpacePinCoordinates(editor, start_pin); + // If we are within the hover radius of a receiving pin, snap the link + // endpoint to it + const ImVec2 end_pos = should_snap + ? GetScreenSpacePinCoordinates( + editor, editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()]) + : GImNodes->MousePos; + + const CubicBezier cubic_bezier = GetCubicBezier( + start_pos, end_pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); +#if IMGUI_VERSION_NUM < 18000 + GImNodes->CanvasDrawList->AddBezierCurve( +#else + GImNodes->CanvasDrawList->AddBezierCubic( +#endif + cubic_bezier.P0, + cubic_bezier.P1, + cubic_bezier.P2, + cubic_bezier.P3, + GImNodes->Style.Colors[ImNodesCol_Link], + GImNodes->Style.LinkThickness, + cubic_bezier.NumSegments); + + const bool link_creation_on_snap = + GImNodes->HoveredPinIdx.HasValue() && + (editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()].Flags & + ImNodesAttributeFlags_EnableLinkCreationOnSnap); + + if (!should_snap) + { + editor.ClickInteraction.LinkCreation.EndPinIdx.Reset(); + } + + const bool create_link = + should_snap && (GImNodes->LeftMouseReleased || link_creation_on_snap); + + if (create_link && !maybe_duplicate_link_idx.HasValue()) + { + // Avoid send OnLinkCreated() events every frame if the snap link is not saved + // (only applies for EnableLinkCreationOnSnap) + if (!GImNodes->LeftMouseReleased && + editor.ClickInteraction.LinkCreation.EndPinIdx == GImNodes->HoveredPinIdx) + { + break; + } + + GImNodes->ImNodesUIState |= ImNodesUIState_LinkCreated; + editor.ClickInteraction.LinkCreation.EndPinIdx = GImNodes->HoveredPinIdx.Value(); + } + + if (GImNodes->LeftMouseReleased) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + if (!create_link) + { + GImNodes->ImNodesUIState |= ImNodesUIState_LinkDropped; + } + } + } + break; + case ImNodesClickInteractionType_Panning: + { + const bool dragging = GImNodes->AltMouseDragging; + + if (dragging) + { + editor.Panning += ImGui::GetIO().MouseDelta; + } + else + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + } + break; + case ImNodesClickInteractionType_ImGuiItem: + { + if (GImNodes->LeftMouseReleased) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_None; + } + } + case ImNodesClickInteractionType_None: + break; + default: + IM_ASSERT(!"Unreachable code!"); + break; + } +} + +void ResolveOccludedPins(const ImNodesEditorContext& editor, ImVector& occluded_pin_indices) +{ + const ImVector& depth_stack = editor.NodeDepthOrder; + + occluded_pin_indices.resize(0); + + if (depth_stack.Size < 2) + { + return; + } + + // For each node in the depth stack + for (int depth_idx = 0; depth_idx < (depth_stack.Size - 1); ++depth_idx) + { + const ImNodeData& node_below = editor.Nodes.Pool[depth_stack[depth_idx]]; + + // Iterate over the rest of the depth stack to find nodes overlapping the pins + for (int next_depth_idx = depth_idx + 1; next_depth_idx < depth_stack.Size; + ++next_depth_idx) + { + const ImRect& rect_above = editor.Nodes.Pool[depth_stack[next_depth_idx]].Rect; + + // Iterate over each pin + for (int idx = 0; idx < node_below.PinIndices.Size; ++idx) + { + const int pin_idx = node_below.PinIndices[idx]; + const ImVec2& pin_pos = editor.Pins.Pool[pin_idx].Pos; + + if (rect_above.Contains(pin_pos)) + { + occluded_pin_indices.push_back(pin_idx); + } + } + } + } +} + +ImOptionalIndex ResolveHoveredPin( + const ImObjectPool& pins, + const ImVector& occluded_pin_indices) +{ + float smallest_distance = FLT_MAX; + ImOptionalIndex pin_idx_with_smallest_distance; + + const float hover_radius_sqr = GImNodes->Style.PinHoverRadius * GImNodes->Style.PinHoverRadius; + + for (int idx = 0; idx < pins.Pool.Size; ++idx) + { + if (!pins.InUse[idx]) + { + continue; + } + + if (occluded_pin_indices.contains(idx)) + { + continue; + } + + const ImVec2& pin_pos = pins.Pool[idx].Pos; + const float distance_sqr = ImLengthSqr(pin_pos - GImNodes->MousePos); + + // TODO: GImNodes->Style.PinHoverRadius needs to be copied into pin data and the pin-local + // value used here. This is no longer called in BeginAttribute/EndAttribute scope and the + // detected pin might have a different hover radius than what the user had when calling + // BeginAttribute/EndAttribute. + if (distance_sqr < hover_radius_sqr && distance_sqr < smallest_distance) + { + smallest_distance = distance_sqr; + pin_idx_with_smallest_distance = idx; + } + } + + return pin_idx_with_smallest_distance; +} + +ImOptionalIndex ResolveHoveredNode(const ImVector& depth_stack) +{ + if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 0) + { + return ImOptionalIndex(); + } + + if (GImNodes->NodeIndicesOverlappingWithMouse.size() == 1) + { + return ImOptionalIndex(GImNodes->NodeIndicesOverlappingWithMouse[0]); + } + + int largest_depth_idx = -1; + int node_idx_on_top = -1; + + for (int i = 0; i < GImNodes->NodeIndicesOverlappingWithMouse.size(); ++i) + { + const int node_idx = GImNodes->NodeIndicesOverlappingWithMouse[i]; + for (int depth_idx = 0; depth_idx < depth_stack.size(); ++depth_idx) + { + if (depth_stack[depth_idx] == node_idx && (depth_idx > largest_depth_idx)) + { + largest_depth_idx = depth_idx; + node_idx_on_top = node_idx; + } + } + } + + IM_ASSERT(node_idx_on_top != -1); + return ImOptionalIndex(node_idx_on_top); +} + +ImOptionalIndex ResolveHoveredLink( + const ImObjectPool& links, + const ImObjectPool& pins) +{ + float smallest_distance = FLT_MAX; + ImOptionalIndex link_idx_with_smallest_distance; + + // There are two ways a link can be detected as "hovered". + // 1. The link is within hover distance to the mouse. The closest such link is selected as being + // hovered over. + // 2. If the link is connected to the currently hovered pin. + // + // The latter is a requirement for link detaching with drag click to work, as both a link and + // pin are required to be hovered over for the feature to work. + + for (int idx = 0; idx < links.Pool.Size; ++idx) + { + if (!links.InUse[idx]) + { + continue; + } + + const ImLinkData& link = links.Pool[idx]; + const ImPinData& start_pin = pins.Pool[link.StartPinIdx]; + const ImPinData& end_pin = pins.Pool[link.EndPinIdx]; + + // If there is a hovered pin links can only be considered hovered if they use that pin + if (GImNodes->HoveredPinIdx.HasValue()) + { + if (GImNodes->HoveredPinIdx == link.StartPinIdx || + GImNodes->HoveredPinIdx == link.EndPinIdx) + { + return idx; + } + continue; + } + + // TODO: the calculated CubicBeziers could be cached since we generate them again when + // rendering the links + + const CubicBezier cubic_bezier = GetCubicBezier( + start_pin.Pos, end_pin.Pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); + + // The distance test + { + const ImRect link_rect = GetContainingRectForCubicBezier(cubic_bezier); + + // First, do a simple bounding box test against the box containing the link + // to see whether calculating the distance to the link is worth doing. + if (link_rect.Contains(GImNodes->MousePos)) + { + const float distance = GetDistanceToCubicBezier( + GImNodes->MousePos, cubic_bezier, cubic_bezier.NumSegments); + + // TODO: GImNodes->Style.LinkHoverDistance could be also copied into ImLinkData, + // since we're not calling this function in the same scope as ImNodes::Link(). The + // rendered/detected link might have a different hover distance than what the user + // had specified when calling Link() + if (distance < GImNodes->Style.LinkHoverDistance && distance < smallest_distance) + { + smallest_distance = distance; + link_idx_with_smallest_distance = idx; + } + } + } + } + + return link_idx_with_smallest_distance; +} + +// [SECTION] render helpers + +inline ImRect GetItemRect() { return ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); } + +inline ImVec2 GetNodeTitleBarOrigin(const ImNodeData& node) +{ + return node.Origin + node.LayoutStyle.Padding; +} + +inline ImVec2 GetNodeContentOrigin(const ImNodeData& node) +{ + const ImVec2 title_bar_height = + ImVec2(0.f, node.TitleBarContentRect.GetHeight() + 2.0f * node.LayoutStyle.Padding.y); + return node.Origin + title_bar_height + node.LayoutStyle.Padding; +} + +inline ImRect GetNodeTitleRect(const ImNodeData& node) +{ + ImRect expanded_title_rect = node.TitleBarContentRect; + expanded_title_rect.Expand(node.LayoutStyle.Padding); + + return ImRect( + expanded_title_rect.Min, + expanded_title_rect.Min + ImVec2(node.Rect.GetWidth(), 0.f) + + ImVec2(0.f, expanded_title_rect.GetHeight())); +} + +void DrawGrid(ImNodesEditorContext& editor, const ImVec2& canvas_size) +{ + const ImVec2 offset = editor.Panning; + ImU32 line_color = GImNodes->Style.Colors[ImNodesCol_GridLine]; + ImU32 line_color_prim = GImNodes->Style.Colors[ImNodesCol_GridLinePrimary]; + bool draw_primary = GImNodes->Style.Flags & ImNodesStyleFlags_GridLinesPrimary; + + for (float x = fmodf(offset.x, GImNodes->Style.GridSpacing); x < canvas_size.x; + x += GImNodes->Style.GridSpacing) + { + GImNodes->CanvasDrawList->AddLine( + EditorSpaceToScreenSpace(ImVec2(x, 0.0f)), + EditorSpaceToScreenSpace(ImVec2(x, canvas_size.y)), + offset.x - x == 0.f && draw_primary ? line_color_prim : line_color); + } + + for (float y = fmodf(offset.y, GImNodes->Style.GridSpacing); y < canvas_size.y; + y += GImNodes->Style.GridSpacing) + { + GImNodes->CanvasDrawList->AddLine( + EditorSpaceToScreenSpace(ImVec2(0.0f, y)), + EditorSpaceToScreenSpace(ImVec2(canvas_size.x, y)), + offset.y - y == 0.f && draw_primary ? line_color_prim : line_color); + } +} + +struct QuadOffsets +{ + ImVec2 TopLeft, BottomLeft, BottomRight, TopRight; +}; + +QuadOffsets CalculateQuadOffsets(const float side_length) +{ + const float half_side = 0.5f * side_length; + + QuadOffsets offset; + + offset.TopLeft = ImVec2(-half_side, half_side); + offset.BottomLeft = ImVec2(-half_side, -half_side); + offset.BottomRight = ImVec2(half_side, -half_side); + offset.TopRight = ImVec2(half_side, half_side); + + return offset; +} + +struct TriangleOffsets +{ + ImVec2 TopLeft, BottomLeft, Right; +}; + +TriangleOffsets CalculateTriangleOffsets(const float side_length) +{ + // Calculates the Vec2 offsets from an equilateral triangle's midpoint to + // its vertices. Here is how the left_offset and right_offset are + // calculated. + // + // For an equilateral triangle of side length s, the + // triangle's height, h, is h = s * sqrt(3) / 2. + // + // The length from the base to the midpoint is (1 / 3) * h. The length from + // the midpoint to the triangle vertex is (2 / 3) * h. + const float sqrt_3 = sqrtf(3.0f); + const float left_offset = -0.1666666666667f * sqrt_3 * side_length; + const float right_offset = 0.333333333333f * sqrt_3 * side_length; + const float vertical_offset = 0.5f * side_length; + + TriangleOffsets offset; + offset.TopLeft = ImVec2(left_offset, vertical_offset); + offset.BottomLeft = ImVec2(left_offset, -vertical_offset); + offset.Right = ImVec2(right_offset, 0.f); + + return offset; +} + +void DrawPinShape(const ImVec2& pin_pos, const ImPinData& pin, const ImU32 pin_color) +{ + static const int CIRCLE_NUM_SEGMENTS = 8; + + switch (pin.Shape) + { + case ImNodesPinShape_Circle: + { + GImNodes->CanvasDrawList->AddCircle( + pin_pos, + GImNodes->Style.PinCircleRadius, + pin_color, + CIRCLE_NUM_SEGMENTS, + GImNodes->Style.PinLineThickness); + } + break; + case ImNodesPinShape_CircleFilled: + { + GImNodes->CanvasDrawList->AddCircleFilled( + pin_pos, GImNodes->Style.PinCircleRadius, pin_color, CIRCLE_NUM_SEGMENTS); + } + break; + case ImNodesPinShape_Quad: + { + const QuadOffsets offset = CalculateQuadOffsets(GImNodes->Style.PinQuadSideLength); + GImNodes->CanvasDrawList->AddQuad( + pin_pos + offset.TopLeft, + pin_pos + offset.BottomLeft, + pin_pos + offset.BottomRight, + pin_pos + offset.TopRight, + pin_color, + GImNodes->Style.PinLineThickness); + } + break; + case ImNodesPinShape_QuadFilled: + { + const QuadOffsets offset = CalculateQuadOffsets(GImNodes->Style.PinQuadSideLength); + GImNodes->CanvasDrawList->AddQuadFilled( + pin_pos + offset.TopLeft, + pin_pos + offset.BottomLeft, + pin_pos + offset.BottomRight, + pin_pos + offset.TopRight, + pin_color); + } + break; + case ImNodesPinShape_Triangle: + { + const TriangleOffsets offset = + CalculateTriangleOffsets(GImNodes->Style.PinTriangleSideLength); + GImNodes->CanvasDrawList->AddTriangle( + pin_pos + offset.TopLeft, + pin_pos + offset.BottomLeft, + pin_pos + offset.Right, + pin_color, + // NOTE: for some weird reason, the line drawn by AddTriangle is + // much thinner than the lines drawn by AddCircle or AddQuad. + // Multiplying the line thickness by two seemed to solve the + // problem at a few different thickness values. + 2.f * GImNodes->Style.PinLineThickness); + } + break; + case ImNodesPinShape_TriangleFilled: + { + const TriangleOffsets offset = + CalculateTriangleOffsets(GImNodes->Style.PinTriangleSideLength); + GImNodes->CanvasDrawList->AddTriangleFilled( + pin_pos + offset.TopLeft, + pin_pos + offset.BottomLeft, + pin_pos + offset.Right, + pin_color); + } + break; + default: + IM_ASSERT(!"Invalid PinShape value!"); + break; + } +} + +void DrawPin(ImNodesEditorContext& editor, const int pin_idx) +{ + ImPinData& pin = editor.Pins.Pool[pin_idx]; + const ImRect& parent_node_rect = editor.Nodes.Pool[pin.ParentNodeIdx].Rect; + + pin.Pos = GetScreenSpacePinCoordinates(parent_node_rect, pin.AttributeRect, pin.Type); + + ImU32 pin_color = pin.ColorStyle.Background; + + if (GImNodes->HoveredPinIdx == pin_idx) + { + pin_color = pin.ColorStyle.Hovered; + } + + DrawPinShape(pin.Pos, pin, pin_color); +} + +void DrawNode(ImNodesEditorContext& editor, const int node_idx) +{ + const ImNodeData& node = editor.Nodes.Pool[node_idx]; + ImGui::SetCursorPos(node.Origin + editor.Panning); + + const bool node_hovered = + GImNodes->HoveredNodeIdx == node_idx && + editor.ClickInteraction.Type != ImNodesClickInteractionType_BoxSelection; + + ImU32 node_background = node.ColorStyle.Background; + ImU32 titlebar_background = node.ColorStyle.Titlebar; + + if (editor.SelectedNodeIndices.contains(node_idx)) + { + node_background = node.ColorStyle.BackgroundSelected; + titlebar_background = node.ColorStyle.TitlebarSelected; + } + else if (node_hovered) + { + node_background = node.ColorStyle.BackgroundHovered; + titlebar_background = node.ColorStyle.TitlebarHovered; + } + + { + // node base + GImNodes->CanvasDrawList->AddRectFilled( + node.Rect.Min, node.Rect.Max, node_background, node.LayoutStyle.CornerRounding); + + // title bar: + if (node.TitleBarContentRect.GetHeight() > 0.f) + { + ImRect title_bar_rect = GetNodeTitleRect(node); + +#if IMGUI_VERSION_NUM < 18200 + GImNodes->CanvasDrawList->AddRectFilled( + title_bar_rect.Min, + title_bar_rect.Max, + titlebar_background, + node.LayoutStyle.CornerRounding, + ImDrawCornerFlags_Top); +#else + GImNodes->CanvasDrawList->AddRectFilled( + title_bar_rect.Min, + title_bar_rect.Max, + titlebar_background, + node.LayoutStyle.CornerRounding, + ImDrawFlags_RoundCornersTop); + +#endif + } + + if ((GImNodes->Style.Flags & ImNodesStyleFlags_NodeOutline) != 0) + { +#if IMGUI_VERSION_NUM < 18200 + GImNodes->CanvasDrawList->AddRect( + node.Rect.Min, + node.Rect.Max, + node.ColorStyle.Outline, + node.LayoutStyle.CornerRounding, + ImDrawCornerFlags_All, + node.LayoutStyle.BorderThickness); +#else + GImNodes->CanvasDrawList->AddRect( + node.Rect.Min, + node.Rect.Max, + node.ColorStyle.Outline, + node.LayoutStyle.CornerRounding, + ImDrawFlags_RoundCornersAll, + node.LayoutStyle.BorderThickness); +#endif + } + } + + for (int i = 0; i < node.PinIndices.size(); ++i) + { + DrawPin(editor, node.PinIndices[i]); + } + + if (node_hovered) + { + GImNodes->HoveredNodeIdx = node_idx; + } +} + +void DrawLink(ImNodesEditorContext& editor, const int link_idx) +{ + const ImLinkData& link = editor.Links.Pool[link_idx]; + const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; + const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + + const CubicBezier cubic_bezier = GetCubicBezier( + start_pin.Pos, end_pin.Pos, start_pin.Type, GImNodes->Style.LinkLineSegmentsPerLength); + + const bool link_hovered = + GImNodes->HoveredLinkIdx == link_idx && + editor.ClickInteraction.Type != ImNodesClickInteractionType_BoxSelection; + + if (link_hovered) + { + GImNodes->HoveredLinkIdx = link_idx; + } + + // It's possible for a link to be deleted in begin_link_interaction. A user + // may detach a link, resulting in the link wire snapping to the mouse + // position. + // + // In other words, skip rendering the link if it was deleted. + if (GImNodes->DeletedLinkIdx == link_idx) + { + return; + } + + ImU32 link_color = link.ColorStyle.Base; + if (editor.SelectedLinkIndices.contains(link_idx)) + { + link_color = link.ColorStyle.Selected; + } + else if (link_hovered) + { + link_color = link.ColorStyle.Hovered; + } + +#if IMGUI_VERSION_NUM < 18000 + GImNodes->CanvasDrawList->AddBezierCurve( +#else + GImNodes->CanvasDrawList->AddBezierCubic( +#endif + cubic_bezier.P0, + cubic_bezier.P1, + cubic_bezier.P2, + cubic_bezier.P3, + link_color, + GImNodes->Style.LinkThickness, + cubic_bezier.NumSegments); +} + +void BeginPinAttribute( + const int id, + const ImNodesAttributeType type, + const ImNodesPinShape shape, + const int node_idx) +{ + // Make sure to call BeginNode() before calling + // BeginAttribute() + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Node); + GImNodes->CurrentScope = ImNodesScope_Attribute; + + ImGui::BeginGroup(); + ImGui::PushID(id); + + ImNodesEditorContext& editor = EditorContextGet(); + + GImNodes->CurrentAttributeId = id; + + const int pin_idx = ObjectPoolFindOrCreateIndex(editor.Pins, id); + GImNodes->CurrentPinIdx = pin_idx; + ImPinData& pin = editor.Pins.Pool[pin_idx]; + pin.Id = id; + pin.ParentNodeIdx = node_idx; + pin.Type = type; + pin.Shape = shape; + pin.Flags = GImNodes->CurrentAttributeFlags; + pin.ColorStyle.Background = GImNodes->Style.Colors[ImNodesCol_Pin]; + pin.ColorStyle.Hovered = GImNodes->Style.Colors[ImNodesCol_PinHovered]; +} + +void EndPinAttribute() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Attribute); + GImNodes->CurrentScope = ImNodesScope_Node; + + ImGui::PopID(); + ImGui::EndGroup(); + + if (ImGui::IsItemActive()) + { + GImNodes->ActiveAttribute = true; + GImNodes->ActiveAttributeId = GImNodes->CurrentAttributeId; + } + + ImNodesEditorContext& editor = EditorContextGet(); + ImPinData& pin = editor.Pins.Pool[GImNodes->CurrentPinIdx]; + ImNodeData& node = editor.Nodes.Pool[GImNodes->CurrentNodeIdx]; + pin.AttributeRect = GetItemRect(); + node.PinIndices.push_back(GImNodes->CurrentPinIdx); +} + +void Initialize(ImNodesContext* context) +{ + context->CanvasOriginScreenSpace = ImVec2(0.0f, 0.0f); + context->CanvasRectScreenSpace = ImRect(ImVec2(0.f, 0.f), ImVec2(0.f, 0.f)); + context->CurrentScope = ImNodesScope_None; + + context->CurrentPinIdx = INT_MAX; + context->CurrentNodeIdx = INT_MAX; + + context->DefaultEditorCtx = EditorContextCreate(); + context->EditorCtx = context->DefaultEditorCtx; + + context->CurrentAttributeFlags = ImNodesAttributeFlags_None; + context->AttributeFlagStack.push_back(GImNodes->CurrentAttributeFlags); + + StyleColorsDark(&context->Style); +} + +void Shutdown(ImNodesContext* ctx) { EditorContextFree(ctx->DefaultEditorCtx); } + +// [SECTION] minimap + +static inline bool IsMiniMapActive() +{ + ImNodesEditorContext& editor = EditorContextGet(); + return editor.MiniMapEnabled && editor.MiniMapSizeFraction > 0.0f; +} + +static inline bool IsMiniMapHovered() +{ + ImNodesEditorContext& editor = EditorContextGet(); + return IsMiniMapActive() && + ImGui::IsMouseHoveringRect( + editor.MiniMapRectScreenSpace.Min, editor.MiniMapRectScreenSpace.Max); +} + +static inline void CalcMiniMapLayout() +{ + ImNodesEditorContext& editor = EditorContextGet(); + const ImVec2 offset = GImNodes->Style.MiniMapOffset; + const ImVec2 border = GImNodes->Style.MiniMapPadding; + const ImRect editor_rect = GImNodes->CanvasRectScreenSpace; + + // Compute the size of the mini-map area + ImVec2 mini_map_size; + float mini_map_scaling; + { + const ImVec2 max_size = + ImFloor(editor_rect.GetSize() * editor.MiniMapSizeFraction - border * 2.0f); + const float max_size_aspect_ratio = max_size.x / max_size.y; + const ImVec2 grid_content_size = editor.GridContentBounds.IsInverted() + ? max_size + : ImFloor(editor.GridContentBounds.GetSize()); + const float grid_content_aspect_ratio = grid_content_size.x / grid_content_size.y; + mini_map_size = ImFloor( + grid_content_aspect_ratio > max_size_aspect_ratio + ? ImVec2(max_size.x, max_size.x / grid_content_aspect_ratio) + : ImVec2(max_size.y * grid_content_aspect_ratio, max_size.y)); + mini_map_scaling = mini_map_size.x / grid_content_size.x; + } + + // Compute location of the mini-map + ImVec2 mini_map_pos; + { + ImVec2 align; + + switch (editor.MiniMapLocation) + { + case ImNodesMiniMapLocation_BottomRight: + align.x = 1.0f; + align.y = 1.0f; + break; + case ImNodesMiniMapLocation_BottomLeft: + align.x = 0.0f; + align.y = 1.0f; + break; + case ImNodesMiniMapLocation_TopRight: + align.x = 1.0f; + align.y = 0.0f; + break; + case ImNodesMiniMapLocation_TopLeft: // [[fallthrough]] + default: + align.x = 0.0f; + align.y = 0.0f; + break; + } + + const ImVec2 top_left_pos = editor_rect.Min + offset + border; + const ImVec2 bottom_right_pos = editor_rect.Max - offset - border - mini_map_size; + mini_map_pos = ImFloor(ImLerp(top_left_pos, bottom_right_pos, align)); + } + + editor.MiniMapRectScreenSpace = + ImRect(mini_map_pos - border, mini_map_pos + mini_map_size + border); + editor.MiniMapContentScreenSpace = ImRect(mini_map_pos, mini_map_pos + mini_map_size); + editor.MiniMapScaling = mini_map_scaling; +} + +static void MiniMapDrawNode(ImNodesEditorContext& editor, const int node_idx) +{ + const ImNodeData& node = editor.Nodes.Pool[node_idx]; + + const ImRect node_rect = ScreenSpaceToMiniMapSpace(editor, node.Rect); + + // Round to near whole pixel value for corner-rounding to prevent visual glitches + const float mini_map_node_rounding = + floorf(node.LayoutStyle.CornerRounding * editor.MiniMapScaling); + + ImU32 mini_map_node_background; + + if (editor.ClickInteraction.Type == ImNodesClickInteractionType_None && + ImGui::IsMouseHoveringRect(node_rect.Min, node_rect.Max)) + { + mini_map_node_background = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; + + // Run user callback when hovering a mini-map node + if (editor.MiniMapNodeHoveringCallback) + { + editor.MiniMapNodeHoveringCallback(node.Id, editor.MiniMapNodeHoveringCallbackUserData); + } + } + else if (editor.SelectedNodeIndices.contains(node_idx)) + { + mini_map_node_background = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeBackgroundSelected]; + } + else + { + mini_map_node_background = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeBackground]; + } + + const ImU32 mini_map_node_outline = GImNodes->Style.Colors[ImNodesCol_MiniMapNodeOutline]; + + GImNodes->CanvasDrawList->AddRectFilled( + 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); +} + +static void MiniMapDrawLink(ImNodesEditorContext& editor, const int link_idx) +{ + const ImLinkData& link = editor.Links.Pool[link_idx]; + const ImPinData& start_pin = editor.Pins.Pool[link.StartPinIdx]; + const ImPinData& end_pin = editor.Pins.Pool[link.EndPinIdx]; + + const CubicBezier cubic_bezier = GetCubicBezier( + ScreenSpaceToMiniMapSpace(editor, start_pin.Pos), + ScreenSpaceToMiniMapSpace(editor, end_pin.Pos), + start_pin.Type, + GImNodes->Style.LinkLineSegmentsPerLength / editor.MiniMapScaling); + + // It's possible for a link to be deleted in begin_link_interaction. A user + // may detach a link, resulting in the link wire snapping to the mouse + // position. + // + // In other words, skip rendering the link if it was deleted. + if (GImNodes->DeletedLinkIdx == link_idx) + { + return; + } + + const ImU32 link_color = + GImNodes->Style.Colors + [editor.SelectedLinkIndices.contains(link_idx) ? ImNodesCol_MiniMapLinkSelected + : ImNodesCol_MiniMapLink]; + +#if IMGUI_VERSION_NUM < 18000 + GImNodes->CanvasDrawList->AddBezierCurve( +#else + GImNodes->CanvasDrawList->AddBezierCubic( +#endif + cubic_bezier.P0, + cubic_bezier.P1, + cubic_bezier.P2, + cubic_bezier.P3, + link_color, + GImNodes->Style.LinkThickness * editor.MiniMapScaling, + cubic_bezier.NumSegments); +} + +static void MiniMapUpdate() +{ + ImNodesEditorContext& editor = EditorContextGet(); + + ImU32 mini_map_background; + + if (IsMiniMapHovered()) + { + mini_map_background = GImNodes->Style.Colors[ImNodesCol_MiniMapBackgroundHovered]; + } + else + { + mini_map_background = GImNodes->Style.Colors[ImNodesCol_MiniMapBackground]; + } + + // Create a child window bellow mini-map, so it blocks all mouse interaction on canvas. + int flags = ImGuiWindowFlags_NoBackground; + ImGui::SetCursorScreenPos(editor.MiniMapRectScreenSpace.Min); + ImGui::BeginChild("minimap", editor.MiniMapRectScreenSpace.GetSize(), false, flags); + + const ImRect& mini_map_rect = editor.MiniMapRectScreenSpace; + + // Draw minimap background and border + GImNodes->CanvasDrawList->AddRectFilled( + 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]); + + // Clip draw list items to mini-map rect (after drawing background/outline) + GImNodes->CanvasDrawList->PushClipRect( + mini_map_rect.Min, mini_map_rect.Max, true /* intersect with editor clip-rect */); + + // Draw links first so they appear under nodes, and we can use the same draw channel + for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) + { + if (editor.Links.InUse[link_idx]) + { + MiniMapDrawLink(editor, link_idx); + } + } + + for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) + { + if (editor.Nodes.InUse[node_idx]) + { + MiniMapDrawNode(editor, node_idx); + } + } + + // Draw editor canvas rect inside mini-map + { + const ImU32 canvas_color = GImNodes->Style.Colors[ImNodesCol_MiniMapCanvas]; + const ImU32 outline_color = GImNodes->Style.Colors[ImNodesCol_MiniMapCanvasOutline]; + 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); + } + + // Have to pop mini-map clip rect + GImNodes->CanvasDrawList->PopClipRect(); + + bool mini_map_is_hovered = ImGui::IsWindowHovered(); + + ImGui::EndChild(); + + bool center_on_click = mini_map_is_hovered && ImGui::IsMouseDown(ImGuiMouseButton_Left) && + editor.ClickInteraction.Type == ImNodesClickInteractionType_None && + !GImNodes->NodeIdxSubmissionOrder.empty(); + if (center_on_click) + { + ImVec2 target = MiniMapSpaceToGridSpace(editor, ImGui::GetMousePos()); + ImVec2 center = GImNodes->CanvasRectScreenSpace.GetSize() * 0.5f; + editor.Panning = ImFloor(center - target); + } + + // Reset callback info after use + editor.MiniMapNodeHoveringCallback = NULL; + editor.MiniMapNodeHoveringCallbackUserData = NULL; +} + +// [SECTION] selection helpers + +template +void SelectObject(const ImObjectPool& objects, ImVector& selected_indices, const int id) +{ + const int idx = ObjectPoolFind(objects, id); + IM_ASSERT(idx >= 0); + IM_ASSERT(selected_indices.find(idx) == selected_indices.end()); + selected_indices.push_back(idx); +} + +template +void ClearObjectSelection( + const ImObjectPool& objects, + ImVector& selected_indices, + const int id) +{ + const int idx = ObjectPoolFind(objects, id); + IM_ASSERT(idx >= 0); + IM_ASSERT(selected_indices.find(idx) != selected_indices.end()); + selected_indices.find_erase_unsorted(idx); +} + +template +bool IsObjectSelected(const ImObjectPool& objects, ImVector& selected_indices, const int id) +{ + const int idx = ObjectPoolFind(objects, id); + return selected_indices.find(idx) != selected_indices.end(); +} + +} // namespace +} // namespace IMNODES_NAMESPACE + +// [SECTION] API implementation + +ImNodesIO::EmulateThreeButtonMouse::EmulateThreeButtonMouse() : Modifier(NULL) {} + +ImNodesIO::LinkDetachWithModifierClick::LinkDetachWithModifierClick() : Modifier(NULL) {} + +ImNodesIO::MultipleSelectModifier::MultipleSelectModifier() : Modifier(NULL) {} + +ImNodesIO::ImNodesIO() + : EmulateThreeButtonMouse(), LinkDetachWithModifierClick(), + AltMouseButton(ImGuiMouseButton_Middle), AutoPanningSpeed(1000.0f) +{ +} + +ImNodesStyle::ImNodesStyle() + : GridSpacing(24.f), NodeCornerRounding(4.f), NodePadding(8.f, 8.f), NodeBorderThickness(1.f), + LinkThickness(3.f), LinkLineSegmentsPerLength(0.1f), LinkHoverDistance(10.f), + PinCircleRadius(4.f), PinQuadSideLength(7.f), PinTriangleSideLength(9.5), + PinLineThickness(1.f), PinHoverRadius(10.f), PinOffset(0.f), MiniMapPadding(8.0f, 8.0f), + MiniMapOffset(4.0f, 4.0f), Flags(ImNodesStyleFlags_NodeOutline | ImNodesStyleFlags_GridLines), + Colors() +{ +} + +namespace IMNODES_NAMESPACE +{ +ImNodesContext* CreateContext() +{ + ImNodesContext* ctx = IM_NEW(ImNodesContext)(); + if (GImNodes == NULL) + SetCurrentContext(ctx); + Initialize(ctx); + return ctx; +} + +void DestroyContext(ImNodesContext* ctx) +{ + if (ctx == NULL) + ctx = GImNodes; + Shutdown(ctx); + if (GImNodes == ctx) + SetCurrentContext(NULL); + IM_DELETE(ctx); +} + +ImNodesContext* GetCurrentContext() { return GImNodes; } + +void SetCurrentContext(ImNodesContext* ctx) { GImNodes = ctx; } + +ImNodesEditorContext* EditorContextCreate() +{ + void* mem = ImGui::MemAlloc(sizeof(ImNodesEditorContext)); + new (mem) ImNodesEditorContext(); + return (ImNodesEditorContext*)mem; +} + +void EditorContextFree(ImNodesEditorContext* ctx) +{ + ctx->~ImNodesEditorContext(); + ImGui::MemFree(ctx); +} + +void EditorContextSet(ImNodesEditorContext* ctx) { GImNodes->EditorCtx = ctx; } + +ImVec2 EditorContextGetPanning() +{ + const ImNodesEditorContext& editor = EditorContextGet(); + return editor.Panning; +} + +void EditorContextResetPanning(const ImVec2& pos) +{ + ImNodesEditorContext& editor = EditorContextGet(); + editor.Panning = pos; +} + +void EditorContextMoveToNode(const int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + + editor.Panning.x = -node.Origin.x; + editor.Panning.y = -node.Origin.y; +} + +void SetImGuiContext(ImGuiContext* ctx) { ImGui::SetCurrentContext(ctx); } + +ImNodesIO& GetIO() { return GImNodes->Io; } + +ImNodesStyle& GetStyle() { return GImNodes->Style; } + +void StyleColorsDark(ImNodesStyle* dest) +{ + if (dest == nullptr) + { + dest = &GImNodes->Style; + } + + dest->Colors[ImNodesCol_NodeBackground] = IM_COL32(50, 50, 50, 255); + dest->Colors[ImNodesCol_NodeBackgroundHovered] = IM_COL32(75, 75, 75, 255); + dest->Colors[ImNodesCol_NodeBackgroundSelected] = IM_COL32(75, 75, 75, 255); + dest->Colors[ImNodesCol_NodeOutline] = IM_COL32(100, 100, 100, 255); + // title bar colors match ImGui's titlebg colors + dest->Colors[ImNodesCol_TitleBar] = IM_COL32(41, 74, 122, 255); + dest->Colors[ImNodesCol_TitleBarHovered] = IM_COL32(66, 150, 250, 255); + dest->Colors[ImNodesCol_TitleBarSelected] = IM_COL32(66, 150, 250, 255); + // link colors match ImGui's slider grab colors + dest->Colors[ImNodesCol_Link] = IM_COL32(61, 133, 224, 200); + dest->Colors[ImNodesCol_LinkHovered] = IM_COL32(66, 150, 250, 255); + dest->Colors[ImNodesCol_LinkSelected] = IM_COL32(66, 150, 250, 255); + // pin colors match ImGui's button colors + dest->Colors[ImNodesCol_Pin] = IM_COL32(53, 150, 250, 180); + dest->Colors[ImNodesCol_PinHovered] = IM_COL32(53, 150, 250, 255); + + dest->Colors[ImNodesCol_BoxSelector] = IM_COL32(61, 133, 224, 30); + dest->Colors[ImNodesCol_BoxSelectorOutline] = IM_COL32(61, 133, 224, 150); + + dest->Colors[ImNodesCol_GridBackground] = IM_COL32(40, 40, 50, 200); + dest->Colors[ImNodesCol_GridLine] = IM_COL32(200, 200, 200, 40); + dest->Colors[ImNodesCol_GridLinePrimary] = IM_COL32(240, 240, 240, 60); + + // minimap colors + dest->Colors[ImNodesCol_MiniMapBackground] = IM_COL32(25, 25, 25, 150); + dest->Colors[ImNodesCol_MiniMapBackgroundHovered] = IM_COL32(25, 25, 25, 200); + dest->Colors[ImNodesCol_MiniMapOutline] = IM_COL32(150, 150, 150, 100); + dest->Colors[ImNodesCol_MiniMapOutlineHovered] = IM_COL32(150, 150, 150, 200); + dest->Colors[ImNodesCol_MiniMapNodeBackground] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapNodeBackgroundHovered] = IM_COL32(200, 200, 200, 255); + dest->Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = + dest->Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; + dest->Colors[ImNodesCol_MiniMapNodeOutline] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapLink] = dest->Colors[ImNodesCol_Link]; + dest->Colors[ImNodesCol_MiniMapLinkSelected] = dest->Colors[ImNodesCol_LinkSelected]; + dest->Colors[ImNodesCol_MiniMapCanvas] = IM_COL32(200, 200, 200, 25); + dest->Colors[ImNodesCol_MiniMapCanvasOutline] = IM_COL32(200, 200, 200, 200); +} + +void StyleColorsClassic(ImNodesStyle* dest) +{ + if (dest == nullptr) + { + dest = &GImNodes->Style; + } + + dest->Colors[ImNodesCol_NodeBackground] = IM_COL32(50, 50, 50, 255); + dest->Colors[ImNodesCol_NodeBackgroundHovered] = IM_COL32(75, 75, 75, 255); + dest->Colors[ImNodesCol_NodeBackgroundSelected] = IM_COL32(75, 75, 75, 255); + dest->Colors[ImNodesCol_NodeOutline] = IM_COL32(100, 100, 100, 255); + dest->Colors[ImNodesCol_TitleBar] = IM_COL32(69, 69, 138, 255); + dest->Colors[ImNodesCol_TitleBarHovered] = IM_COL32(82, 82, 161, 255); + dest->Colors[ImNodesCol_TitleBarSelected] = IM_COL32(82, 82, 161, 255); + dest->Colors[ImNodesCol_Link] = IM_COL32(255, 255, 255, 100); + dest->Colors[ImNodesCol_LinkHovered] = IM_COL32(105, 99, 204, 153); + dest->Colors[ImNodesCol_LinkSelected] = IM_COL32(105, 99, 204, 153); + dest->Colors[ImNodesCol_Pin] = IM_COL32(89, 102, 156, 170); + dest->Colors[ImNodesCol_PinHovered] = IM_COL32(102, 122, 179, 200); + dest->Colors[ImNodesCol_BoxSelector] = IM_COL32(82, 82, 161, 100); + dest->Colors[ImNodesCol_BoxSelectorOutline] = IM_COL32(82, 82, 161, 255); + dest->Colors[ImNodesCol_GridBackground] = IM_COL32(40, 40, 50, 200); + dest->Colors[ImNodesCol_GridLine] = IM_COL32(200, 200, 200, 40); + dest->Colors[ImNodesCol_GridLinePrimary] = IM_COL32(240, 240, 240, 60); + + // minimap colors + dest->Colors[ImNodesCol_MiniMapBackground] = IM_COL32(25, 25, 25, 100); + dest->Colors[ImNodesCol_MiniMapBackgroundHovered] = IM_COL32(25, 25, 25, 200); + dest->Colors[ImNodesCol_MiniMapOutline] = IM_COL32(150, 150, 150, 100); + dest->Colors[ImNodesCol_MiniMapOutlineHovered] = IM_COL32(150, 150, 150, 200); + dest->Colors[ImNodesCol_MiniMapNodeBackground] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = + dest->Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; + dest->Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = IM_COL32(200, 200, 240, 255); + dest->Colors[ImNodesCol_MiniMapNodeOutline] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapLink] = dest->Colors[ImNodesCol_Link]; + dest->Colors[ImNodesCol_MiniMapLinkSelected] = dest->Colors[ImNodesCol_LinkSelected]; + dest->Colors[ImNodesCol_MiniMapCanvas] = IM_COL32(200, 200, 200, 25); + dest->Colors[ImNodesCol_MiniMapCanvasOutline] = IM_COL32(200, 200, 200, 200); +} + +void StyleColorsLight(ImNodesStyle* dest) +{ + if (dest == nullptr) + { + dest = &GImNodes->Style; + } + + dest->Colors[ImNodesCol_NodeBackground] = IM_COL32(240, 240, 240, 255); + dest->Colors[ImNodesCol_NodeBackgroundHovered] = IM_COL32(240, 240, 240, 255); + dest->Colors[ImNodesCol_NodeBackgroundSelected] = IM_COL32(240, 240, 240, 255); + dest->Colors[ImNodesCol_NodeOutline] = IM_COL32(100, 100, 100, 255); + dest->Colors[ImNodesCol_TitleBar] = IM_COL32(248, 248, 248, 255); + dest->Colors[ImNodesCol_TitleBarHovered] = IM_COL32(209, 209, 209, 255); + dest->Colors[ImNodesCol_TitleBarSelected] = IM_COL32(209, 209, 209, 255); + // original imgui values: 66, 150, 250 + dest->Colors[ImNodesCol_Link] = IM_COL32(66, 150, 250, 100); + // original imgui values: 117, 138, 204 + dest->Colors[ImNodesCol_LinkHovered] = IM_COL32(66, 150, 250, 242); + dest->Colors[ImNodesCol_LinkSelected] = IM_COL32(66, 150, 250, 242); + // original imgui values: 66, 150, 250 + dest->Colors[ImNodesCol_Pin] = IM_COL32(66, 150, 250, 160); + dest->Colors[ImNodesCol_PinHovered] = IM_COL32(66, 150, 250, 255); + dest->Colors[ImNodesCol_BoxSelector] = IM_COL32(90, 170, 250, 30); + dest->Colors[ImNodesCol_BoxSelectorOutline] = IM_COL32(90, 170, 250, 150); + dest->Colors[ImNodesCol_GridBackground] = IM_COL32(225, 225, 225, 255); + dest->Colors[ImNodesCol_GridLine] = IM_COL32(180, 180, 180, 100); + dest->Colors[ImNodesCol_GridLinePrimary] = IM_COL32(120, 120, 120, 100); + + // minimap colors + dest->Colors[ImNodesCol_MiniMapBackground] = IM_COL32(25, 25, 25, 100); + dest->Colors[ImNodesCol_MiniMapBackgroundHovered] = IM_COL32(25, 25, 25, 200); + dest->Colors[ImNodesCol_MiniMapOutline] = IM_COL32(150, 150, 150, 100); + dest->Colors[ImNodesCol_MiniMapOutlineHovered] = IM_COL32(150, 150, 150, 200); + dest->Colors[ImNodesCol_MiniMapNodeBackground] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = + dest->Colors[ImNodesCol_MiniMapNodeBackgroundHovered]; + dest->Colors[ImNodesCol_MiniMapNodeBackgroundSelected] = IM_COL32(200, 200, 240, 255); + dest->Colors[ImNodesCol_MiniMapNodeOutline] = IM_COL32(200, 200, 200, 100); + dest->Colors[ImNodesCol_MiniMapLink] = dest->Colors[ImNodesCol_Link]; + dest->Colors[ImNodesCol_MiniMapLinkSelected] = dest->Colors[ImNodesCol_LinkSelected]; + dest->Colors[ImNodesCol_MiniMapCanvas] = IM_COL32(200, 200, 200, 25); + dest->Colors[ImNodesCol_MiniMapCanvasOutline] = IM_COL32(200, 200, 200, 200); +} + +void BeginNodeEditor() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + GImNodes->CurrentScope = ImNodesScope_Editor; + + // Reset state from previous pass + + ImNodesEditorContext& editor = EditorContextGet(); + editor.AutoPanningDelta = ImVec2(0, 0); + editor.GridContentBounds = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + editor.MiniMapEnabled = false; + ObjectPoolReset(editor.Nodes); + ObjectPoolReset(editor.Pins); + ObjectPoolReset(editor.Links); + + GImNodes->HoveredNodeIdx.Reset(); + GImNodes->HoveredLinkIdx.Reset(); + GImNodes->HoveredPinIdx.Reset(); + GImNodes->DeletedLinkIdx.Reset(); + GImNodes->SnapLinkIdx.Reset(); + + GImNodes->NodeIndicesOverlappingWithMouse.clear(); + + 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; + + 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); + GImNodes->CanvasOriginScreenSpace = ImGui::GetCursorScreenPos(); + + // NOTE: we have to fetch the canvas draw list *after* we call + // BeginChild(), otherwise the ImGui UI elements are going to be + // rendered into the parent window draw list. + DrawListSet(ImGui::GetWindowDrawList()); + + { + const ImVec2 canvas_size = ImGui::GetWindowSize(); + GImNodes->CanvasRectScreenSpace = ImRect( + EditorSpaceToScreenSpace(ImVec2(0.f, 0.f)), EditorSpaceToScreenSpace(canvas_size)); + + if (GImNodes->Style.Flags & ImNodesStyleFlags_GridLines) + { + DrawGrid(editor, canvas_size); + } + } + } +} + +void EndNodeEditor() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); + GImNodes->CurrentScope = ImNodesScope_None; + + ImNodesEditorContext& editor = EditorContextGet(); + + bool no_grid_content = editor.GridContentBounds.IsInverted(); + if (no_grid_content) + { + editor.GridContentBounds = ScreenSpaceToGridSpace(editor, GImNodes->CanvasRectScreenSpace); + } + + // Detect ImGui interaction first, because it blocks interaction with the rest of the UI + + if (GImNodes->LeftMouseClicked && ImGui::IsAnyItemActive()) + { + editor.ClickInteraction.Type = ImNodesClickInteractionType_ImGuiItem; + } + + // Detect which UI element is being hovered over. Detection is done in a hierarchical fashion, + // because a UI element being hovered excludes any other as being hovered over. + + // Don't do hovering detection for nodes/links/pins when interacting with the mini-map, since + // its an *overlay* with its own interaction behavior and must have precedence during mouse + // interaction. + + if ((editor.ClickInteraction.Type == ImNodesClickInteractionType_None || + editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation) && + MouseInCanvas() && !IsMiniMapHovered()) + { + // Pins needs some special care. We need to check the depth stack to see which pins are + // being occluded by other nodes. + ResolveOccludedPins(editor, GImNodes->OccludedPinIndices); + + GImNodes->HoveredPinIdx = ResolveHoveredPin(editor.Pins, GImNodes->OccludedPinIndices); + + if (!GImNodes->HoveredPinIdx.HasValue()) + { + // Resolve which node is actually on top and being hovered using the depth stack. + GImNodes->HoveredNodeIdx = ResolveHoveredNode(editor.NodeDepthOrder); + } + + // We don't check for hovered pins here, because if we want to detach a link by clicking and + // dragging, we need to have both a link and pin hovered. + if (!GImNodes->HoveredNodeIdx.HasValue()) + { + GImNodes->HoveredLinkIdx = ResolveHoveredLink(editor.Links, editor.Pins); + } + } + + for (int node_idx = 0; node_idx < editor.Nodes.Pool.size(); ++node_idx) + { + if (editor.Nodes.InUse[node_idx]) + { + DrawListActivateNodeBackground(node_idx); + DrawNode(editor, node_idx); + } + } + + // In order to render the links underneath the nodes, we want to first select the bottom draw + // channel. + GImNodes->CanvasDrawList->ChannelsSetCurrent(0); + + for (int link_idx = 0; link_idx < editor.Links.Pool.size(); ++link_idx) + { + if (editor.Links.InUse[link_idx]) + { + DrawLink(editor, link_idx); + } + } + + // Render the click interaction UI elements (partial links, box selector) on top of everything + // else. + + DrawListAppendClickInteractionChannel(); + DrawListActivateClickInteractionChannel(); + + if (IsMiniMapActive()) + { + CalcMiniMapLayout(); + MiniMapUpdate(); + } + + // Handle node graph interaction + + if (!IsMiniMapHovered()) + { + if (GImNodes->LeftMouseClicked && GImNodes->HoveredLinkIdx.HasValue()) + { + BeginLinkInteraction(editor, GImNodes->HoveredLinkIdx.Value(), GImNodes->HoveredPinIdx); + } + + else if (GImNodes->LeftMouseClicked && GImNodes->HoveredPinIdx.HasValue()) + { + BeginLinkCreation(editor, GImNodes->HoveredPinIdx.Value()); + } + + else if (GImNodes->LeftMouseClicked && GImNodes->HoveredNodeIdx.HasValue()) + { + BeginNodeSelection(editor, GImNodes->HoveredNodeIdx.Value()); + } + + else if ( + GImNodes->LeftMouseClicked || GImNodes->LeftMouseReleased || + GImNodes->AltMouseClicked || GImNodes->AltMouseScrollDelta != 0.f) + { + BeginCanvasInteraction(editor); + } + + bool should_auto_pan = + editor.ClickInteraction.Type == ImNodesClickInteractionType_BoxSelection || + editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation || + editor.ClickInteraction.Type == ImNodesClickInteractionType_Node; + if (should_auto_pan && !MouseInCanvas()) + { + ImVec2 mouse = ImGui::GetMousePos(); + ImVec2 center = GImNodes->CanvasRectScreenSpace.GetCenter(); + ImVec2 direction = (center - mouse); + direction = direction * ImInvLength(direction, 0.0); + + editor.AutoPanningDelta = + direction * ImGui::GetIO().DeltaTime * GImNodes->Io.AutoPanningSpeed; + editor.Panning += editor.AutoPanningDelta; + } + } + ClickInteractionUpdate(editor); + + // At this point, draw commands have been issued for all nodes (and pins). Update the node pool + // to detect unused node slots and remove those indices from the depth stack before sorting the + // node draw commands by depth. + ObjectPoolUpdate(editor.Nodes); + ObjectPoolUpdate(editor.Pins); + + DrawListSortChannelsByDepth(editor.NodeDepthOrder); + + // After the links have been rendered, the link pool can be updated as well. + ObjectPoolUpdate(editor.Links); + + // 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 + ImGui::EndGroup(); +} + +void MiniMap( + const float minimap_size_fraction, + const ImNodesMiniMapLocation location, + const ImNodesMiniMapNodeHoveringCallback node_hovering_callback, + const ImNodesMiniMapNodeHoveringCallbackUserData node_hovering_callback_data) +{ + // Check that editor size fraction is sane; must be in the range (0, 1] + IM_ASSERT(minimap_size_fraction > 0.f && minimap_size_fraction <= 1.f); + + // Remember to call before EndNodeEditor + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); + + ImNodesEditorContext& editor = EditorContextGet(); + + editor.MiniMapEnabled = true; + editor.MiniMapSizeFraction = minimap_size_fraction; + editor.MiniMapLocation = location; + + // Set node hovering callback information + editor.MiniMapNodeHoveringCallback = node_hovering_callback; + editor.MiniMapNodeHoveringCallbackUserData = node_hovering_callback_data; + + // Actual drawing/updating of the MiniMap is done in EndNodeEditor so that + // mini map is draw over everything and all pin/link positions are updated + // correctly relative to their respective nodes. Hence, we must store some of + // of the state for the mini map in GImNodes for the actual drawing/updating +} + +void BeginNode(const int node_id) +{ + // Remember to call BeginNodeEditor before calling BeginNode + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); + GImNodes->CurrentScope = ImNodesScope_Node; + + ImNodesEditorContext& editor = EditorContextGet(); + + const int node_idx = ObjectPoolFindOrCreateIndex(editor.Nodes, node_id); + GImNodes->CurrentNodeIdx = node_idx; + + ImNodeData& node = editor.Nodes.Pool[node_idx]; + node.ColorStyle.Background = GImNodes->Style.Colors[ImNodesCol_NodeBackground]; + node.ColorStyle.BackgroundHovered = GImNodes->Style.Colors[ImNodesCol_NodeBackgroundHovered]; + node.ColorStyle.BackgroundSelected = GImNodes->Style.Colors[ImNodesCol_NodeBackgroundSelected]; + node.ColorStyle.Outline = GImNodes->Style.Colors[ImNodesCol_NodeOutline]; + node.ColorStyle.Titlebar = GImNodes->Style.Colors[ImNodesCol_TitleBar]; + node.ColorStyle.TitlebarHovered = GImNodes->Style.Colors[ImNodesCol_TitleBarHovered]; + node.ColorStyle.TitlebarSelected = GImNodes->Style.Colors[ImNodesCol_TitleBarSelected]; + node.LayoutStyle.CornerRounding = GImNodes->Style.NodeCornerRounding; + node.LayoutStyle.Padding = GImNodes->Style.NodePadding; + node.LayoutStyle.BorderThickness = GImNodes->Style.NodeBorderThickness; + + // ImGui::SetCursorPos sets the cursor position, local to the current widget + // (in this case, the child object started in BeginNodeEditor). Use + // ImGui::SetCursorScreenPos to set the screen space coordinates directly. + ImGui::SetCursorPos(GridSpaceToEditorSpace(editor, GetNodeTitleBarOrigin(node))); + + DrawListAddNode(node_idx); + DrawListActivateCurrentNodeForeground(); + + ImGui::PushID(node.Id); + ImGui::BeginGroup(); +} + +void EndNode() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Node); + GImNodes->CurrentScope = ImNodesScope_Editor; + + ImNodesEditorContext& editor = EditorContextGet(); + + // The node's rectangle depends on the ImGui UI group size. + ImGui::EndGroup(); + ImGui::PopID(); + + ImNodeData& node = editor.Nodes.Pool[GImNodes->CurrentNodeIdx]; + node.Rect = GetItemRect(); + node.Rect.Expand(node.LayoutStyle.Padding); + + editor.GridContentBounds.Add(node.Origin); + editor.GridContentBounds.Add(node.Origin + node.Rect.GetSize()); + + if (node.Rect.Contains(GImNodes->MousePos)) + { + GImNodes->NodeIndicesOverlappingWithMouse.push_back(GImNodes->CurrentNodeIdx); + } +} + +ImVec2 GetNodeDimensions(int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + const int node_idx = ObjectPoolFind(editor.Nodes, node_id); + IM_ASSERT(node_idx != -1); // invalid node_id + const ImNodeData& node = editor.Nodes.Pool[node_idx]; + return node.Rect.GetSize(); +} + +void BeginNodeTitleBar() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Node); + ImGui::BeginGroup(); +} + +void EndNodeTitleBar() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Node); + ImGui::EndGroup(); + + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = editor.Nodes.Pool[GImNodes->CurrentNodeIdx]; + node.TitleBarContentRect = GetItemRect(); + + ImGui::ItemAdd(GetNodeTitleRect(node), ImGui::GetID("title_bar")); + + ImGui::SetCursorPos(GridSpaceToEditorSpace(editor, GetNodeContentOrigin(node))); +} + +void BeginInputAttribute(const int id, const ImNodesPinShape shape) +{ + BeginPinAttribute(id, ImNodesAttributeType_Input, shape, GImNodes->CurrentNodeIdx); +} + +void EndInputAttribute() { EndPinAttribute(); } + +void BeginOutputAttribute(const int id, const ImNodesPinShape shape) +{ + BeginPinAttribute(id, ImNodesAttributeType_Output, shape, GImNodes->CurrentNodeIdx); +} + +void EndOutputAttribute() { EndPinAttribute(); } + +void BeginStaticAttribute(const int id) +{ + // Make sure to call BeginNode() before calling BeginAttribute() + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Node); + GImNodes->CurrentScope = ImNodesScope_Attribute; + + GImNodes->CurrentAttributeId = id; + + ImGui::BeginGroup(); + ImGui::PushID(id); +} + +void EndStaticAttribute() +{ + // Make sure to call BeginNode() before calling BeginAttribute() + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Attribute); + GImNodes->CurrentScope = ImNodesScope_Node; + + ImGui::PopID(); + ImGui::EndGroup(); + + if (ImGui::IsItemActive()) + { + GImNodes->ActiveAttribute = true; + GImNodes->ActiveAttributeId = GImNodes->CurrentAttributeId; + } +} + +void PushAttributeFlag(const ImNodesAttributeFlags flag) +{ + GImNodes->CurrentAttributeFlags |= flag; + GImNodes->AttributeFlagStack.push_back(GImNodes->CurrentAttributeFlags); +} + +void PopAttributeFlag() +{ + // PopAttributeFlag called without a matching PushAttributeFlag! + // The bottom value is always the default value, pushed in Initialize(). + IM_ASSERT(GImNodes->AttributeFlagStack.size() > 1); + + GImNodes->AttributeFlagStack.pop_back(); + GImNodes->CurrentAttributeFlags = GImNodes->AttributeFlagStack.back(); +} + +void Link(const int id, const int start_attr_id, const int end_attr_id) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_Editor); + + ImNodesEditorContext& editor = EditorContextGet(); + ImLinkData& link = ObjectPoolFindOrCreateObject(editor.Links, id); + link.Id = id; + link.StartPinIdx = ObjectPoolFindOrCreateIndex(editor.Pins, start_attr_id); + link.EndPinIdx = ObjectPoolFindOrCreateIndex(editor.Pins, end_attr_id); + link.ColorStyle.Base = GImNodes->Style.Colors[ImNodesCol_Link]; + link.ColorStyle.Hovered = GImNodes->Style.Colors[ImNodesCol_LinkHovered]; + link.ColorStyle.Selected = GImNodes->Style.Colors[ImNodesCol_LinkSelected]; + + // Check if this link was created by the current link event + if ((editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation && + editor.Pins.Pool[link.EndPinIdx].Flags & ImNodesAttributeFlags_EnableLinkCreationOnSnap && + editor.ClickInteraction.LinkCreation.StartPinIdx == link.StartPinIdx && + editor.ClickInteraction.LinkCreation.EndPinIdx == link.EndPinIdx) || + (editor.ClickInteraction.LinkCreation.StartPinIdx == link.EndPinIdx && + editor.ClickInteraction.LinkCreation.EndPinIdx == link.StartPinIdx)) + { + GImNodes->SnapLinkIdx = ObjectPoolFindOrCreateIndex(editor.Links, id); + } +} + +void PushColorStyle(const ImNodesCol item, unsigned int color) +{ + GImNodes->ColorModifierStack.push_back(ImNodesColElement(GImNodes->Style.Colors[item], item)); + GImNodes->Style.Colors[item] = color; +} + +void PopColorStyle() +{ + IM_ASSERT(GImNodes->ColorModifierStack.size() > 0); + const ImNodesColElement elem = GImNodes->ColorModifierStack.back(); + GImNodes->Style.Colors[elem.Item] = elem.Color; + GImNodes->ColorModifierStack.pop_back(); +} + +struct ImNodesStyleVarInfo +{ + ImGuiDataType Type; + ImU32 Count; + ImU32 Offset; + void* GetVarPtr(ImNodesStyle* style) const { return (void*)((unsigned char*)style + Offset); } +}; + +static const ImNodesStyleVarInfo GStyleVarInfo[] = { + // ImNodesStyleVar_GridSpacing + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, GridSpacing)}, + // ImNodesStyleVar_NodeCornerRounding + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, NodeCornerRounding)}, + // ImNodesStyleVar_NodePadding + {ImGuiDataType_Float, 2, (ImU32)offsetof(ImNodesStyle, NodePadding)}, + // ImNodesStyleVar_NodeBorderThickness + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, NodeBorderThickness)}, + // ImNodesStyleVar_LinkThickness + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, LinkThickness)}, + // ImNodesStyleVar_LinkLineSegmentsPerLength + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, LinkLineSegmentsPerLength)}, + // ImNodesStyleVar_LinkHoverDistance + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, LinkHoverDistance)}, + // ImNodesStyleVar_PinCircleRadius + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinCircleRadius)}, + // ImNodesStyleVar_PinQuadSideLength + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinQuadSideLength)}, + // ImNodesStyleVar_PinTriangleSideLength + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinTriangleSideLength)}, + // ImNodesStyleVar_PinLineThickness + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinLineThickness)}, + // ImNodesStyleVar_PinHoverRadius + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinHoverRadius)}, + // ImNodesStyleVar_PinOffset + {ImGuiDataType_Float, 1, (ImU32)offsetof(ImNodesStyle, PinOffset)}, + // ImNodesStyleVar_MiniMapPadding + {ImGuiDataType_Float, 2, (ImU32)offsetof(ImNodesStyle, MiniMapPadding)}, + // ImNodesStyleVar_MiniMapOffset + {ImGuiDataType_Float, 2, (ImU32)offsetof(ImNodesStyle, MiniMapOffset)}, +}; + +static const ImNodesStyleVarInfo* GetStyleVarInfo(ImNodesStyleVar idx) +{ + IM_ASSERT(idx >= 0 && idx < ImNodesStyleVar_COUNT); + IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImNodesStyleVar_COUNT); + return &GStyleVarInfo[idx]; +} + +void PushStyleVar(const ImNodesStyleVar item, const float value) +{ + const ImNodesStyleVarInfo* var_info = GetStyleVarInfo(item); + if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) + { + float& style_var = *(float*)var_info->GetVarPtr(&GImNodes->Style); + GImNodes->StyleModifierStack.push_back(ImNodesStyleVarElement(item, style_var)); + style_var = value; + return; + } + IM_ASSERT(0 && "Called PushStyleVar() float variant but variable is not a float!"); +} + +void PushStyleVar(const ImNodesStyleVar item, const ImVec2& value) +{ + const ImNodesStyleVarInfo* var_info = GetStyleVarInfo(item); + if (var_info->Type == ImGuiDataType_Float && var_info->Count == 2) + { + ImVec2& style_var = *(ImVec2*)var_info->GetVarPtr(&GImNodes->Style); + GImNodes->StyleModifierStack.push_back(ImNodesStyleVarElement(item, style_var)); + style_var = value; + return; + } + IM_ASSERT(0 && "Called PushStyleVar() ImVec2 variant but variable is not a ImVec2!"); +} + +void PopStyleVar(int count) +{ + while (count > 0) + { + IM_ASSERT(GImNodes->StyleModifierStack.size() > 0); + const ImNodesStyleVarElement style_backup = GImNodes->StyleModifierStack.back(); + GImNodes->StyleModifierStack.pop_back(); + const ImNodesStyleVarInfo* var_info = GetStyleVarInfo(style_backup.Item); + void* style_var = var_info->GetVarPtr(&GImNodes->Style); + if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) + { + ((float*)style_var)[0] = style_backup.FloatValue[0]; + } + else if (var_info->Type == ImGuiDataType_Float && var_info->Count == 2) + { + ((float*)style_var)[0] = style_backup.FloatValue[0]; + ((float*)style_var)[1] = style_backup.FloatValue[1]; + } + count--; + } +} + +void SetNodeScreenSpacePos(const int node_id, const ImVec2& screen_space_pos) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + node.Origin = ScreenSpaceToGridSpace(editor, screen_space_pos); +} + +void SetNodeEditorSpacePos(const int node_id, const ImVec2& editor_space_pos) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + node.Origin = EditorSpaceToGridSpace(editor, editor_space_pos); +} + +void SetNodeGridSpacePos(const int node_id, const ImVec2& grid_pos) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + node.Origin = grid_pos; +} + +void SetNodeDraggable(const int node_id, const bool draggable) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + node.Draggable = draggable; +} + +ImVec2 GetNodeScreenSpacePos(const int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + const int node_idx = ObjectPoolFind(editor.Nodes, node_id); + IM_ASSERT(node_idx != -1); + ImNodeData& node = editor.Nodes.Pool[node_idx]; + return GridSpaceToScreenSpace(editor, node.Origin); +} + +ImVec2 GetNodeEditorSpacePos(const int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + const int node_idx = ObjectPoolFind(editor.Nodes, node_id); + IM_ASSERT(node_idx != -1); + ImNodeData& node = editor.Nodes.Pool[node_idx]; + return GridSpaceToEditorSpace(editor, node.Origin); +} + +ImVec2 GetNodeGridSpacePos(const int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + const int node_idx = ObjectPoolFind(editor.Nodes, node_id); + IM_ASSERT(node_idx != -1); + ImNodeData& node = editor.Nodes.Pool[node_idx]; + return node.Origin; +} + +void SnapNodeToGrid(int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ImNodeData& node = ObjectPoolFindOrCreateObject(editor.Nodes, node_id); + node.Origin = SnapOriginToGrid(node.Origin); +} + +bool IsEditorHovered() { return MouseInCanvas(); } + +bool IsNodeHovered(int* const node_id) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(node_id != NULL); + + const bool is_hovered = GImNodes->HoveredNodeIdx.HasValue(); + if (is_hovered) + { + const ImNodesEditorContext& editor = EditorContextGet(); + *node_id = editor.Nodes.Pool[GImNodes->HoveredNodeIdx.Value()].Id; + } + return is_hovered; +} + +bool IsLinkHovered(int* const link_id) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(link_id != NULL); + + const bool is_hovered = GImNodes->HoveredLinkIdx.HasValue(); + if (is_hovered) + { + const ImNodesEditorContext& editor = EditorContextGet(); + *link_id = editor.Links.Pool[GImNodes->HoveredLinkIdx.Value()].Id; + } + return is_hovered; +} + +bool IsPinHovered(int* const attr) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(attr != NULL); + + const bool is_hovered = GImNodes->HoveredPinIdx.HasValue(); + if (is_hovered) + { + const ImNodesEditorContext& editor = EditorContextGet(); + *attr = editor.Pins.Pool[GImNodes->HoveredPinIdx.Value()].Id; + } + return is_hovered; +} + +int NumSelectedNodes() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + const ImNodesEditorContext& editor = EditorContextGet(); + return editor.SelectedNodeIndices.size(); +} + +int NumSelectedLinks() +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + const ImNodesEditorContext& editor = EditorContextGet(); + return editor.SelectedLinkIndices.size(); +} + +void GetSelectedNodes(int* node_ids) +{ + IM_ASSERT(node_ids != NULL); + + const ImNodesEditorContext& editor = EditorContextGet(); + for (int i = 0; i < editor.SelectedNodeIndices.size(); ++i) + { + const int node_idx = editor.SelectedNodeIndices[i]; + node_ids[i] = editor.Nodes.Pool[node_idx].Id; + } +} + +void GetSelectedLinks(int* link_ids) +{ + IM_ASSERT(link_ids != NULL); + + const ImNodesEditorContext& editor = EditorContextGet(); + for (int i = 0; i < editor.SelectedLinkIndices.size(); ++i) + { + const int link_idx = editor.SelectedLinkIndices[i]; + link_ids[i] = editor.Links.Pool[link_idx].Id; + } +} + +void ClearNodeSelection() +{ + ImNodesEditorContext& editor = EditorContextGet(); + editor.SelectedNodeIndices.clear(); +} + +void ClearNodeSelection(int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ClearObjectSelection(editor.Nodes, editor.SelectedNodeIndices, node_id); +} + +void ClearLinkSelection() +{ + ImNodesEditorContext& editor = EditorContextGet(); + editor.SelectedLinkIndices.clear(); +} + +void ClearLinkSelection(int link_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + ClearObjectSelection(editor.Links, editor.SelectedLinkIndices, link_id); +} + +void SelectNode(int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + SelectObject(editor.Nodes, editor.SelectedNodeIndices, node_id); +} + +void SelectLink(int link_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + SelectObject(editor.Links, editor.SelectedLinkIndices, link_id); +} + +bool IsNodeSelected(int node_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + return IsObjectSelected(editor.Nodes, editor.SelectedNodeIndices, node_id); +} + +bool IsLinkSelected(int link_id) +{ + ImNodesEditorContext& editor = EditorContextGet(); + return IsObjectSelected(editor.Links, editor.SelectedLinkIndices, link_id); +} + +bool IsAttributeActive() +{ + IM_ASSERT((GImNodes->CurrentScope & ImNodesScope_Node) != 0); + + if (!GImNodes->ActiveAttribute) + { + return false; + } + + return GImNodes->ActiveAttributeId == GImNodes->CurrentAttributeId; +} + +bool IsAnyAttributeActive(int* const attribute_id) +{ + IM_ASSERT((GImNodes->CurrentScope & (ImNodesScope_Node | ImNodesScope_Attribute)) == 0); + + if (!GImNodes->ActiveAttribute) + { + return false; + } + + if (attribute_id != NULL) + { + *attribute_id = GImNodes->ActiveAttributeId; + } + + return true; +} + +bool IsLinkStarted(int* const started_at_id) +{ + // Call this function after EndNodeEditor()! + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(started_at_id != NULL); + + const bool is_started = (GImNodes->ImNodesUIState & ImNodesUIState_LinkStarted) != 0; + if (is_started) + { + const ImNodesEditorContext& editor = EditorContextGet(); + const int pin_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; + *started_at_id = editor.Pins.Pool[pin_idx].Id; + } + + return is_started; +} + +bool IsLinkDropped(int* const started_at_id, const bool including_detached_links) +{ + // Call this function after EndNodeEditor()! + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + + const ImNodesEditorContext& editor = EditorContextGet(); + + const bool link_dropped = + (GImNodes->ImNodesUIState & ImNodesUIState_LinkDropped) != 0 && + (including_detached_links || + editor.ClickInteraction.LinkCreation.Type != ImNodesLinkCreationType_FromDetach); + + if (link_dropped && started_at_id) + { + const int pin_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; + *started_at_id = editor.Pins.Pool[pin_idx].Id; + } + + return link_dropped; +} + +bool IsLinkCreated( + int* const started_at_pin_id, + int* const ended_at_pin_id, + bool* const created_from_snap) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(started_at_pin_id != NULL); + IM_ASSERT(ended_at_pin_id != NULL); + + const bool is_created = (GImNodes->ImNodesUIState & ImNodesUIState_LinkCreated) != 0; + + if (is_created) + { + const ImNodesEditorContext& editor = EditorContextGet(); + const int start_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; + const int end_idx = editor.ClickInteraction.LinkCreation.EndPinIdx.Value(); + const ImPinData& start_pin = editor.Pins.Pool[start_idx]; + const ImPinData& end_pin = editor.Pins.Pool[end_idx]; + + if (start_pin.Type == ImNodesAttributeType_Output) + { + *started_at_pin_id = start_pin.Id; + *ended_at_pin_id = end_pin.Id; + } + else + { + *started_at_pin_id = end_pin.Id; + *ended_at_pin_id = start_pin.Id; + } + + if (created_from_snap) + { + *created_from_snap = + editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation; + } + } + + return is_created; +} + +bool IsLinkCreated( + int* started_at_node_id, + int* started_at_pin_id, + int* ended_at_node_id, + int* ended_at_pin_id, + bool* created_from_snap) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + IM_ASSERT(started_at_node_id != NULL); + IM_ASSERT(started_at_pin_id != NULL); + IM_ASSERT(ended_at_node_id != NULL); + IM_ASSERT(ended_at_pin_id != NULL); + + const bool is_created = (GImNodes->ImNodesUIState & ImNodesUIState_LinkCreated) != 0; + + if (is_created) + { + const ImNodesEditorContext& editor = EditorContextGet(); + const int start_idx = editor.ClickInteraction.LinkCreation.StartPinIdx; + const int end_idx = editor.ClickInteraction.LinkCreation.EndPinIdx.Value(); + const ImPinData& start_pin = editor.Pins.Pool[start_idx]; + const ImNodeData& start_node = editor.Nodes.Pool[start_pin.ParentNodeIdx]; + const ImPinData& end_pin = editor.Pins.Pool[end_idx]; + const ImNodeData& end_node = editor.Nodes.Pool[end_pin.ParentNodeIdx]; + + if (start_pin.Type == ImNodesAttributeType_Output) + { + *started_at_pin_id = start_pin.Id; + *started_at_node_id = start_node.Id; + *ended_at_pin_id = end_pin.Id; + *ended_at_node_id = end_node.Id; + } + else + { + *started_at_pin_id = end_pin.Id; + *started_at_node_id = end_node.Id; + *ended_at_pin_id = start_pin.Id; + *ended_at_node_id = start_node.Id; + } + + if (created_from_snap) + { + *created_from_snap = + editor.ClickInteraction.Type == ImNodesClickInteractionType_LinkCreation; + } + } + + return is_created; +} + +bool IsLinkDestroyed(int* const link_id) +{ + IM_ASSERT(GImNodes->CurrentScope == ImNodesScope_None); + + const bool link_destroyed = GImNodes->DeletedLinkIdx.HasValue(); + if (link_destroyed) + { + const ImNodesEditorContext& editor = EditorContextGet(); + const int link_idx = GImNodes->DeletedLinkIdx.Value(); + *link_id = editor.Links.Pool[link_idx].Id; + } + + return link_destroyed; +} + +namespace +{ +void NodeLineHandler(ImNodesEditorContext& editor, const char* const line) +{ + int id; + int x, y; + if (sscanf(line, "[node.%i", &id) == 1) + { + const int node_idx = ObjectPoolFindOrCreateIndex(editor.Nodes, id); + GImNodes->CurrentNodeIdx = node_idx; + ImNodeData& node = editor.Nodes.Pool[node_idx]; + node.Id = id; + } + else if (sscanf(line, "origin=%i,%i", &x, &y) == 2) + { + ImNodeData& node = editor.Nodes.Pool[GImNodes->CurrentNodeIdx]; + node.Origin = SnapOriginToGrid(ImVec2((float)x, (float)y)); + } +} + +void EditorLineHandler(ImNodesEditorContext& editor, const char* const line) +{ + (void)sscanf(line, "panning=%f,%f", &editor.Panning.x, &editor.Panning.y); +} +} // namespace + +const char* SaveCurrentEditorStateToIniString(size_t* const data_size) +{ + return SaveEditorStateToIniString(&EditorContextGet(), data_size); +} + +const char* SaveEditorStateToIniString( + const ImNodesEditorContext* const editor_ptr, + size_t* const data_size) +{ + IM_ASSERT(editor_ptr != NULL); + const ImNodesEditorContext& editor = *editor_ptr; + + GImNodes->TextBuffer.clear(); + // TODO: check to make sure that the estimate is the upper bound of element + GImNodes->TextBuffer.reserve(64 * editor.Nodes.Pool.size()); + + GImNodes->TextBuffer.appendf( + "[editor]\npanning=%i,%i\n", (int)editor.Panning.x, (int)editor.Panning.y); + + for (int i = 0; i < editor.Nodes.Pool.size(); i++) + { + if (editor.Nodes.InUse[i]) + { + const ImNodeData& node = editor.Nodes.Pool[i]; + GImNodes->TextBuffer.appendf("\n[node.%d]\n", node.Id); + GImNodes->TextBuffer.appendf("origin=%i,%i\n", (int)node.Origin.x, (int)node.Origin.y); + } + } + + if (data_size != NULL) + { + *data_size = GImNodes->TextBuffer.size(); + } + + return GImNodes->TextBuffer.c_str(); +} + +void LoadCurrentEditorStateFromIniString(const char* const data, const size_t data_size) +{ + LoadEditorStateFromIniString(&EditorContextGet(), data, data_size); +} + +void LoadEditorStateFromIniString( + ImNodesEditorContext* const editor_ptr, + const char* const data, + const size_t data_size) +{ + if (data_size == 0u) + { + return; + } + + ImNodesEditorContext& editor = editor_ptr == NULL ? EditorContextGet() : *editor_ptr; + + char* buf = (char*)ImGui::MemAlloc(data_size + 1); + const char* buf_end = buf + data_size; + memcpy(buf, data, data_size); + buf[data_size] = 0; + + void (*line_handler)(ImNodesEditorContext&, const char*); + line_handler = NULL; + char* line_end = NULL; + for (char* line = buf; line < buf_end; line = line_end + 1) + { + while (*line == '\n' || *line == '\r') + { + line++; + } + line_end = line; + while (line_end < buf_end && *line_end != '\n' && *line_end != '\r') + { + line_end++; + } + line_end[0] = 0; + + if (*line == ';' || *line == '\0') + { + continue; + } + + if (line[0] == '[' && line_end[-1] == ']') + { + line_end[-1] = 0; + if (strncmp(line + 1, "node", 4) == 0) + { + line_handler = NodeLineHandler; + } + else if (strcmp(line + 1, "editor") == 0) + { + line_handler = EditorLineHandler; + } + } + + if (line_handler != NULL) + { + line_handler(editor, line); + } + } + ImGui::MemFree(buf); +} + +void SaveCurrentEditorStateToIniFile(const char* const file_name) +{ + SaveEditorStateToIniFile(&EditorContextGet(), file_name); +} + +void SaveEditorStateToIniFile(const ImNodesEditorContext* const editor, const char* const file_name) +{ + size_t data_size = 0u; + const char* data = SaveEditorStateToIniString(editor, &data_size); + FILE* file = ImFileOpen(file_name, "wt"); + if (!file) + { + return; + } + + fwrite(data, sizeof(char), data_size, file); + fclose(file); +} + +void LoadCurrentEditorStateFromIniFile(const char* const file_name) +{ + LoadEditorStateFromIniFile(&EditorContextGet(), file_name); +} + +void LoadEditorStateFromIniFile(ImNodesEditorContext* const editor, const char* const file_name) +{ + size_t data_size = 0u; + char* file_data = (char*)ImFileLoadToMemory(file_name, "rb", &data_size); + + if (!file_data) + { + return; + } + + LoadEditorStateFromIniString(editor, file_data, data_size); + ImGui::MemFree(file_data); +} +} // namespace IMNODES_NAMESPACE diff --git a/Lib/imnodes-master-b2ec254/imnodes.h b/Lib/imnodes-master-b2ec254/imnodes.h new file mode 100644 index 0000000..15ad5ed --- /dev/null +++ b/Lib/imnodes-master-b2ec254/imnodes.h @@ -0,0 +1,438 @@ +#pragma once + +#include +#include + +#ifdef IMNODES_USER_CONFIG +#include IMNODES_USER_CONFIG +#endif + +#ifndef IMNODES_NAMESPACE +#define IMNODES_NAMESPACE ImNodes +#endif + +typedef int ImNodesCol; // -> enum ImNodesCol_ +typedef int ImNodesStyleVar; // -> enum ImNodesStyleVar_ +typedef int ImNodesStyleFlags; // -> enum ImNodesStyleFlags_ +typedef int ImNodesPinShape; // -> enum ImNodesPinShape_ +typedef int ImNodesAttributeFlags; // -> enum ImNodesAttributeFlags_ +typedef int ImNodesMiniMapLocation; // -> enum ImNodesMiniMapLocation_ + +enum ImNodesCol_ +{ + ImNodesCol_NodeBackground = 0, + ImNodesCol_NodeBackgroundHovered, + ImNodesCol_NodeBackgroundSelected, + ImNodesCol_NodeOutline, + ImNodesCol_TitleBar, + ImNodesCol_TitleBarHovered, + ImNodesCol_TitleBarSelected, + ImNodesCol_Link, + ImNodesCol_LinkHovered, + ImNodesCol_LinkSelected, + ImNodesCol_Pin, + ImNodesCol_PinHovered, + ImNodesCol_BoxSelector, + ImNodesCol_BoxSelectorOutline, + ImNodesCol_GridBackground, + ImNodesCol_GridLine, + ImNodesCol_GridLinePrimary, + ImNodesCol_MiniMapBackground, + ImNodesCol_MiniMapBackgroundHovered, + ImNodesCol_MiniMapOutline, + ImNodesCol_MiniMapOutlineHovered, + ImNodesCol_MiniMapNodeBackground, + ImNodesCol_MiniMapNodeBackgroundHovered, + ImNodesCol_MiniMapNodeBackgroundSelected, + ImNodesCol_MiniMapNodeOutline, + ImNodesCol_MiniMapLink, + ImNodesCol_MiniMapLinkSelected, + ImNodesCol_MiniMapCanvas, + ImNodesCol_MiniMapCanvasOutline, + ImNodesCol_COUNT +}; + +enum ImNodesStyleVar_ +{ + ImNodesStyleVar_GridSpacing = 0, + ImNodesStyleVar_NodeCornerRounding, + ImNodesStyleVar_NodePadding, + ImNodesStyleVar_NodeBorderThickness, + ImNodesStyleVar_LinkThickness, + ImNodesStyleVar_LinkLineSegmentsPerLength, + ImNodesStyleVar_LinkHoverDistance, + ImNodesStyleVar_PinCircleRadius, + ImNodesStyleVar_PinQuadSideLength, + ImNodesStyleVar_PinTriangleSideLength, + ImNodesStyleVar_PinLineThickness, + ImNodesStyleVar_PinHoverRadius, + ImNodesStyleVar_PinOffset, + ImNodesStyleVar_MiniMapPadding, + ImNodesStyleVar_MiniMapOffset, + ImNodesStyleVar_COUNT +}; + +enum ImNodesStyleFlags_ +{ + ImNodesStyleFlags_None = 0, + ImNodesStyleFlags_NodeOutline = 1 << 0, + ImNodesStyleFlags_GridLines = 1 << 2, + ImNodesStyleFlags_GridLinesPrimary = 1 << 3, + ImNodesStyleFlags_GridSnapping = 1 << 4 +}; + +enum ImNodesPinShape_ +{ + ImNodesPinShape_Circle, + ImNodesPinShape_CircleFilled, + ImNodesPinShape_Triangle, + ImNodesPinShape_TriangleFilled, + ImNodesPinShape_Quad, + ImNodesPinShape_QuadFilled +}; + +// This enum controls the way the attribute pins behave. +enum ImNodesAttributeFlags_ +{ + ImNodesAttributeFlags_None = 0, + // Allow detaching a link by left-clicking and dragging the link at a pin it is connected to. + // NOTE: the user has to actually delete the link for this to work. A deleted link can be + // detected by calling IsLinkDestroyed() after EndNodeEditor(). + ImNodesAttributeFlags_EnableLinkDetachWithDragClick = 1 << 0, + // Visual snapping of an in progress link will trigger IsLink Created/Destroyed events. Allows + // for previewing the creation of a link while dragging it across attributes. See here for demo: + // https://github.com/Nelarius/imnodes/issues/41#issuecomment-647132113 NOTE: the user has to + // actually delete the link for this to work. A deleted link can be detected by calling + // IsLinkDestroyed() after EndNodeEditor(). + ImNodesAttributeFlags_EnableLinkCreationOnSnap = 1 << 1 +}; + +struct ImNodesIO +{ + struct EmulateThreeButtonMouse + { + EmulateThreeButtonMouse(); + + // The keyboard modifier to use in combination with mouse left click to pan the editor view. + // Set to NULL by default. To enable this feature, set the modifier to point to a boolean + // indicating the state of a modifier. For example, + // + // ImNodes::GetIO().EmulateThreeButtonMouse.Modifier = &ImGui::GetIO().KeyAlt; + const bool* Modifier; + } EmulateThreeButtonMouse; + + struct LinkDetachWithModifierClick + { + LinkDetachWithModifierClick(); + + // Pointer to a boolean value indicating when the desired modifier is pressed. Set to NULL + // by default. To enable the feature, set the modifier to point to a boolean indicating the + // state of a modifier. For example, + // + // ImNodes::GetIO().LinkDetachWithModifierClick.Modifier = &ImGui::GetIO().KeyCtrl; + // + // Left-clicking a link with this modifier pressed will detach that link. NOTE: the user has + // to actually delete the link for this to work. A deleted link can be detected by calling + // IsLinkDestroyed() after EndNodeEditor(). + const bool* Modifier; + } LinkDetachWithModifierClick; + + struct MultipleSelectModifier + { + MultipleSelectModifier(); + + // Pointer to a boolean value indicating when the desired modifier is pressed. Set to NULL + // by default. To enable the feature, set the modifier to point to a boolean indicating the + // state of a modifier. For example, + // + // ImNodes::GetIO().MultipleSelectModifier.Modifier = &ImGui::GetIO().KeyCtrl; + // + // Left-clicking a node with this modifier pressed will add the node to the list of + // currently selected nodes. If this value is NULL, the Ctrl key will be used. + const bool* Modifier; + } MultipleSelectModifier; + + // Holding alt mouse button pans the node area, by default middle mouse button will be used + // Set based on ImGuiMouseButton values + int AltMouseButton; + + // Panning speed when dragging an element and mouse is outside the main editor view. + float AutoPanningSpeed; + + ImNodesIO(); +}; + +struct ImNodesStyle +{ + float GridSpacing; + + float NodeCornerRounding; + ImVec2 NodePadding; + float NodeBorderThickness; + + float LinkThickness; + float LinkLineSegmentsPerLength; + float LinkHoverDistance; + + // The following variables control the look and behavior of the pins. The default size of each + // pin shape is balanced to occupy approximately the same surface area on the screen. + + // The circle radius used when the pin shape is either ImNodesPinShape_Circle or + // ImNodesPinShape_CircleFilled. + float PinCircleRadius; + // The quad side length used when the shape is either ImNodesPinShape_Quad or + // ImNodesPinShape_QuadFilled. + float PinQuadSideLength; + // The equilateral triangle side length used when the pin shape is either + // ImNodesPinShape_Triangle or ImNodesPinShape_TriangleFilled. + float PinTriangleSideLength; + // The thickness of the line used when the pin shape is not filled. + float PinLineThickness; + // The radius from the pin's center position inside of which it is detected as being hovered + // over. + float PinHoverRadius; + // Offsets the pins' positions from the edge of the node to the outside of the node. + float PinOffset; + + // Mini-map padding size between mini-map edge and mini-map content. + ImVec2 MiniMapPadding; + // Mini-map offset from the screen side. + ImVec2 MiniMapOffset; + + // By default, ImNodesStyleFlags_NodeOutline and ImNodesStyleFlags_Gridlines are enabled. + ImNodesStyleFlags Flags; + // Set these mid-frame using Push/PopColorStyle. You can index this color array with with a + // ImNodesCol value. + unsigned int Colors[ImNodesCol_COUNT]; + + ImNodesStyle(); +}; + +enum ImNodesMiniMapLocation_ +{ + ImNodesMiniMapLocation_BottomLeft, + ImNodesMiniMapLocation_BottomRight, + ImNodesMiniMapLocation_TopLeft, + ImNodesMiniMapLocation_TopRight, +}; + +struct ImGuiContext; +struct ImVec2; + +struct ImNodesContext; + +// An editor context corresponds to a set of nodes in a single workspace (created with a single +// Begin/EndNodeEditor pair) +// +// By default, the library creates an editor context behind the scenes, so using any of the imnodes +// functions doesn't require you to explicitly create a context. +struct ImNodesEditorContext; + +// Callback type used to specify special behavior when hovering a node in the minimap +#ifndef ImNodesMiniMapNodeHoveringCallback +typedef void (*ImNodesMiniMapNodeHoveringCallback)(int, void*); +#endif + +#ifndef ImNodesMiniMapNodeHoveringCallbackUserData +typedef void* ImNodesMiniMapNodeHoveringCallbackUserData; +#endif + +namespace IMNODES_NAMESPACE +{ +// Call this function if you are compiling imnodes in to a dll, separate from ImGui. Calling this +// function sets the GImGui global variable, which is not shared across dll boundaries. +void SetImGuiContext(ImGuiContext* ctx); + +ImNodesContext* CreateContext(); +void DestroyContext(ImNodesContext* ctx = NULL); // NULL = destroy current context +ImNodesContext* GetCurrentContext(); +void SetCurrentContext(ImNodesContext* ctx); + +ImNodesEditorContext* EditorContextCreate(); +void EditorContextFree(ImNodesEditorContext*); +void EditorContextSet(ImNodesEditorContext*); +ImVec2 EditorContextGetPanning(); +void EditorContextResetPanning(const ImVec2& pos); +void EditorContextMoveToNode(const int node_id); + +ImNodesIO& GetIO(); + +// Returns the global style struct. See the struct declaration for default values. +ImNodesStyle& GetStyle(); +// Style presets matching the dear imgui styles of the same name. If dest is NULL, the active +// context's ImNodesStyle instance will be used as the destination. +void StyleColorsDark(ImNodesStyle* dest = NULL); // on by default +void StyleColorsClassic(ImNodesStyle* dest = NULL); +void StyleColorsLight(ImNodesStyle* dest = NULL); + +// The top-level function call. Call this before calling BeginNode/EndNode. Calling this function +// will result the node editor grid workspace being rendered. +void BeginNodeEditor(); +void EndNodeEditor(); + +// Add a navigable minimap to the editor; call before EndNodeEditor after all +// nodes and links have been specified +void MiniMap( + const float minimap_size_fraction = 0.2f, + const ImNodesMiniMapLocation location = ImNodesMiniMapLocation_TopLeft, + const ImNodesMiniMapNodeHoveringCallback node_hovering_callback = NULL, + const ImNodesMiniMapNodeHoveringCallbackUserData node_hovering_callback_data = NULL); + +// Use PushColorStyle and PopColorStyle to modify ImNodesStyle::Colors mid-frame. +void PushColorStyle(ImNodesCol item, unsigned int color); +void PopColorStyle(); +void PushStyleVar(ImNodesStyleVar style_item, float value); +void PushStyleVar(ImNodesStyleVar style_item, const ImVec2& value); +void PopStyleVar(int count = 1); + +// id can be any positive or negative integer, but INT_MIN is currently reserved for internal use. +void BeginNode(int id); +void EndNode(); + +ImVec2 GetNodeDimensions(int id); + +// Place your node title bar content (such as the node title, using ImGui::Text) between the +// following function calls. These functions have to be called before adding any attributes, or the +// layout of the node will be incorrect. +void BeginNodeTitleBar(); +void EndNodeTitleBar(); + +// Attributes are ImGui UI elements embedded within the node. Attributes can have pin shapes +// rendered next to them. Links are created between pins. +// +// The activity status of an attribute can be checked via the IsAttributeActive() and +// IsAnyAttributeActive() function calls. This is one easy way of checking for any changes made to +// an attribute's drag float UI, for instance. +// +// Each attribute id must be unique. + +// Create an input attribute block. The pin is rendered on left side. +void BeginInputAttribute(int id, ImNodesPinShape shape = ImNodesPinShape_CircleFilled); +void EndInputAttribute(); +// Create an output attribute block. The pin is rendered on the right side. +void BeginOutputAttribute(int id, ImNodesPinShape shape = ImNodesPinShape_CircleFilled); +void EndOutputAttribute(); +// Create a static attribute block. A static attribute has no pin, and therefore can't be linked to +// anything. However, you can still use IsAttributeActive() and IsAnyAttributeActive() to check for +// attribute activity. +void BeginStaticAttribute(int id); +void EndStaticAttribute(); + +// Push a single AttributeFlags value. By default, only AttributeFlags_None is set. +void PushAttributeFlag(ImNodesAttributeFlags flag); +void PopAttributeFlag(); + +// Render a link between attributes. +// The attributes ids used here must match the ids used in Begin(Input|Output)Attribute function +// calls. The order of start_attr and end_attr doesn't make a difference for rendering the link. +void Link(int id, int start_attribute_id, int end_attribute_id); + +// Enable or disable the ability to click and drag a specific node. +void SetNodeDraggable(int node_id, const bool draggable); + +// The node's position can be expressed in three coordinate systems: +// * screen space coordinates, -- the origin is the upper left corner of the window. +// * editor space coordinates -- the origin is the upper left corner of the node editor window +// * grid space coordinates, -- the origin is the upper left corner of the node editor window, +// translated by the current editor panning vector (see EditorContextGetPanning() and +// EditorContextResetPanning()) + +// Use the following functions to get and set the node's coordinates in these coordinate systems. + +void SetNodeScreenSpacePos(int node_id, const ImVec2& screen_space_pos); +void SetNodeEditorSpacePos(int node_id, const ImVec2& editor_space_pos); +void SetNodeGridSpacePos(int node_id, const ImVec2& grid_pos); + +ImVec2 GetNodeScreenSpacePos(const int node_id); +ImVec2 GetNodeEditorSpacePos(const int node_id); +ImVec2 GetNodeGridSpacePos(const int node_id); + +// If ImNodesStyleFlags_GridSnapping is enabled, snap the specified node's origin to the grid. +void SnapNodeToGrid(int node_id); + +// Returns true if the current node editor canvas is being hovered over by the mouse, and is not +// blocked by any other windows. +bool IsEditorHovered(); +// The following functions return true if a UI element is being hovered over by the mouse cursor. +// Assigns the id of the UI element being hovered over to the function argument. Use these functions +// after EndNodeEditor() has been called. +bool IsNodeHovered(int* node_id); +bool IsLinkHovered(int* link_id); +bool IsPinHovered(int* attribute_id); + +// Use The following two functions to query the number of selected nodes or links in the current +// editor. Use after calling EndNodeEditor(). +int NumSelectedNodes(); +int NumSelectedLinks(); +// Get the selected node/link ids. The pointer argument should point to an integer array with at +// least as many elements as the respective NumSelectedNodes/NumSelectedLinks function call +// returned. +void GetSelectedNodes(int* node_ids); +void GetSelectedLinks(int* link_ids); +// Clears the list of selected nodes/links. Useful if you want to delete a selected node or link. +void ClearNodeSelection(); +void ClearLinkSelection(); +// Use the following functions to add or remove individual nodes or links from the current editors +// selection. Note that all functions require the id to be an existing valid id for this editor. +// Select-functions has the precondition that the object is currently considered unselected. +// Clear-functions has the precondition that the object is currently considered selected. +// Preconditions listed above can be checked via IsNodeSelected/IsLinkSelected if not already +// known. +void SelectNode(int node_id); +void ClearNodeSelection(int node_id); +bool IsNodeSelected(int node_id); +void SelectLink(int link_id); +void ClearLinkSelection(int link_id); +bool IsLinkSelected(int link_id); + +// Was the previous attribute active? This will continuously return true while the left mouse button +// is being pressed over the UI content of the attribute. +bool IsAttributeActive(); +// Was any attribute active? If so, sets the active attribute id to the output function argument. +bool IsAnyAttributeActive(int* attribute_id = NULL); + +// Use the following functions to query a change of state for an existing link, or new link. Call +// these after EndNodeEditor(). + +// Did the user start dragging a new link from a pin? +bool IsLinkStarted(int* started_at_attribute_id); +// Did the user drop the dragged link before attaching it to a pin? +// There are two different kinds of situations to consider when handling this event: +// 1) a link which is created at a pin and then dropped +// 2) an existing link which is detached from a pin and then dropped +// Use the including_detached_links flag to control whether this function triggers when the user +// detaches a link and drops it. +bool IsLinkDropped(int* started_at_attribute_id = NULL, bool including_detached_links = true); +// Did the user finish creating a new link? +bool IsLinkCreated( + int* started_at_attribute_id, + int* ended_at_attribute_id, + bool* created_from_snap = NULL); +bool IsLinkCreated( + int* started_at_node_id, + int* started_at_attribute_id, + int* ended_at_node_id, + int* ended_at_attribute_id, + bool* created_from_snap = NULL); + +// Was an existing link detached from a pin by the user? The detached link's id is assigned to the +// output argument link_id. +bool IsLinkDestroyed(int* link_id); + +// Use the following functions to write the editor context's state to a string, or directly to a +// file. The editor context is serialized in the INI file format. + +const char* SaveCurrentEditorStateToIniString(size_t* data_size = NULL); +const char* SaveEditorStateToIniString( + const ImNodesEditorContext* editor, + size_t* data_size = NULL); + +void LoadCurrentEditorStateFromIniString(const char* data, size_t data_size); +void LoadEditorStateFromIniString(ImNodesEditorContext* editor, const char* data, size_t data_size); + +void SaveCurrentEditorStateToIniFile(const char* file_name); +void SaveEditorStateToIniFile(const ImNodesEditorContext* editor, const char* file_name); + +void LoadCurrentEditorStateFromIniFile(const char* file_name); +void LoadEditorStateFromIniFile(ImNodesEditorContext* editor, const char* file_name); +} // namespace IMNODES_NAMESPACE diff --git a/Lib/imnodes-master-b2ec254/imnodes_internal.h b/Lib/imnodes-master-b2ec254/imnodes_internal.h new file mode 100644 index 0000000..bb1ee8f --- /dev/null +++ b/Lib/imnodes-master-b2ec254/imnodes_internal.h @@ -0,0 +1,500 @@ +#pragma once + +#define IMGUI_DEFINE_MATH_OPERATORS +#include +#include + +#include "imnodes.h" + +#include + +// the structure of this file: +// +// [SECTION] internal enums +// [SECTION] internal data structures +// [SECTION] global and editor context structs +// [SECTION] object pool implementation + +struct ImNodesContext; + +extern ImNodesContext* GImNodes; + +// [SECTION] internal enums + +typedef int ImNodesScope; +typedef int ImNodesAttributeType; +typedef int ImNodesUIState; +typedef int ImNodesClickInteractionType; +typedef int ImNodesLinkCreationType; + +enum ImNodesScope_ +{ + ImNodesScope_None = 1, + ImNodesScope_Editor = 1 << 1, + ImNodesScope_Node = 1 << 2, + ImNodesScope_Attribute = 1 << 3 +}; + +enum ImNodesAttributeType_ +{ + ImNodesAttributeType_None, + ImNodesAttributeType_Input, + ImNodesAttributeType_Output +}; + +enum ImNodesUIState_ +{ + ImNodesUIState_None = 0, + ImNodesUIState_LinkStarted = 1 << 0, + ImNodesUIState_LinkDropped = 1 << 1, + ImNodesUIState_LinkCreated = 1 << 2 +}; + +enum ImNodesClickInteractionType_ +{ + ImNodesClickInteractionType_Node, + ImNodesClickInteractionType_Link, + ImNodesClickInteractionType_LinkCreation, + ImNodesClickInteractionType_Panning, + ImNodesClickInteractionType_BoxSelection, + ImNodesClickInteractionType_ImGuiItem, + ImNodesClickInteractionType_None +}; + +enum ImNodesLinkCreationType_ +{ + ImNodesLinkCreationType_Standard, + ImNodesLinkCreationType_FromDetach +}; + +// [SECTION] internal data structures + +// The object T must have the following interface: +// +// struct T +// { +// T(); +// +// int id; +// }; +template +struct ImObjectPool +{ + ImVector Pool; + ImVector InUse; + ImVector FreeList; + ImGuiStorage IdMap; + + ImObjectPool() : Pool(), InUse(), FreeList(), IdMap() {} +}; + +// Emulates std::optional using the sentinel value `INVALID_INDEX`. +struct ImOptionalIndex +{ + ImOptionalIndex() : _Index(INVALID_INDEX) {} + ImOptionalIndex(const int value) : _Index(value) {} + + // Observers + + inline bool HasValue() const { return _Index != INVALID_INDEX; } + + inline int Value() const + { + IM_ASSERT(HasValue()); + return _Index; + } + + // Modifiers + + inline ImOptionalIndex& operator=(const int value) + { + _Index = value; + return *this; + } + + inline void Reset() { _Index = INVALID_INDEX; } + + inline bool operator==(const ImOptionalIndex& rhs) const { return _Index == rhs._Index; } + + inline bool operator==(const int rhs) const { return _Index == rhs; } + + inline bool operator!=(const ImOptionalIndex& rhs) const { return _Index != rhs._Index; } + + inline bool operator!=(const int rhs) const { return _Index != rhs; } + + static const int INVALID_INDEX = -1; + +private: + int _Index; +}; + +struct ImNodeData +{ + int Id; + ImVec2 Origin; // The node origin is in editor space + ImRect TitleBarContentRect; + ImRect Rect; + + struct + { + ImU32 Background, BackgroundHovered, BackgroundSelected, Outline, Titlebar, TitlebarHovered, + TitlebarSelected; + } ColorStyle; + + struct + { + float CornerRounding; + ImVec2 Padding; + float BorderThickness; + } LayoutStyle; + + ImVector PinIndices; + bool Draggable; + + ImNodeData(const int node_id) + : Id(node_id), Origin(0.0f, 0.0f), TitleBarContentRect(), + Rect(ImVec2(0.0f, 0.0f), ImVec2(0.0f, 0.0f)), ColorStyle(), LayoutStyle(), PinIndices(), + Draggable(true) + { + } + + ~ImNodeData() { Id = INT_MIN; } +}; + +struct ImPinData +{ + int Id; + int ParentNodeIdx; + ImRect AttributeRect; + ImNodesAttributeType Type; + ImNodesPinShape Shape; + ImVec2 Pos; // screen-space coordinates + int Flags; + + struct + { + ImU32 Background, Hovered; + } ColorStyle; + + ImPinData(const int pin_id) + : Id(pin_id), ParentNodeIdx(), AttributeRect(), Type(ImNodesAttributeType_None), + Shape(ImNodesPinShape_CircleFilled), Pos(), Flags(ImNodesAttributeFlags_None), + ColorStyle() + { + } +}; + +struct ImLinkData +{ + int Id; + int StartPinIdx, EndPinIdx; + + struct + { + ImU32 Base, Hovered, Selected; + } ColorStyle; + + ImLinkData(const int link_id) : Id(link_id), StartPinIdx(), EndPinIdx(), ColorStyle() {} +}; + +struct ImClickInteractionState +{ + ImNodesClickInteractionType Type; + + struct + { + int StartPinIdx; + ImOptionalIndex EndPinIdx; + ImNodesLinkCreationType Type; + } LinkCreation; + + struct + { + ImRect Rect; // Coordinates in grid space + } BoxSelector; + + ImClickInteractionState() : Type(ImNodesClickInteractionType_None) {} +}; + +struct ImNodesColElement +{ + ImU32 Color; + ImNodesCol Item; + + ImNodesColElement(const ImU32 c, const ImNodesCol s) : Color(c), Item(s) {} +}; + +struct ImNodesStyleVarElement +{ + ImNodesStyleVar Item; + float FloatValue[2]; + + ImNodesStyleVarElement(const ImNodesStyleVar variable, const float value) : Item(variable) + { + FloatValue[0] = value; + } + + ImNodesStyleVarElement(const ImNodesStyleVar variable, const ImVec2 value) : Item(variable) + { + FloatValue[0] = value.x; + FloatValue[1] = value.y; + } +}; + +// [SECTION] global and editor context structs + +struct ImNodesEditorContext +{ + ImObjectPool Nodes; + ImObjectPool Pins; + ImObjectPool Links; + + ImVector NodeDepthOrder; + + // ui related fields + ImVec2 Panning; + ImVec2 AutoPanningDelta; + // Minimum and maximum extents of all content in grid space. Valid after final + // ImNodes::EndNode() call. + ImRect GridContentBounds; + + ImVector SelectedNodeIndices; + ImVector SelectedLinkIndices; + + // Relative origins of selected nodes for snapping of dragged nodes + ImVector SelectedNodeOffsets; + // Offset of the primary node origin relative to the mouse cursor. + ImVec2 PrimaryNodeOffset; + + ImClickInteractionState ClickInteraction; + + // Mini-map state set by MiniMap() + + bool MiniMapEnabled; + ImNodesMiniMapLocation MiniMapLocation; + float MiniMapSizeFraction; + ImNodesMiniMapNodeHoveringCallback MiniMapNodeHoveringCallback; + ImNodesMiniMapNodeHoveringCallbackUserData MiniMapNodeHoveringCallbackUserData; + + // Mini-map state set during EndNodeEditor() call + + ImRect MiniMapRectScreenSpace; + ImRect MiniMapContentScreenSpace; + float MiniMapScaling; + + ImNodesEditorContext() + : Nodes(), Pins(), Links(), Panning(0.f, 0.f), SelectedNodeIndices(), SelectedLinkIndices(), + SelectedNodeOffsets(), PrimaryNodeOffset(0.f, 0.f), ClickInteraction(), + MiniMapEnabled(false), MiniMapSizeFraction(0.0f), MiniMapNodeHoveringCallback(NULL), + MiniMapNodeHoveringCallbackUserData(NULL), MiniMapScaling(0.0f) + { + } +}; + +struct ImNodesContext +{ + ImNodesEditorContext* DefaultEditorCtx; + ImNodesEditorContext* EditorCtx; + + // Canvas draw list and helper state + ImDrawList* CanvasDrawList; + ImGuiStorage NodeIdxToSubmissionIdx; + ImVector NodeIdxSubmissionOrder; + ImVector NodeIndicesOverlappingWithMouse; + ImVector OccludedPinIndices; + + // Canvas extents + ImVec2 CanvasOriginScreenSpace; + ImRect CanvasRectScreenSpace; + + // Debug helpers + ImNodesScope CurrentScope; + + // Configuration state + ImNodesIO Io; + ImNodesStyle Style; + ImVector ColorModifierStack; + ImVector StyleModifierStack; + ImGuiTextBuffer TextBuffer; + + int CurrentAttributeFlags; + ImVector AttributeFlagStack; + + // UI element state + int CurrentNodeIdx; + int CurrentPinIdx; + int CurrentAttributeId; + + ImOptionalIndex HoveredNodeIdx; + ImOptionalIndex HoveredLinkIdx; + ImOptionalIndex HoveredPinIdx; + + ImOptionalIndex DeletedLinkIdx; + ImOptionalIndex SnapLinkIdx; + + // Event helper state + // TODO: this should be a part of a state machine, and not a member of the global struct. + // Unclear what parts of the code this relates to. + int ImNodesUIState; + + int ActiveAttributeId; + bool ActiveAttribute; + + // ImGui::IO cache + + ImVec2 MousePos; + + bool LeftMouseClicked; + bool LeftMouseReleased; + bool AltMouseClicked; + bool LeftMouseDragging; + bool AltMouseDragging; + float AltMouseScrollDelta; + bool MultipleSelectModifier; +}; + +namespace IMNODES_NAMESPACE +{ +static inline ImNodesEditorContext& EditorContextGet() +{ + // No editor context was set! Did you forget to call ImNodes::CreateContext()? + IM_ASSERT(GImNodes->EditorCtx != NULL); + return *GImNodes->EditorCtx; +} + +// [SECTION] ObjectPool implementation + +template +static inline int ObjectPoolFind(const ImObjectPool& objects, const int id) +{ + const int index = objects.IdMap.GetInt(static_cast(id), -1); + return index; +} + +template +static inline void ObjectPoolUpdate(ImObjectPool& objects) +{ + for (int i = 0; i < objects.InUse.size(); ++i) + { + const int id = objects.Pool[i].Id; + + if (!objects.InUse[i] && objects.IdMap.GetInt(id, -1) == i) + { + objects.IdMap.SetInt(id, -1); + objects.FreeList.push_back(i); + (objects.Pool.Data + i)->~T(); + } + } +} + +template<> +inline void ObjectPoolUpdate(ImObjectPool& nodes) +{ + for (int i = 0; i < nodes.InUse.size(); ++i) + { + if (nodes.InUse[i]) + { + nodes.Pool[i].PinIndices.clear(); + } + else + { + const int id = nodes.Pool[i].Id; + + if (nodes.IdMap.GetInt(id, -1) == i) + { + // Remove node idx form depth stack the first time we detect that this idx slot is + // unused + ImVector& depth_stack = EditorContextGet().NodeDepthOrder; + const int* const elem = depth_stack.find(i); + IM_ASSERT(elem != depth_stack.end()); + depth_stack.erase(elem); + + nodes.IdMap.SetInt(id, -1); + nodes.FreeList.push_back(i); + (nodes.Pool.Data + i)->~ImNodeData(); + } + } + } +} + +template +static inline void ObjectPoolReset(ImObjectPool& objects) +{ + if (!objects.InUse.empty()) + { + memset(objects.InUse.Data, 0, objects.InUse.size_in_bytes()); + } +} + +template +static inline int ObjectPoolFindOrCreateIndex(ImObjectPool& objects, const int id) +{ + int index = objects.IdMap.GetInt(static_cast(id), -1); + + // Construct new object + if (index == -1) + { + if (objects.FreeList.empty()) + { + index = objects.Pool.size(); + IM_ASSERT(objects.Pool.size() == objects.InUse.size()); + const int new_size = objects.Pool.size() + 1; + objects.Pool.resize(new_size); + objects.InUse.resize(new_size); + } + else + { + index = objects.FreeList.back(); + objects.FreeList.pop_back(); + } + IM_PLACEMENT_NEW(objects.Pool.Data + index) T(id); + objects.IdMap.SetInt(static_cast(id), index); + } + + // Flag it as used + objects.InUse[index] = true; + + return index; +} + +template<> +inline int ObjectPoolFindOrCreateIndex(ImObjectPool& nodes, const int node_id) +{ + int node_idx = nodes.IdMap.GetInt(static_cast(node_id), -1); + + // Construct new node + if (node_idx == -1) + { + if (nodes.FreeList.empty()) + { + node_idx = nodes.Pool.size(); + IM_ASSERT(nodes.Pool.size() == nodes.InUse.size()); + const int new_size = nodes.Pool.size() + 1; + nodes.Pool.resize(new_size); + nodes.InUse.resize(new_size); + } + else + { + node_idx = nodes.FreeList.back(); + nodes.FreeList.pop_back(); + } + IM_PLACEMENT_NEW(nodes.Pool.Data + node_idx) ImNodeData(node_id); + nodes.IdMap.SetInt(static_cast(node_id), node_idx); + + ImNodesEditorContext& editor = EditorContextGet(); + editor.NodeDepthOrder.push_back(node_idx); + } + + // Flag node as used + nodes.InUse[node_idx] = true; + + return node_idx; +} + +template +static inline T& ObjectPoolFindOrCreateObject(ImObjectPool& objects, const int id) +{ + const int index = ObjectPoolFindOrCreateIndex(objects, id); + return objects.Pool[index]; +} +} // namespace IMNODES_NAMESPACE diff --git a/Lib/imnodes-master-b2ec254/vcpkg.json b/Lib/imnodes-master-b2ec254/vcpkg.json new file mode 100644 index 0000000..f16de75 --- /dev/null +++ b/Lib/imnodes-master-b2ec254/vcpkg.json @@ -0,0 +1,11 @@ +{ + "name": "imnodes", + "version-string": "0.1.0-dev", + "dependencies": [ + "sdl2", + { + "name": "imgui", + "features": [ "sdl2-binding", "opengl3-binding" ] + } + ] +} diff --git a/Makefile b/Makefile index 68d27d0..dd79e3e 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,7 @@ CXX_INCLUDES_PATHS = \ Inc \ Lib/imgui-1.92.4-docking \ Lib/imgui-1.92.4-docking/backends \ +Lib/imnodes-master-b2ec254 \ # C++源文件目录 CXX_SOURCES_PATHS = \ @@ -93,7 +94,8 @@ Lib/imgui-1.92.4-docking/imgui_tables.cpp \ Lib/imgui-1.92.4-docking/imgui_widgets.cpp \ Lib/imgui-1.92.4-docking/backends/imgui_impl_dx11.cpp \ Lib/imgui-1.92.4-docking/backends/imgui_impl_win32.cpp \ -# Lib/imgui-1.92.4-docking/examples/example_win32_directx11/main.cpp \ + \ +Lib/imnodes-master-b2ec254/imnodes.cpp \ # Windows 资源文件脚本头文件路径 WIN_RESOURCE_INCLUDES_PATHS = \ diff --git a/Src/Main.cpp b/Src/Main.cpp index fa04576..164c4b3 100644 --- a/Src/Main.cpp +++ b/Src/Main.cpp @@ -3,6 +3,9 @@ #include "imgui_impl_win32.h" #include "imgui_impl_dx11.h" +// ImNodes相关 +#include "imnodes.h" + extern "C" { // Mingw 相关库 @@ -87,6 +90,9 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // 建立 Dear ImGui 上下文 ImGui::CreateContext(); + // 建立ImNodes 上下文 + ImNodes::CreateContext(); + // 获取ImGui IO 设备配置结构体 ImGuiIO &io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls @@ -220,6 +226,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine ImGui_ImplDX11_Shutdown(); // 关闭 ImGui DX11 组件 ImGui_ImplWin32_Shutdown(); // 关闭 ImGui Win32 组件 ImGui::DestroyContext(); // 销毁 ImGui 上下文 + ImNodes::DestroyContext(); // 销毁 ImNodes 上下文 CleanupDeviceD3D(); // 清理 Direct 3D 设备 DestroyWindow(hwnd); // 销毁窗体 diff --git a/Src/UI_Layout.cpp b/Src/UI_Layout.cpp index 245aef3..e9eef50 100644 --- a/Src/UI_Layout.cpp +++ b/Src/UI_Layout.cpp @@ -24,10 +24,30 @@ void UI_Layout() // 设置窗体相关属性,不允许停靠 window_flags |= ImGuiWindowFlags_NoDocking; - ImGui::Begin("Main Panel", NULL, window_flags); + ImGui::Begin("Main Panel", NULL, window_flags); // 创建Label + ImGui::PopStyleVar(2); // 退出绘制风格栈中的设置项 { - // 退出绘制风格栈中的设置项 - ImGui::PopStyleVar(2); + + ImNodes::BeginNodeEditor(); + { + + ImNodes::BeginNode(1); + + ImNodes::BeginNodeTitleBar(); + ImGui::TextUnformatted("output node"); + ImNodes::EndNodeTitleBar(); + + ImGui::Text("Test Format %%d :%d", 123); + + ImGui::Button("Click"); + + ImGui::SameLine(); + static bool check = false; + ImGui::Checkbox("", &check); + + ImNodes::EndNode(); + } + ImNodes::EndNodeEditor(); } ImGui::End();