Skip to content

Instantly share code, notes, and snippets.

@MattRix
Created October 27, 2019 23:18
Show Gist options
  • Save MattRix/564fa9c36c511ce9ec2b8f5c84022a97 to your computer and use it in GitHub Desktop.
Save MattRix/564fa9c36c511ce9ec2b8f5c84022a97 to your computer and use it in GitHub Desktop.
EditorZoomer - an easy way to do panning and zooming inside Unity Editor IMGUI
using UnityEngine;
using System.Collections;
using System;
//based on the code in this post: http://martinecker.com/martincodes/unity-editor-window-zooming/
//but I changed how the API works and made it much more flexible
//usage: create an EditorZoomer instance wherever you want to use it (it tracks the pan + zoom state)
//in your OnGUI, draw your scrollable content between zoomer.Begin() and zoomer.End();
//you also must offset your content by zoomer.GetContentOffset();
public class EditorZoomer
{
private const float kEditorWindowTabHeight = 21.0f;
public float zoom = 1f;
public Rect zoomArea = new Rect();
public Vector2 zoomOrigin = Vector2.zero;
Vector2 lastMouse = Vector2.zero;
Matrix4x4 prevMatrix;
public Rect Begin(params GUILayoutOption[] options)
{
HandleEvents();
//fill the available area
var possibleZoomArea = GUILayoutUtility.GetRect(0, 10000, 0, 10000, options);
if (Event.current.type == EventType.Repaint) //the size is correct during repaint, during layout it's 1,1
{
zoomArea = possibleZoomArea;
}
GUI.EndGroup(); // End the group Unity begins automatically for an EditorWindow to clip out the window tab. This allows us to draw outside of the size of the EditorWindow.
Rect clippedArea = zoomArea.ScaleSizeBy(1f/zoom, zoomArea.TopLeft());
clippedArea.y += kEditorWindowTabHeight;
GUI.BeginGroup(clippedArea);
prevMatrix = GUI.matrix;
Matrix4x4 translation = Matrix4x4.TRS(clippedArea.TopLeft(), Quaternion.identity, Vector3.one);
Matrix4x4 scale = Matrix4x4.Scale(new Vector3(zoom, zoom, 1.0f));
GUI.matrix = translation * scale * translation.inverse * GUI.matrix;
return clippedArea;
}
public void End()
{
GUI.matrix = prevMatrix; //restore the original matrix
GUI.EndGroup();
GUI.BeginGroup(new Rect(0.0f, kEditorWindowTabHeight, Screen.width, Screen.height));
}
public void HandleEvents()
{
if (Event.current.isMouse)
{
if (Event.current.type == EventType.MouseDrag && ((Event.current.button == 0 && Event.current.modifiers == EventModifiers.Alt) || Event.current.button == 2))
{
var mouseDelta = Event.current.mousePosition - lastMouse;
zoomOrigin += mouseDelta;
Event.current.Use();
}
lastMouse = Event.current.mousePosition;
}
if (Event.current.type == EventType.ScrollWheel)
{
float oldZoom = zoom;
float zoomChange = 1.10f;
zoom *= Mathf.Pow(zoomChange, -Event.current.delta.y / 3f);
zoom = Mathf.Clamp(zoom,0.1f, 10f);
bool shouldZoomTowardsMouse = true; //if this is false, it will always zoom towards the center of the content (0,0)
if (shouldZoomTowardsMouse)
{
//we want the same content that was under the mouse pre-zoom to be there post-zoom as well
//in other words, the content's position *relative to the mouse* should not change
Vector2 areaMousePos = Event.current.mousePosition - zoomArea.center;
Vector2 contentOldMousePos = (areaMousePos/oldZoom) - (zoomOrigin/oldZoom);
Vector2 contentMousePos = (areaMousePos/zoom) - (zoomOrigin/zoom);
Vector2 mouseDelta = contentMousePos - contentOldMousePos;
zoomOrigin += mouseDelta * zoom;
}
Event.current.Use();
}
}
public Vector2 GetContentOffset()
{
Vector2 offset = -zoomOrigin / zoom; //offset the midpoint
offset -= (zoomArea.size / 2f) / zoom; //offset the center
return offset;
}
}
// Helper Rect extension methods
public static class RectExtensions
{
public static Vector2 TopLeft(this Rect rect)
{
return new Vector2(rect.xMin, rect.yMin);
}
public static Rect ScaleSizeBy(this Rect rect, float scale)
{
return rect.ScaleSizeBy(scale, rect.center);
}
public static Rect ScaleSizeBy(this Rect rect, float scale, Vector2 pivotPoint)
{
Rect result = rect;
result.x -= pivotPoint.x;
result.y -= pivotPoint.y;
result.xMin *= scale;
result.xMax *= scale;
result.yMin *= scale;
result.yMax *= scale;
result.x += pivotPoint.x;
result.y += pivotPoint.y;
return result;
}
public static Rect ScaleSizeBy(this Rect rect, Vector2 scale)
{
return rect.ScaleSizeBy(scale, rect.center);
}
public static Rect ScaleSizeBy(this Rect rect, Vector2 scale, Vector2 pivotPoint)
{
Rect result = rect;
result.x -= pivotPoint.x;
result.y -= pivotPoint.y;
result.xMin *= scale.x;
result.xMax *= scale.x;
result.yMin *= scale.y;
result.yMax *= scale.y;
result.x += pivotPoint.x;
result.y += pivotPoint.y;
return result;
}
}
@longtran2904
Copy link

