#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 { /// /// Orchestrating drawing and updating of it's children /// Also a CUIComponent, but it's draw and update methods /// Attached directly to games life cycle /// public class CUIMainComponent : CUIComponent { /// /// Wrapper for global events /// public class CUIGlobalEvents { public Action OnMouseDown; public void InvokeOnMouseDown(CUIInput e) => OnMouseDown?.Invoke(e); public Action OnMouseUp; public void InvokeOnMouseUp(CUIInput e) => OnMouseUp?.Invoke(e); public Action OnMouseMoved; public void InvokeOnMouseMoved(CUIInput e) => OnMouseMoved?.Invoke(e); public Action OnClick; public void InvokeOnClick(CUIInput e) => OnClick?.Invoke(e); public Action OnKeyDown; public void InvokeOnKeyDown(CUIInput e) => OnKeyDown?.Invoke(e); public Action OnKeyUp; public void InvokeOnKeyUp(CUIInput e) => OnKeyUp?.Invoke(e); } /// /// Frozen window doesn't update /// public bool Frozen { get; set; } public double UpdateInterval = 1.0 / 300.0; /// /// If true will update layout until it settles to prevent blinking /// public bool CalculateUntilResolved = true; /// /// If your GUI needs more than this steps of layout update /// you will get a warning /// 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; } /// /// 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 /// public CUIGlobalEvents Global = new CUIGlobalEvents(); private Stopwatch sw = new Stopwatch(); internal List Flat = new List(); internal List Leaves = new List(); internal SortedList> Layers = new SortedList>(); private List MouseOnList = new List(); private Vector2 GrabbedOffset; private void RunStraigth(Action a) { for (int i = 0; i < Flat.Count; i++) a(Flat[i]); } private void RunReverse(Action 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(); 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; /// /// Forses 1 layout update step, even when Frozen /// 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 prevMouseOnList = new List(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 /// /// Obsolete function /// Will run generator func with this /// /// Generator function that adds components to passed Main public void Load(Action 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); } } }