Search Unity

Question Faking mouse click behavior in OnGUI via Event.current

Discussion in 'Immediate Mode GUI (IMGUI)' started by jasonboukheir, Jul 22, 2020.

  1. jasonboukheir

    jasonboukheir

    Joined:
    May 3, 2017
    Posts:
    84
    I'm writing an EditorGUI field for my custom serialized field, so I'm trying to mimic that API.

    Here's an example of what I'm trying to do:
    Code (CSharp):
    1. public static Rect MyField(Rect position)
    2. {
    3.     var evt = Event.current;
    4.     if (evt.type == EventType.MouseDown)
    5.         Debug.Log("MyField clicked!");
    6. }
    I'm trying to test it, with a
    TestEditorWindow
    and onGui callbacks:
    Code (CSharp):
    1. public class TestEditorWindow : EditorWindow
    2. {
    3.     public Action onGui;
    4.  
    5.     void OnGUI() => onGui?.Invoke();
    6. }
    Code (CSharp):
    1. [UnityTest]
    2. public IEnumerator TestMyField()
    3. {
    4.     bool onGuiCalled = false;
    5.     var position = new Rect(x: 0, y: 0, width: 150, height: 30);
    6.     void OnGUI()
    7.     {
    8.         var evt = new Event() { type = EventType.MouseDown, clickCount = 1, mousePosition = position.center };
    9.         Event.current = evt;
    10.         MyField(position);
    11.         onGuiCalled = true;
    12.     }
    13.     var testWindow = EditorWindow.GetWindow<TestEditorWindow>();
    14.     testWindow.onGui = OnGUI;
    15.     yield return new WaitUntil(() => onGuiCalled);
    16.     // Assert for MyField behavior here...
    17. }
    The issue I run into is that the
    EventType
    is changed to
    EventType.Ignored
    . I realize this is probably pretty poor testing behavior because the Layout and Repaint current events are overwritten.

    I was thinking there was some behavior in
    Event.current
    that was validating the set event, so I tried changing my custom field to look like
    Code (CSharp):
    1. public static Rect MyField(Rect position, Event customEvent = null)
    2. {
    3.     var evt = customEvent ?? Event.current;
    4.     if (evt.type == EventType.MouseDown)
    5.         Debug.Log("MyField clicked!");
    6. }
    And I changed the
    OnGUI 
    method in my test to look like:
    Code (CSharp):
    1. void OnGUI()
    2. {
    3.     var evt = new Event() { type = EventType.MouseDown, clickCount = 1, mousePosition = position.center };
    4.     MyField(position, evt);
    5.     onGuiCalled = true;
    6. }
    But somehow, the event type is still changed to
    EventType.Ignored
    . I tried debugging the creation of the event, and the event type is changed when it's passed to
    MyField
    . I have no clue how this is happening.

    After this, I was thinking that I probably shouldn't be trying to set the current event while the
    OnGUI
    method is being called. I reverted the changes I made earlier, and tried the following:
    Code (CSharp):
    1. [UnityTest]
    2. public IEnumerator TestMyField()
    3. {
    4.     bool onGuiCalled = false;
    5.     var position = new Rect(x: 0, y: 0, width: 150, height: 30);
    6.     void OnGUI()
    7.     {
    8.         Event.current = evt;
    9.         MyField(position);
    10.         onGuiCalled = true;
    11.     }
    12.     var testWindow = EditorWindow.GetWindow<TestEditorWindow>();
    13.     testWindow.onGui = OnGUI;
    14.     var evt = new Event() { type = EventType.MouseDown, clickCount = 1, mousePosition = position.center };
    15.     Event.current = evt;
    16.     yield return new WaitUntil(() => onGuiCalled);
    17.     // Assert for MyField behavior here...
    18. }
    However, the
    OnGUI
    method never received the set
    Event.current
    . No amount of
    yield return null
    and combinations of
    WaitUntil
    could get the
    OnGUI
    method to receive the event.

    So I tried digging through the API one more time, and stumbled upon a method,
    EditorWindow.SendEvent
    . It looked promising--perhaps this was the way I could invoke an event in an editor window.
    Code (CSharp):
    1. [UnityTest]
    2. public IEnumerator TestMyField()
    3. {
    4.     bool onGuiCalled = false;
    5.     var position = new Rect(x: 0, y: 0, width: 150, height: 30);
    6.     void OnGUI()
    7.     {
    8.         Event.current = evt;
    9.         MyField(position);
    10.         onGuiCalled = true;
    11.     }
    12.     var testWindow = EditorWindow.GetWindow<TestEditorWindow>();
    13.     testWindow.onGui = OnGUI;
    14.     var evt = new Event() { type = EventType.MouseDown, clickCount = 1, mousePosition = position.center };
    15.     testWindow.SendEvent(evt);
    16.     yield return new WaitUntil(() => onGuiCalled);
    17.     // Assert for MyField behavior here...
    18. }
    Nope! Somehow,
    EditorWindow.SendEvent
    automatically sets the
    EventType
    to
    EventType.Used
    .

    I've been bashing my head at this problem for a couple hours now, and I need some help.
    1. How the heck am I supposed to mock a simple MouseDownEvent at a position in IMGUI?
    2. What's causing my mouse down events set to
      Event.current
      to become ignored?
    3. What magical being is causing the mouse down events passed to
      MyField
      to become ignored when they're of type MouseDown after creation, and I don't touch them in my editor script?
    Sincerely, a Unity Dev with hopes of 100% Code Coverage.
     
  2. jasonboukheir

    jasonboukheir

    Joined:
    May 3, 2017
    Posts:
    84
    To anyone wondering how I did it, I ended up doing the following:

    Created my own
    GUIEvent
    valuetype:
    Code (CSharp):
    1. public struct GUIEvent
    2. {
    3.     public EventType Type { get; }
    4.  
    5.     public GUIEvent(EventType type)
    6.     {
    7.         Type = type;
    8.     }
    9. }
    Added an overload
    MyField
    method:
    Code (CSharp):
    1. public static Rect MyField(Rect position)
    2. {
    3.     var evt = Event.current;
    4.     return MyField(position, new GUIEvent(evt.type));
    5. }
    6.  
    7. public static Rect MyField(Rect position, GUIEvent guiEvent)
    8. {
    9.     if (guiEvent.Type == EventType.MouseDown)
    10.         Debug.Log("MyField clicked!");
    11.     return position;
    12. }
    This gives me full control over the events I pass in. I still used the TestClass, but instead of calling
    MyField(Rect position)
    , I called the overload
    MyField(Rect position, GUIEvent guiEvent)
    . I have a separate test that tests the functionality of mapping
    Event.current
    to a
    GUIEvent
    . 100% Code Coverage is achievable!

    I mean... code coverage has its faults, but it's good to be able to know how to test all the things ;).