corrected GUI.Button code (works properly with layered controls)

As discussed in this thread, and also on Unity Answers, there is a major problem with things like pop-up menus when they happen to overlap GUI buttons.

After some investigating, we’ve found it to be a flaw in the GUI.Button method: it’s not properly honoring the GUIUtility.hotControl indicator. When that’s not set to either 0 or the button’s own ID, it should ignore mouse events completely. Instead, while it does refrain from drawing in mouse-over style, it nonetheless handles a mouseDown event that was intended for some other control.

We banged out a replacement function that appears to work correctly in our tests. Here it is – just put it someplace convenient in your code, and replace your calls to GUI.Button with calls to goodButton:

	bool goodButton(Rect bounds, string caption) {
		GUIStyle btnStyle = GUI.skin.FindStyle("button");
		int controlID = GUIUtility.GetControlID(bounds.GetHashCode(), FocusType.Passive);
		
		bool isMouseOver = bounds.Contains(Event.current.mousePosition);
		bool isDown = GUIUtility.hotControl == controlID;

		if (GUIUtility.hotControl != 0  !isDown) {
			// ignore mouse while some other control has it
			// (this is the key bit that GUI.Button appears to be missing)
			isMouseOver = false;
		}
		
		if (Event.current.type == EventType.Repaint) {
			btnStyle.Draw(bounds, new GUIContent(caption), isMouseOver, isDown, false, false);
		}
		switch (Event.current.GetTypeForControl(controlID)) {
			case EventType.mouseDown:
				if (isMouseOver) {	// (note: isMouseOver will be false when another control is hot)
					GUIUtility.hotControl = controlID;
				}
				break;
                
			case EventType.mouseUp:
				if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0;
				if (isMouseOver  bounds.Contains(Event.current.mousePosition)) return true;
				break;
		}

		return false;
	}

Note that, contrary to claims elsewhere, you do not need to put your overlapping controls into separate MonoBehaviours. You just need a button function that’s not broken. :wink:

Enjoy,

  • Joe

Is it possible to get a JavaScript version?
The Bounds parameter in the

is a little difficult to work out.

TIA.

the JS version / not thoroughly tested , just replaced declaration at the beginning… /:

function goodButton(bounds: Rect, caption: String): boolean
	{
		var btnStyle : GUIStyle = GUI.skin.FindStyle("button");
		var controlID : int = GUIUtility.GetControlID(bounds.GetHashCode(), FocusType.Passive);
		
		var isMouseOver : boolean = bounds.Contains(Event.current.mousePosition);
		var isDown : boolean = GUIUtility.hotControl == controlID;

		if (GUIUtility.hotControl != 0  !isDown) {
			// ignore mouse while some other control has it
			// (this is the key bit that GUI.Button appears to be missing)
			isMouseOver = false;
		}
		
		if (Event.current.type == EventType.Repaint) {
			btnStyle.Draw(bounds, new GUIContent(caption), isMouseOver, isDown, false, false);
		}
		switch (Event.current.GetTypeForControl(controlID)) {
			case EventType.mouseDown:
				if (isMouseOver) {	// (note: isMouseOver will be false when another control is hot)
					GUIUtility.hotControl = controlID;
				}
				break;
                
			case EventType.mouseUp:
				if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0;
				if (isMouseOver  bounds.Contains(Event.current.mousePosition)) return true;
				break;
		}

		return false;
	}

btw thanks JoeStrout !

I’ve been layouting my controls for a while until I realized that the button behavior is probably not exactly as intended :slight_smile:

i’ve been annoyed by this button overlapping problem until i see your post, thank you so much for sharing this solution!

I don’t understand. For me it does the exact same problem. I used the Javascript version translated by r618. Did I missed something ?

It didn’t work for me either at first, my problem was that i was using GUI.Button() for one of the buttons and the code mentioned above for the other. Make sure you use the code above for all buttons actually overlapping.

Thank you OP for the solution, was really helpful!

Wow, that’s a strange one, because I solved the problem in using both methods : The JoeStrout Button under the GUIButton. When I used the JoeStrout for all, it didn’t work.

