Files
barotrauma-localmods/Quick Interactions/CSharp/Client/CrabUI/Components/CUIMainComponent.cs

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