Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How a control keep same controlID from GetControlID() in a dynamic UI

Discussion in 'Immediate Mode GUI (IMGUI)' started by ArcherSS, Feb 27, 2020.

  1. ArcherSS

    ArcherSS

    Joined:
    Apr 7, 2018
    Posts:
    39
    Hi everybody,
    Recently I have been studying the IMGUI. From various materials, I'm told that a control must keep same controlID if they require one for every event in every frame. But I wonder how that can be done in a dynamic UI, where some controls calling GetControlID is created only at a specific condition, then its creation will definitely affect all the controlID of controls after it. For example if those controls call getStateObject with the requiring control ID, then they have to get wrong state object. So can anyone tell me how this situation should be handled or do I misunderstand something? Thanks very much in advance!
     
  2. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Hi there!

    As I understand it, control IDs do not need to remain stable through the whole lifetime of the GUI container. They only need to remain stable for every event of the same frame - like the Layout event and the Repaint event that follows it - otherwise things can break.

    What this means in practice is that if you use GUIUtility.GetControlID, you should call it during every event type, not just certain event types.
    Code (CSharp):
    1. //WRONG:
    2. if(Event.current.type == EventType.Repaint)
    3. {
    4.     int controlId = GUIUtility.GetControlID(FocusType.Passive);
    5.     GUI.DrawTexture(position, icon);
    6. }
    7.  
    8. //CORRECT:
    9. int controlId = GUIUtility.GetControlID(FocusType.Passive);
    10. if(Event.current.GetTypeForControl(controlId) == EventType.Repaint)
    11. {
    12.     GUI.DrawTexture(position, icon);
    13. }
    When you change the Layout of the GUI, e.g. in reaction to user input, you should usually call GUIUtility.ExitGUI to prevent issues occurring due to the layout changing unexpectedly between events.
    If I recall correctly, many of Unity's built-in methods like EditorGUILayout.BeginFadeGroup already handle this for you automatically, so it's not always necessary.

    The code for all IMGUI controls should be written in such a way that the control ID is requested again during every event instead of being cached, so there shouldn't be too many problems even if these IDs change in the long term. GUIUtility.keyboardControl might point to the wrong control, and GUIUtility.GetStateObject could return null, but those should not cause any breaking issues if everything has been coded properly.

    So basically whenever the GUI layout changes, you can think of it as an event that wipes the slate clean.


    Curiously, If you take a look at Unity's internal code, they almost always seem use the variant of GUIUtility.GetControlID that accepts an int "hint", and a Rect "position".
    Code (CSharp):
    1. int controlId = GUIUtility.GetControlID(EditorGUI.s_ToggleHash, FocusType.Keyboard, position);
    This might result in more unique control IDs being generated, so if the GUI changes, GUIUtility.keyboardControl etc. are more likely to point to a non-existent control instead of a valid but incorrect one. I'm not sure exactly how this is any better, but that seems to be their preferred way of handling this for what it's worth.
     
    Deadcow_ likes this.
  3. ArcherSS

    ArcherSS

    Joined:
    Apr 7, 2018
    Posts:
    39
    Hi SisusCo,
    I'm very grateful for your detailed answer. Now I think I'm clear about my question. ControlID must not need to be the same in the runtime, otherwise we can really get a stable ID and don't need to call GetControlID all the time. What really mess my mind is GUIUtility.keyboardControl, GUIUtility.hotControl, and GUIUtility.GetStateObject. They look like session things that persist cross frames. This blog introduces a example that uses the GetStateObject to implement a flash button, it takes a state object to remember the mouse down time and calculate the current button color. There is where I get confusion. If the controlId changed, the control will definitely get wrong state and break. Now I understand that IMGUI is just provide a way to work, it is not safe enough, and it needs not to be. When it comes situation that these stuff cannot work well, we must handle it by ourselves. Take the GetStateObject for example, if we want to get a stable state object, we can implement a state object map on our own and give the control a unique id that will never changed during frames. That's why IMGUI looks like so confusing. It just provides a low-level infrastructure but gives a high-level name, I think :D. And for the GUIUtility.ExitGUI, it's rather new to me, I will study how to use it and the related others. Thank you again for this awesome answer, cheers!
     
    SisusCo likes this.
  4. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Yeah, the flashing button example can "break" a little bit, but only in the specific situation that the GUI layout is changed as a direct result of the flashing button itself being clicked.

    In any other situation it wouldn't matter too much if the ID for flashing button gets changed.
    1. If FlashingButtonInfo doesn't yet exist for the ID when GUIUtility.GetStateObject is called, then a new instance is just created. So even if the control ID changes, this step doesn't break anything seriously. There could be a tiny unnoticeable jump in the animation if the button had previously been clicked.
    2. During Repaint event FlashingButtonInfo.IsFlashing is polled for the control ID. This just returns true or false based on how long it has been since the button was last clicked. If the control ID changes, again the only effect is possibly a small jump in the flashing animation.
    3. During MouseDown event the mouseDownAt time is updated inside FlashingButtonInfo. The intended result of this is to stop the button from flashing for the next 2 seconds. If the GUI layout is changed as a result of this button being clicked then this won't work (FlashingButtonInfo state is lost if the ID changes at all). If however the layout is changed by some other user interaction, it won't really matter - the button is most likely already flashing at this time anyways, and if it isn't, it probably won't be too disturbing if it starts flashing again a tiny bit sooner.
     
    Deadcow_ likes this.
  5. ArcherSS

    ArcherSS

    Joined:
    Apr 7, 2018
    Posts:
    39
    Yes, in this case the GetStateObject should not bother the flash button too much. But this also demonstrates that we need to be very careful while using IMGUI. However IMGUI now only focuses on Editor GUI and most dynamic UI controls like folder, tab are already provided by Unity, so I think I may not go such depth in practice, but only need to be familiar with common pattern usage and know these detail in case to avoid trap as much as possible. Anyway, Thanks:)!
     
    SisusCo likes this.
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    Yeah, definitely a good thing to be aware of these things about the control ID.

    A positive thing about using GetStateObject for persisting all control state across GUI events is that the default value of the type is automatically a pretty safe state to reset to. Although... I'm not really sure how frequently old cached values are cleared from GetStateObject's databases. Maybe issues could actually arise if GetStateObject returns an old state from way back :confused:

    So yeah, complex state machine type logic...maybe not the best idea when using control ID based state persisting :p
    IMGUI can be pretty weird.

    A great example of doing it wrong is the internal implementation of EditorGUI.DoColorField.
    1. Static variable EditorGUI.s_ColorPickID is set to match control ID if the eyedropper is clicked.
    2. While EditorGUI.s_ColorPickID equals control ID the color swatch is drawn (shows color of pixel currently under the cursor).
    3. The only way that EditorGUI.s_ColorPickID is reset back to 0 is if GUIUtility.keyboardControl equals control ID and a specific event EyeDropperClicked or EyeDropperCancelled is detected.
    So if for example GUIUtility.keyboardControl should get changed for whatever reason by anything, the color field can get stuck drawing the color swatch indefinitely...

    This actually caused a pretty hard to solve bug in one of my assets, because I was resetting GUIUtility.keyboardControl to 0 whenever the EditorWindow losed focus - which of course always occurred whenever the eyedropped was clicked :D
     
  7. el_trex

    el_trex

    Joined:
    Apr 13, 2016
    Posts:
    22
    Hi I hijack a little this thread to prevent creating a new one as it is related.
    I currently trying understand ControlID and State Object.
    I though the Control ID was unique thought the whole life of the control related to, but As I read you here it doesn't seems so ?
    And for the State Object can't really figured it out what is that really.
    Naively I though a
    Code (CSharp):
    1. var control = GUIUtility.GetStateObject(typeof(MYTYPE), ControlID) as MYTYPE;
    Will return the control object being draw for example. The "State" keyword here make me feel I may have misunderstood it.
     
  8. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,297
    As long as the controls inside your GUI do not change the ids returned by GUIUtility.GetControlID will remain the same.
    Only if you dynamically show or hide some controls in your GUI, then the ids for all controls that follow it can be affected.

    So the more often your GUI changes, the shorter the duration that your ids will remain the same becomes.

    1. The first time you call GUIUtility.GetStateObject with a specific class type and id, it creates an instance of said class and caches it behind the given id. You can pass any class type you want here.
    2. On subsequent calls with the same parameters GUIUtility.GetStateObject will return this same previously created instance.

    Because this "state object" persists outside of the scope of your IMGUI drawing method, you can use it to have any data you would like persist between subsequent calls to the method.

    For example lets say you wanted to create a custom button that changes to a pressed state when the mouse button is down if it was pressed down over the button. You can use GUIUtility.GetStateObject to help you do this.

    First you'd need to create a class for holding information about whether or not the mouse was pressed down over the control:
    Code (CSharp):
    1. public class ButtonState
    2. {
    3.     public bool mouseDownOverControl;
    4. }
    Then you can implement the functionality like this:
    Code (CSharp):
    1.     int controlID = GUIUtility.GetControlID(FocusType.Passive, position);
    2.     var buttonState = (ButtonState)GUIUtility.GetStateObject(typeof(ButtonState), ControlID);
    3.     switch(Event.current.type)
    4.     {
    5.         case EventType.MouseDown:
    6.             buttonState.mouseDownOverControl = position.Contains(Event.current.position);
    7.             break;
    8.         case EventType.MouseUp:
    9.             if(buttonState.mouseDownOverControl)
    10.             {
    11.                 if(position.Contains(Event.current.position))
    12.                 {
    13.                     OnButtonClicked();
    14.                 }
    15.                 buttonState.mouseDownOverControl = false;
    16.             }
    17.             break;
    18.         case EventType.Repaint:
    19.             if(buttonState.mouseDownOverControl)
    20.             {
    21.                 GUI.DrawTexture(position, pressedDownIcon);
    22.             }
    23.             else
    24.             {
    25.                 GUI.DrawTexture(position, idleIcon);
    26.             }
    27.             break;
    28.     }
     
    Deadcow_ likes this.