Basic window manager: Z-order ping-pong between overlapping windows

26 Views Asked by At

For Unity, I implemented a basic window manager with Z-order.

It works well until I restrict drag region to window title bar (using GUI.DragWindow).

What happens is that, say I click where B and C windows are:

  • 1 click, B comes first
  • 1 click, C comes first
  • cycle repeats forever

When I invoke GUI.DrawWindow and use full rect, i.e. drag/move from anywhere, bug vanishes.

But when I restrict the drag/move to only window title bar, bug appears.

Although I know the cause, I'm unable to figure out how to fix it...

enter image description here

Code:

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using UnityEngine;

public sealed class Windows : MonoBehaviour
{
    [SerializeField]
    private WindowManager Manager = new();

    private void OnEnable()
    {
        if (Manager.Windows.Count == 0)
        {
            Manager.GetNewWindow("A").Background = Color.cyan;
            Manager.GetNewWindow("B").Background = Color.magenta;
            Manager.GetNewWindow("C").Background = Color.yellow;
            Manager.GetNewWindow("D").Background = Color.white;
            Manager.GetNewWindow("E").Background = Color.black;
        }

        Manager.Initialize(s =>
        {
            return s.Title switch
            {
                "A" => DrawWindowA,
                "B" => DrawWindowB,
                "C" => DrawWindowC,
                "D" => DrawWindowD,
                "E" => DrawWindowE,
                _   => throw new ArgumentOutOfRangeException(nameof(s))
            };
        });
    }

    private void OnGUI()
    {
        GUIUtility.ScaleAroundPivot(Vector2.one * Manager.Scale, Vector2.zero);

        GUILayout.Label(Manager.LastActive.ToString());

        Manager.Draw();
    }

    private void DrawWindowA(int id)
    {
        GUILayout.Label("A");
    }

    private void DrawWindowB(int id)
    {
        GUILayout.Label("B");
    }

    private void DrawWindowC(int id)
    {
        GUILayout.Label("C");
    }

    private void DrawWindowD(int id)
    {
        GUILayout.Label("D");
    }

    private void DrawWindowE(int id)
    {
        GUILayout.Label("E");
    }
}


[Serializable]
public sealed class WindowManager
{
    public Window LastActive;

    public List<Window> Windows = new();

    public List<Window> WindowsZOrder = new();

    public float Scale => Screen.dpi / 96.0f;

    public Window GetNewWindow(string title)
    {
        var window = new Window(Windows.Count + 1, title);

        Windows.Add(window);

        WindowsZOrder.Add(window);

        return window;
    }

    public void Initialize(Func<Window, GUI.WindowFunction> windowFunctionGetter)
    {
        foreach (var window in Windows)
        {
            window.Function = windowFunctionGetter(window);
        }
    }

    public void Draw()
    {
        foreach (var window in Windows.OrderBy(s => WindowsZOrder.IndexOf(s)))
        {
            if (window.Visible)
            {
                Draw(window);
            }
        }
    }

    public void Draw(Window window)
    {
        DrawUpdate(window);

        using var _ = new GUIColorScope(background: window.Background);

        var rect = GUI.Window(window.Id, window.Rect, DrawWindow, window.Title);

        var max = new Vector2(Screen.width, Screen.height) / (Screen.dpi / 96.0f) - rect.size;

        rect.position = Vector2.Min(Vector2.Max(rect.position, Vector2.zero), max);

        window.Rect = rect;

        return;

        void DrawWindow(int id)
        {
            var position = new Rect(new Vector2(window.Rect.width - 16, 4), new Vector2(12, 12));

            if (GUI.Button(position, "\u00D7", Styles.Button))
            {
                window.Visible = false;
            }

            window.Function(id);

            // BUG/TODO if one restricts drag to title bar, it breaks Z-order

            var rc = false
                ? new Rect(Vector2.zero, window.Rect.size)
                : new Rect(Vector2.zero, new Vector2(window.Rect.width, 16));

            GUI.DragWindow(rc);
        }
    }

    /// <summary>
    ///     Updates Z-order and last active window.
    /// </summary>
    private void DrawUpdate(Window window)
    {
        var current = Event.current;

        if (current.type == EventType.Used)
        {
            while (current.type != EventType.MouseDown && Event.PopEvent(current))
            {
            }
        }

        if (current is not { type: EventType.MouseDown })
        {
            return;
        }

        if (window.Rect.Contains(current.mousePosition / Scale) == false)
        {
            return;
        }

        current.Use();

        GUI.BringWindowToFront(window.Id);

        LastActive = window;

        var order = WindowsZOrder;

        order.Remove(window);

        order.Insert(0, window);
    }

    private static class Styles
    {
        public static readonly GUIStyle Button = new(GUI.skin.button) { padding = new RectOffset(2, 2, 0, 2), fontStyle = FontStyle.Bold };
    }
}

[Serializable]
public sealed class Window : IEquatable<Window>
{
    public int Id;

    public Rect Rect = new(Vector2.zero, new Vector2(200.0f, 200.0f));

    public string Title;

    public Color Background = Color.white;

    public bool Visible = true;

    public GUI.WindowFunction Function;

    public Window(int id, string title)
    {
        Id    = id;
        Title = title;
    }

    [SuppressMessage("Style", "IDE0041:Use 'is null' check")]
    public bool Equals(Window? other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;

        return Id == other.Id;
    }

    public override string ToString()
    {
        return $"{nameof(Id)}: {Id}, {nameof(Title)}: {Title}, {nameof(Rect)}: {Rect}";
    }

    public override bool Equals(object? obj)
    {
        return ReferenceEquals(this, obj) || obj is Window other && Equals(other);
    }

    [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
    public override int GetHashCode()
    {
        return Id;
    }

    public static bool operator ==(Window? left, Window? right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Window? left, Window? right)
    {
        return !Equals(left, right);
    }
}

public readonly struct GUIColorScope : IDisposable
{
    private readonly Color Global, Background, Text;

    public GUIColorScope(Color? global = null, Color? background = null, Color? text = null)
    {
        Global     = GUI.color;
        Background = GUI.backgroundColor;
        Text       = GUI.contentColor;

        GUI.color           = global ?? Global;
        GUI.backgroundColor = background ?? Background;
        GUI.contentColor    = text ?? Text;
    }

    public void Dispose()
    {
        GUI.color           = Global;
        GUI.backgroundColor = Background;
        GUI.contentColor    = Text;
    }
}
0

There are 0 best solutions below