522 lines
15 KiB
C#
522 lines
15 KiB
C#
#define SHOWPERF
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
|
|
using Barotrauma;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Input;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using System.Diagnostics;
|
|
|
|
namespace QICrabUI
|
|
{
|
|
/// <summary>
|
|
/// Orchestrating drawing and updating of it's children
|
|
/// Also a CUIComponent, but it's draw and update methods
|
|
/// Attached directly to games life cycle
|
|
/// </summary>
|
|
public class CUIMainComponent : CUIComponent
|
|
{
|
|
/// <summary>
|
|
/// Wrapper for global events
|
|
/// </summary>
|
|
public class CUIGlobalEvents
|
|
{
|
|
public Action<CUIInput> OnMouseDown; public void InvokeOnMouseDown(CUIInput e) => OnMouseDown?.Invoke(e);
|
|
public Action<CUIInput> OnMouseUp; public void InvokeOnMouseUp(CUIInput e) => OnMouseUp?.Invoke(e);
|
|
public Action<CUIInput> OnMouseMoved; public void InvokeOnMouseMoved(CUIInput e) => OnMouseMoved?.Invoke(e);
|
|
public Action<CUIInput> OnClick; public void InvokeOnClick(CUIInput e) => OnClick?.Invoke(e);
|
|
public Action<CUIInput> OnKeyDown; public void InvokeOnKeyDown(CUIInput e) => OnKeyDown?.Invoke(e);
|
|
public Action<CUIInput> OnKeyUp; public void InvokeOnKeyUp(CUIInput e) => OnKeyUp?.Invoke(e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frozen window doesn't update
|
|
/// </summary>
|
|
public bool Frozen { get; set; }
|
|
public double UpdateInterval = 1.0 / 300.0;
|
|
/// <summary>
|
|
/// If true will update layout until it settles to prevent blinking
|
|
/// </summary>
|
|
public bool CalculateUntilResolved = true;
|
|
/// <summary>
|
|
/// If your GUI needs more than this steps of layout update
|
|
/// you will get a warning
|
|
/// </summary>
|
|
public int MaxLayoutRecalcLoopsPerUpdate = 10;
|
|
public event Action OnTreeChanged;
|
|
public Action AddOnTreeChanged { set { OnTreeChanged += value; } }
|
|
|
|
public CUIDragHandle GrabbedDragHandle;
|
|
public CUIResizeHandle GrabbedResizeHandle;
|
|
public CUISwipeHandle GrabbedSwipeHandle;
|
|
public CUIComponent MouseOn;
|
|
public CUIComponent FocusedComponent
|
|
{
|
|
get => CUI.FocusedComponent;
|
|
set => CUI.FocusedComponent = value;
|
|
}
|
|
/// <summary>
|
|
/// Container for true global events
|
|
/// CUIMainComponent itself can react to events and you can listen for those,
|
|
/// but e.g. mouse events may be consumed before they reach Main
|
|
/// </summary>
|
|
public CUIGlobalEvents Global = new CUIGlobalEvents();
|
|
|
|
private Stopwatch sw = new Stopwatch();
|
|
|
|
internal List<CUIComponent> Flat = new List<CUIComponent>();
|
|
internal List<CUIComponent> Leaves = new List<CUIComponent>();
|
|
internal SortedList<int, List<CUIComponent>> Layers = new SortedList<int, List<CUIComponent>>();
|
|
private List<CUIComponent> MouseOnList = new List<CUIComponent>();
|
|
private Vector2 GrabbedOffset;
|
|
|
|
private void RunStraigth(Action<CUIComponent> a) { for (int i = 0; i < Flat.Count; i++) a(Flat[i]); }
|
|
private void RunReverse(Action<CUIComponent> a) { for (int i = Flat.Count - 1; i >= 0; i--) a(Flat[i]); }
|
|
|
|
|
|
|
|
|
|
private void FlattenTree()
|
|
{
|
|
int retries = 0;
|
|
bool done = false;
|
|
do
|
|
{
|
|
retries++;
|
|
if (retries > 10) break;
|
|
try
|
|
{
|
|
Flat.Clear();
|
|
Layers.Clear();
|
|
|
|
int globalIndex = 0;
|
|
void CalcZIndexRec(CUIComponent component, int added = 0)
|
|
{
|
|
component.positionalZIndex = globalIndex;
|
|
globalIndex += 1;
|
|
component.addedZIndex = added;
|
|
if (component.ZIndex.HasValue) component.addedZIndex += component.ZIndex.Value;
|
|
|
|
foreach (CUIComponent child in component.Children)
|
|
{
|
|
CalcZIndexRec(child, component.addedZIndex);
|
|
}
|
|
}
|
|
|
|
CalcZIndexRec(this, 0);
|
|
RunRecursiveOn(this, (c) =>
|
|
{
|
|
int i = c.positionalZIndex + c.addedZIndex;
|
|
if (!Layers.ContainsKey(i)) Layers[i] = new List<CUIComponent>();
|
|
Layers[i].Add(c);
|
|
});
|
|
|
|
foreach (var layer in Layers)
|
|
{
|
|
Flat.AddRange(layer.Value);
|
|
}
|
|
|
|
done = true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
CUI.Warning($"Couldn't Flatten component tree: {e.Message}");
|
|
}
|
|
} while (!done);
|
|
}
|
|
|
|
#region Update
|
|
|
|
internal bool GlobalLayoutChanged;
|
|
internal void LayoutChanged() => GlobalLayoutChanged = true;
|
|
private double LastUpdateTime;
|
|
private int UpdateLoopCount = 0;
|
|
/// <summary>
|
|
/// Forses 1 layout update step, even when Frozen
|
|
/// </summary>
|
|
public void Step()
|
|
{
|
|
Update(LastUpdateTime + UpdateInterval, true, true);
|
|
}
|
|
public void Update(double totalTime, bool force = false, bool noInput = false)
|
|
{
|
|
if (!force)
|
|
{
|
|
if (Frozen) return;
|
|
if (totalTime - LastUpdateTime <= UpdateInterval) return;
|
|
}
|
|
|
|
CUIDebug.Flush();
|
|
|
|
if (TreeChanged)
|
|
{
|
|
OnTreeChanged?.Invoke();
|
|
|
|
FlattenTree();
|
|
TreeChanged = false;
|
|
}
|
|
|
|
if (!noInput) HandleInput(totalTime);
|
|
|
|
RunStraigth(c => c.InvokeOnUpdate(totalTime));
|
|
|
|
|
|
if (CalculateUntilResolved)
|
|
{
|
|
UpdateLoopCount = 0;
|
|
do
|
|
{
|
|
GlobalLayoutChanged = false;
|
|
|
|
if (TreeChanged)
|
|
{
|
|
OnTreeChanged?.Invoke();
|
|
|
|
FlattenTree();
|
|
TreeChanged = false;
|
|
}
|
|
|
|
RunReverse(c =>
|
|
{
|
|
c.Layout.ResizeToContent();
|
|
});
|
|
|
|
RunStraigth(c =>
|
|
{
|
|
c.Layout.Update();
|
|
c.Layout.UpdateDecor();
|
|
});
|
|
|
|
UpdateLoopCount++;
|
|
if (UpdateLoopCount >= MaxLayoutRecalcLoopsPerUpdate)
|
|
{
|
|
PrintRecalLimitWarning();
|
|
break;
|
|
}
|
|
}
|
|
while (GlobalLayoutChanged);
|
|
//CUI.Log($"UpdateLoopCount: {UpdateLoopCount}");
|
|
}
|
|
else
|
|
{
|
|
RunReverse(c =>
|
|
{
|
|
c.Layout.ResizeToContent();
|
|
});
|
|
|
|
RunStraigth(c =>
|
|
{
|
|
c.Layout.Update();
|
|
c.Layout.UpdateDecor();
|
|
});
|
|
}
|
|
|
|
//TODO do i need 2 updates?
|
|
//RunStraigth(c => c.InvokeOnUpdate(totalTime));
|
|
|
|
LastUpdateTime = totalTime;
|
|
}
|
|
|
|
#endregion
|
|
#region Draw
|
|
|
|
private void StopStart(SpriteBatch spriteBatch, Rectangle SRect, SamplerState? samplerState = null)
|
|
{
|
|
samplerState ??= GUI.SamplerState;
|
|
spriteBatch.End();
|
|
spriteBatch.GraphicsDevice.ScissorRectangle = SRect;
|
|
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: samplerState, rasterizerState: GameMain.ScissorTestEnable);
|
|
}
|
|
|
|
public new void Draw(SpriteBatch spriteBatch)
|
|
{
|
|
sw.Restart();
|
|
|
|
Rectangle OriginalSRect = spriteBatch.GraphicsDevice.ScissorRectangle;
|
|
Rectangle SRect = OriginalSRect;
|
|
|
|
try
|
|
{
|
|
RunStraigth(c =>
|
|
{
|
|
if (!c.Visible || c.CulledOut) return;
|
|
if (c.Parent != null && c.Parent.ScissorRect.HasValue && SRect != c.Parent.ScissorRect.Value)
|
|
{
|
|
SRect = c.Parent.ScissorRect.Value;
|
|
StopStart(spriteBatch, SRect, c.SamplerState);
|
|
}
|
|
c.Draw(spriteBatch);
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
if (spriteBatch.GraphicsDevice.ScissorRectangle != OriginalSRect) StopStart(spriteBatch, OriginalSRect);
|
|
}
|
|
|
|
RunStraigth(c =>
|
|
{
|
|
if (!c.Visible || c.CulledOut) return;
|
|
c.DrawFront(spriteBatch);
|
|
});
|
|
|
|
sw.Stop();
|
|
// CUIDebug.EnsureCategory();
|
|
// CUIDebug.CaptureTicks(sw.ElapsedTicks, "CUI.Draw");
|
|
}
|
|
#endregion
|
|
// https://youtu.be/xuFgUmYCS8E?feature=shared&t=72
|
|
#region HandleInput Start
|
|
|
|
public void OnDragEnd(CUIDragHandle h) { if (h == GrabbedDragHandle) GrabbedDragHandle = null; }
|
|
public void OnResizeEnd(CUIResizeHandle h) { if (h == GrabbedResizeHandle) GrabbedResizeHandle = null; }
|
|
public void OnSwipeEnd(CUISwipeHandle h) { if (h == GrabbedSwipeHandle) GrabbedSwipeHandle = null; }
|
|
|
|
|
|
private void HandleInput(double totalTime)
|
|
{
|
|
HandleGlobal(totalTime);
|
|
HandleMouse(totalTime);
|
|
HandleKeyboard(totalTime);
|
|
}
|
|
|
|
private void HandleGlobal(double totalTime)
|
|
{
|
|
if (CUI.Input.MouseDown) Global.InvokeOnMouseDown(CUI.Input);
|
|
if (CUI.Input.MouseUp)
|
|
{
|
|
Global.InvokeOnMouseUp(CUI.Input);
|
|
Global.InvokeOnClick(CUI.Input);
|
|
}
|
|
if (CUI.Input.MouseMoved) Global.InvokeOnMouseMoved(CUI.Input);
|
|
if (CUI.Input.SomeKeyPressed) Global.InvokeOnKeyDown(CUI.Input);
|
|
if (CUI.Input.SomeKeyUnpressed) Global.InvokeOnKeyUp(CUI.Input);
|
|
}
|
|
|
|
private void HandleKeyboard(double totalTime)
|
|
{
|
|
if (FocusedComponent == null) FocusedComponent = this;
|
|
if (CUI.Input.PressedKeys.Contains(Keys.Escape)) FocusedComponent = this;
|
|
if (CUI.Input.SomeKeyPressed) FocusedComponent.InvokeOnKeyDown(CUI.Input);
|
|
if (CUI.Input.SomeKeyUnpressed) FocusedComponent.InvokeOnKeyUp(CUI.Input);
|
|
if (CUI.Input.SomeWindowEvents) FocusedComponent.InvokeOnTextInput(CUI.Input);
|
|
}
|
|
|
|
private void HandleMouse(double totalTime)
|
|
{
|
|
if (!CUI.Input.SomethingHappened) return;
|
|
|
|
if (!CUI.Input.MouseHeld)
|
|
{
|
|
GrabbedDragHandle?.EndDrag();
|
|
GrabbedResizeHandle?.EndResize();
|
|
GrabbedSwipeHandle?.EndSwipe();
|
|
}
|
|
|
|
if (CUI.Input.MouseMoved)
|
|
{
|
|
GrabbedDragHandle?.DragTo(CUI.Input.MousePosition);
|
|
GrabbedResizeHandle?.Resize(CUI.Input.MousePosition);
|
|
GrabbedSwipeHandle?.Swipe(CUI.Input);
|
|
}
|
|
|
|
if (CUI.Input.MouseInputHandled) return;
|
|
|
|
//HACK
|
|
//if (CUI.Input.ClickConsumed) return;
|
|
|
|
//TODO think where should i put it?
|
|
if (GrabbedResizeHandle != null || GrabbedDragHandle != null || GrabbedSwipeHandle != null) return;
|
|
|
|
List<CUIComponent> prevMouseOnList = new List<CUIComponent>(MouseOnList);
|
|
|
|
CUIComponent CurrentMouseOn = null;
|
|
MouseOnList.Clear();
|
|
|
|
|
|
|
|
// form MouseOnList
|
|
// Note: including main component
|
|
if (
|
|
GUI.MouseOn == null || (GUI.MouseOn is GUIButton btn && btn.Text == "DUMMY")
|
|
|| (this == CUI.TopMain) //TODO guh
|
|
)
|
|
{
|
|
RunStraigth(c =>
|
|
{
|
|
bool ok = !c.IgnoreEvents && c.Real.Contains(CUI.Input.MousePosition) && c.ShouldInvoke(CUI.Input);
|
|
|
|
if (c.Parent != null && c.Parent.ScissorRect.HasValue &&
|
|
!c.Parent.ScissorRect.Value.Contains(CUI.Input.Mouse.Position))
|
|
{
|
|
ok = false;
|
|
}
|
|
|
|
if (ok) MouseOnList.Add(c);
|
|
});
|
|
}
|
|
|
|
MouseOn = MouseOnList.LastOrDefault();
|
|
|
|
//HACK
|
|
if (MouseOn != this)
|
|
{
|
|
CUI.Input.MouseInputHandled = true;
|
|
CUIMultiModResolver.MarkOtherInputsAsHandled();
|
|
}
|
|
|
|
//if (CurrentMouseOn != null) GUI.MouseOn = dummyComponent;
|
|
|
|
|
|
foreach (CUIComponent c in prevMouseOnList)
|
|
{
|
|
c.MousePressed = false;
|
|
c.MouseOver = false;
|
|
c.InvokeOnMouseOff(CUI.Input);
|
|
}
|
|
|
|
foreach (CUIComponent c in MouseOnList)
|
|
{
|
|
c.MousePressed = CUI.Input.MouseHeld;
|
|
c.MouseOver = true;
|
|
c.InvokeOnMouseOn(CUI.Input);
|
|
}
|
|
|
|
// Mouse enter / leave
|
|
foreach (CUIComponent c in prevMouseOnList.Except(MouseOnList)) c.InvokeOnMouseLeave(CUI.Input);
|
|
foreach (CUIComponent c in MouseOnList.Except(prevMouseOnList)) c.InvokeOnMouseEnter(CUI.Input);
|
|
|
|
|
|
// focus
|
|
if (CUI.Input.MouseDown)
|
|
{
|
|
CUIComponent newFocused = this;
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (MouseOnList[i].FocusHandle.ShouldStart(CUI.Input))
|
|
{
|
|
newFocused = MouseOnList[i];
|
|
break;
|
|
}
|
|
}
|
|
FocusedComponent = newFocused;
|
|
}
|
|
|
|
// Resize
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (MouseOnList[i].RightResizeHandle.ShouldStart(CUI.Input))
|
|
{
|
|
GrabbedResizeHandle = MouseOnList[i].RightResizeHandle;
|
|
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
|
|
break;
|
|
}
|
|
|
|
if (MouseOnList[i].LeftResizeHandle.ShouldStart(CUI.Input))
|
|
{
|
|
GrabbedResizeHandle = MouseOnList[i].LeftResizeHandle;
|
|
GrabbedResizeHandle.BeginResize(CUI.Input.MousePosition);
|
|
break;
|
|
}
|
|
}
|
|
if (GrabbedResizeHandle != null) return;
|
|
|
|
//Scroll
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (CUI.Input.Scrolled) MouseOnList[i].InvokeOnScroll(CUI.Input);
|
|
|
|
if (MouseOnList[i].ConsumeMouseScroll) break;
|
|
}
|
|
|
|
//Move
|
|
if (CUI.Input.MouseMoved)
|
|
{
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
MouseOnList[i].InvokeOnMouseMove(CUI.Input);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//Clicks
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (CUI.Input.MouseDown) MouseOnList[i].InvokeOnMouseDown(CUI.Input);
|
|
if (CUI.Input.MouseUp)
|
|
{
|
|
MouseOnList[i].InvokeOnMouseUp(CUI.Input);
|
|
MouseOnList[i].InvokeOnClick(CUI.Input);
|
|
}
|
|
if (CUI.Input.DoubleClick) MouseOnList[i].InvokeOnDClick(CUI.Input);
|
|
|
|
if (MouseOnList[i].ConsumeMouseClicks || CUI.Input.ClickConsumed) break;
|
|
}
|
|
if (CUI.Input.ClickConsumed) return;
|
|
|
|
// Swipe
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (MouseOnList[i].SwipeHandle.ShouldStart(CUI.Input))
|
|
{
|
|
GrabbedSwipeHandle = MouseOnList[i].SwipeHandle;
|
|
GrabbedSwipeHandle.BeginSwipe(CUI.Input.MousePosition);
|
|
break;
|
|
}
|
|
|
|
if (MouseOnList[i].ConsumeSwipe) break;
|
|
}
|
|
if (GrabbedSwipeHandle != null) return;
|
|
|
|
// Drag
|
|
for (int i = MouseOnList.Count - 1; i >= 0; i--)
|
|
{
|
|
if (MouseOnList[i].DragHandle.ShouldStart(CUI.Input))
|
|
{
|
|
GrabbedDragHandle = MouseOnList[i].DragHandle;
|
|
GrabbedDragHandle.BeginDrag(CUI.Input.MousePosition);
|
|
break;
|
|
}
|
|
|
|
if (MouseOnList[i].ConsumeDragAndDrop) break;
|
|
}
|
|
if (GrabbedDragHandle != null) return;
|
|
}
|
|
#endregion
|
|
#region HandleInput End
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Obsolete function
|
|
/// Will run generator func with this
|
|
/// </summary>
|
|
/// <param name="initFunc"> Generator function that adds components to passed Main </param>
|
|
public void Load(Action<CUIMainComponent> initFunc)
|
|
{
|
|
RemoveAllChildren();
|
|
initFunc(this);
|
|
}
|
|
|
|
public CUIMainComponent() : base()
|
|
{
|
|
CullChildren = true;
|
|
Real = new CUIRect(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
|
|
Visible = false;
|
|
//IgnoreEvents = true;
|
|
ShouldPassPropsToChildren = false;
|
|
|
|
|
|
Debug = true;
|
|
ChildrenBoundaries = CUIBoundaries.Box;
|
|
}
|
|
|
|
public void PrintRecalLimitWarning()
|
|
{
|
|
CUI.Log($"Warning: Your GUI code requires {MaxLayoutRecalcLoopsPerUpdate} layout update loops to fully resolve (which is cringe). Optimize it!", Color.Orange);
|
|
}
|
|
}
|
|
} |