longtran2904 commented Feb 27, 2021

@MattRix The GetContentOffset() is for panning the content around, right? I have some nodes which each have a rect and are drawn by GUI.Box(). So should I do something like node.rect.position += GetContentOffset(). If I don't do that then zooming works okay and I can't pan around, but if I do that then zooming and panning will offset my content a lot. Also when I said that it didn't work above it mean that it offset all my content a lot. Can you explain more about the function (maybe give some real examples)?

@MattRix
Copy link
Author

MattRix commented Mar 1, 2021

Is it possible you are accumulating the position offsets? GetContentOffset() is the total offset from the center, not a per-frame delta. In other words, your nodes should be resetting their positions before getting the += GetContentOffset().

I went to one of my older projects and uploaded an editor window where I use the zoomer: https://gist.github.com/MattRix/bfeb6f23b5cd6edfe93cee66f163993f

This will show you how I use the zoomer in my own projects, but note that you won't be able to run it as is, since it has a bunch of project specific stuff in it (ex. all the stuff about "demands").

Also worth noting that this example code uses a force-directed graph to automatically spread out the nodes.

If you're trying to make a node-graph editor for programming (or something similar), I recommend checking out this tutorial on how to do it using Unity's UIElements/UIToolkit framework, which has built-in helpers (called manipulators) to do panning and zooming etc: https://youtu.be/7KHGH0fPL84

@longtran2904
Copy link

@MattRix What do you mean when you say I should reset my node's position? My node-based graph is something like the animator window, each node has its own position and you can drag it around to change it. I've just looked into your example and seen you had a midPoint position which you divided from each node's position. I don't know if I should do something like that because you were making a force-directed one.

@MattRix
Copy link
Author

MattRix commented Mar 2, 2021

You don’t need to do the midpoint thing, but if you’re just adding the GetContentOffset() to your node positions every frame then they will fly away. GetContentOffset() is the absolute offset from the origin, not the delta change every frame.

@longtran2904
Copy link

longtran2904 commented Mar 7, 2021

  1. So the offset is the distance between the center of the screen to the origin, right? Because if I do nothing then the GetContentOffset() will return -position.size.
  2. Can you explain how to make a function to return the delta?
  3. In your script, you had a lastMouse to calculate the delta between frames. Why didn't you use Event.delta? I've changed it to Event.delta, will it have any bugs?

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