i dont think the problem has anything to do with the hotControl, the solution you presented does not call Event.current.Use() which the normal button calls after setting the hotControl to its control Id. Other controls ignores EventType.Used, so you wont get any events processed if the one control sets the “MouseDown” event to “Used”.

Great work, Joe. - Thanks for sharing this! You’ve made my day!

Okay guys, even though the code is great, i’ve still had some problems with it: when using overlapping buttons, it would fire button events for EVERY button. Obviously, i only want it to fire an event for the topmost button. Therefore i have come up with the following solution. It is based on Joe’s script and solves the problem of overlapping buttons (both: inside the same GUI.depth or with diferent GUI.depths). It also works with scrollviews (e.g. i have a main menu button, it’s description can be expanded and then is overlapping a scrollview, which also has buttons). It will fire the event only to the topmost button.

My first approach was to collect all button rect’s and their rollover states during the Event.current.Layout process, and then i will know which button is really the topmost. (This is possible because the button routine is always called TWICE per frame as probably everyone of you know …)
With this i had a solution for button clicks, but when there were overlapping buttons inside the same GUI.depth level, still all my buttons were hovering, including the ones “behind” another button. - Therefore i’ve added a “highest control ID” for rollovers, and this finally simplified the whole script and doesn’t need any hashtables anymore. The “highest control ID” is defined by using GUI.depth in combination with the controlID, and only hovered buttons will update it. At the end of the whole Event.current.Layout process, you will have processed all buttons and therefore know the definitive topmost hovered button. When the process status changes (i.e. the Event.current.type changes from EventType.Repaint back to EventType.Layout, a new frame has started and the highest control ID is reset to zero. - This will make sure that the routine starts collecting all button IDs again, so it will also by working for dynamic user interfaces where buttons are added or removed over time …

If you are using GUI.depth, make sure you’re only using values between 0 and 1000. (or, adjust the values in the script).

I have also added code for mobile devices, so the button doesn’t fire when he’s being dragged over. This way it is working nicely for me around and inside scrollViews etc.

This is what i came up with:

/* 
 * 
 * **** GUIButton CLASS ****
 * 
 * this versions sends only events to the topmost button ...
 * 
 * 
 * Fixes the bugs from the original GUI.Button function
 * Based on the script from Joe Strout: 
 * http://forum.unity3d.com/threads/96563-corrected-GUI.Button-code-%28works-properly-with-layered-controls%29?p=629284#post629284
 * 
 * 
 * The difference in this script is that it will only fire events (click and rollover!)
 * for the topmost button when using overlapping buttons inside the same GUI.depth! 
 * Therefore the script finds the topmost button during the layout process, so it 
 * can decide which button REALLY has been clicked.
 * 
 * Benefits:
 * 1. The script will only hover the topmost button!
 *    (doesn't matter wheter the topmost button is defined via GUI.depth or via drawing order!)
 * 2. The script will only send events to the topmost button (as opposed to Joe's original script)
 * 3. The script works for overlapping buttons inside same GUI.depth levels,
 *    as well as for overlapping buttons using different GUI.depth values
 * 4. The script also works when overlapping buttons over buttons inside scrollviews, etc.
 * 
 * Usage:  just like GUI.Button() ... for example:
 * 
 * 	if ( GUIButton.Button(new Rect(0,0,100,100), "button_action", GUI.skin.customStyles[0]) )
 *	{
 * 		Debug.Log( "Button clicked ..." );
 *	}
 *
 * 
 *
 * Original script (c) by Joe Strout!
 * 
 * Code changes:
 * Copyright (c) 2012 by Frank Baumgartner, Baumgartner New Media GmbH, fb@b-nm.at
 *
 * 
 * */


using UnityEngine;
using System.Collections;

public class GUIButton 
{
	private static int highestDepthID = 0;
	private static Vector2 touchBeganPosition = Vector2.zero;
	private static EventType lastEventType = EventType.Layout;

	private static bool wasDragging = false;
	
	private static int frame = 0;
	private static int lastEventFrame = 0;
	
	
	public static bool Button(Rect bounds, string caption, GUIStyle btnStyle = null ) 
	{
		int controlID = GUIUtility.GetControlID(bounds.GetHashCode(), FocusType.Passive);
		bool isMouseOver = bounds.Contains(Event.current.mousePosition);
		int depth = (1000 - GUI.depth) * 1000 + controlID;
		if ( isMouseOver  depth > highestDepthID ) highestDepthID = depth;
		bool isTopmostMouseOver = (highestDepthID == depth);
#if (UNITY_IPHONE || UNITY_ANDROID)   !UNITY_EDITOR
		bool paintMouseOver = isTopmostMouseOver  (Input.touchCount > 0);
#else
		bool paintMouseOver = isTopmostMouseOver;
#endif

		if ( btnStyle == null ) 
		{
			btnStyle = GUI.skin.FindStyle("button");
		}
			
		if ( Event.current.type == EventType.Layout  lastEventType != EventType.Layout )
		{
			highestDepthID = 0;
			frame++;
		}
		lastEventType = Event.current.type;
	
		if ( Event.current.type == EventType.Repaint ) 
		{
	        bool isDown = (GUIUtility.hotControl == controlID);
            btnStyle.Draw(bounds, new GUIContent(caption), paintMouseOver, isDown, false, false);			
			
        }
		
#if (UNITY_IPHONE || UNITY_ANDROID) 
		
		if ( Input.touchCount > 0 )
		{
	    	Touch touch = Input.GetTouch(0);
	        if ( touch.phase == TouchPhase.Began )
	        {
	            touchBeganPosition = touch.position;
				wasDragging = true;
			}
	        else if ( touch.phase == TouchPhase.Ended  
	                    	(   (Mathf.Abs(touch.position.x - touchBeganPosition.x) > 15) || 
	                        	(Mathf.Abs(touch.position.y - touchBeganPosition.y) > 15)       )
	                )
	        {
	        	wasDragging = true;
	        }
			else
			{
				wasDragging = false;
			}
	  	}
		else if ( Event.current.type == EventType.Repaint )
		{
			wasDragging = false;
		}
		
#endif
		
		// Workaround:
		// ignore duplicate mouseUp events. These can occur when running
		// unity editor with unity remote on iOS ... (anybody knows WHY?)
		if ( frame <= (1+lastEventFrame) ) return false;

        switch ( Event.current.GetTypeForControl(controlID) ) 
		{
            case EventType.mouseDown:
			{ 
				if ( isTopmostMouseOver  !wasDragging )
				{
					GUIUtility.hotControl = controlID;
				}
                break;
			}

			case EventType.mouseUp:
			{
				if ( isTopmostMouseOver  !wasDragging )
				{
					GUIUtility.hotControl = 0;
					lastEventFrame = frame;
					return true;
				}
	            break;
	        }
		}
        return false;
    }
}

Hello,

I need this for a GUI.TextField control. I’ve spent many hours trying to get it to work but couldn’t make it to work. It’s possible to do make it to work for a TextField ? If yes, could someone point me on the right direction please ?

bedford: try using GUI.depth and place the textfield into a separate GameObject (you need different gameobjects in order to make GUI.depth work)

This is Solution.

using UnityEngine;
using System.Collections;

public class GUIButton {
	
	private static int highestDepthID = 0;
	private static int targetID;
	
	
	static public bool Button(Rect bounds, string caption, GUIStyle btnStyle)
	{
		int controlID = GUIUtility.GetControlID(bounds.GetHashCode(), FocusType.Passive);
		int depth = (1000 - GUI.depth) * 1000 + controlID;
		
		
		bool result = false;
		
		if(Event.current.type == EventType.Layout)
		{
			bool isMouseOver = bounds.Contains(Event.current.mousePosition);
        	if ( isMouseOver  depth > highestDepthID ) { highestDepthID = depth;  targetID = controlID; }
		}
		else if(Event.current.type == EventType.Repaint)
		{
			result = (GUIUtility.hotControl == controlID);
			
			btnStyle.Draw(bounds, new GUIContent(caption), (targetID == controlID), result, false, false);
			if(targetID == controlID) { highestDepthID = 0; targetID = 0; } 
		}
		
		switch (Event.current.GetTypeForControl(controlID)) {
            case EventType.mouseDown:
                if (targetID == controlID)  GUIUtility.hotControl = controlID;

                break;
            case EventType.mouseUp:

                if (GUIUtility.hotControl == controlID) GUIUtility.hotControl = 0;
				return (targetID == controlID);
                break;
        }

		return false;
	}
}
1 Like

Nice Useful Stuff , but i still do have some issue… if a create a button and another inside it. the isMouseOver boolean returns true for both the buttons…how do i stop it…and how do i chnage the texture if isMouseOver is true and vice versa…thanks in advance

I know this is an old thread, but in case it helps anyone… The overlap issue was only a problem when using my custom-made pop-up menus, i.e., they’re the only buttons that might overlap other buttons, so my solution was to use a custom Button method, shown here, that checked whether the popup menu was open, and if so, just draw the button using its style instead of actually calling GUI.Button.

I use this custom method for buttons everywhere except in my menu drawing routines, which make the regular GUI.Button calls because they should never draw the “fake” buttons. (Note: Menu.IsAnyOpen is a static method on my Menu class, not shown, that indicates whether a popup menu is open.)

        /// <summary>
        /// Draws a button, or just the appearance of a button if a menu is open.
        /// </summary>
        /// <param name="bounds">bounds</param>
        /// <param name="content">content</param>
        /// <param name="style">style</param>
        /// <returns>whether the user clicked the button</returns>
        public static bool Button(Rect bounds, GUIContent content, GUIStyle style = null)
        {
            if (style == null) style = Skin.button;
            if (Menu.IsAnyOpen)
            {
                if (Event.current.type == EventType.repaint) style.Draw(bounds, content, false, false, false, false);
                return false;
            }
            else return GUI.Button(bounds, content, style);
        }

Great, thanks! Works like a charm!

(I renamed the class “GUIButton” into “CitizensForABetterHandlingOfOverlappingButtons” though. Sounds better, methinks.)

Thank you for sharing this solution. Here, it works when using the GoodButton on both buttons.

Now I am having the same overlapping problem when having a button over a SelectionGrid. Even when using the GoodButton.

Is there a way to have the same solution but for SelectionGrid? A replacement GoodSelectionGrid, please.

Thank you

7 years later and it’s still an issue… :face_with_spiral_eyes:

If anyone wants to try the code by franky303 use the one posted in this thread instead: 2 Buttons overlap problem - Questions & Answers - Unity Discussions

Somehow the code he posted here is missing several operators.

1 Like

Still no solution? I’m having nightmares with a notice popup that naturally pops up op top of the rest of the UI when something is wrong, and the buttons underneath it are clicked when the user clicks to dismiss it.

None of the solutions here worked for me in Unity 2019, so after a bit of tinkering with some of the examples here, I found what works for me. Hope it helps somebody else too.

public class GUIButton
{
    static int highestDepthID = 0;
    static int targetID;
    static Vector2 lastPosition;

    static public bool Button( Rect bounds, string caption, GUIStyle btnStyle )
    {
        int controlID = GUIUtility.GetControlID( bounds.GetHashCode(), FocusType.Passive );
        int depth = (1000 - UnityEngine.GUI.depth) * 1000 + controlID;

        // Mouse moved, reset depthID
        var pos = Event.current.mousePosition;
        if( lastPosition != pos ) {
            highestDepthID = int.MinValue;
            targetID = int.MinValue;
            lastPosition = pos;
        }

        bool isMouseOver = bounds.Contains( pos );
        if( isMouseOver ) {
            if( depth > highestDepthID ) {
                highestDepthID = depth;
                targetID = controlID;
            }
        }

        if( Event.current.type == EventType.Repaint ) {
            var result = GUIUtility.hotControl == controlID;
            btnStyle.Draw( bounds, new GUIContent( caption ), targetID == controlID, result, false, false );
        }

        switch( Event.current.GetTypeForControl( controlID ) )
        {
            case EventType.MouseDown:
                if( targetID == controlID ) GUIUtility.hotControl = controlID;
                break;

            case EventType.MouseUp:
                if( GUIUtility.hotControl == controlID ) GUIUtility.hotControl = 0;
                return targetID == controlID;
        }

        return false;
    }
}