Skip to content

Instantly share code, notes, and snippets.

@MattRix
Created October 27, 2019 23:18
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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;
}
}
@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