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...
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;
}
}
