Browse Source

Make layout happen before draw (#10)

* Make layout happen before draw

* Update docs
Jean-David Moisan 2 years ago
parent
commit
8fdecda550

+ 2 - 2
Example/Game/GameRoot.cs

@@ -48,7 +48,7 @@ namespace GameProject {
             if (_quit.Pressed())
                 Exit();
 
-            _ui.UpdateAll(gameTime);
+            _ui.UpdateStart(gameTime);
 
             Dock.Put(0, 0, InputHelper.WindowWidth, InputHelper.WindowHeight);
             Vertical.Push();
@@ -71,6 +71,7 @@ namespace GameProject {
             }
             Vertical.Pop();
 
+            _ui.UpdateEnd(gameTime);
             GuiHelper.UpdateCleanup();
             base.Update(gameTime);
         }
@@ -100,7 +101,6 @@ namespace GameProject {
             );
 
         IMGUI _ui;
-        int _counter = 0;
 
         TextureRegion2D _apos;
     }

+ 31 - 32
Source/Button.cs

@@ -22,26 +22,12 @@ namespace Apos.Gui {
             }
         }
 
-        public override void UpdatePrefSize(GameTime gameTime) {
-            if (Child != null) {
-                Child.UpdatePrefSize(gameTime);
-
-                PrefWidth = Child.PrefWidth;
-                PrefHeight = Child.PrefHeight;
-            }
-        }
         public override void UpdateSetup(GameTime gameTime) {
             if (Clicked) {
                 Clicked = false;
             }
 
             if (Child != null) {
-                Child.X = X;
-                Child.Y = Y;
-                Child.Width = Width;
-                Child.Height = Height;
-                Child.Clip = Child.Bounds.Intersection(Clip);
-
                 Child.UpdateSetup(gameTime);
             }
         }
@@ -85,6 +71,28 @@ namespace Apos.Gui {
             }
         }
 
+        public override void UpdatePrefSize(GameTime gameTime) {
+            if (Child != null) {
+                Child.UpdatePrefSize(gameTime);
+
+                PrefWidth = Child.PrefWidth;
+                PrefHeight = Child.PrefHeight;
+            }
+        }
+        public virtual void UpdateLayout(GameTime gameTime) {
+            if (Child != null) {
+                Child.X = X;
+                Child.Y = Y;
+                Child.Width = Width;
+                Child.Height = Height;
+                Child.Clip = Child.Bounds.Intersection(Clip);
+
+                if (Child is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
+            }
+        }
+
         public override void Draw(GameTime gameTime) {
             GuiHelper.PushScissor(Clip);
 
@@ -123,8 +131,9 @@ namespace Apos.Gui {
                 Child = null;
             }
         }
-        public void Reset() { }
-        public int NextIndex() => 0;
+        public virtual void Reset() { }
+        public virtual int PeekNextIndex() => 0;
+        public virtual int NextIndex() => 0;
 
         public virtual IComponent GetPrev(IComponent c) {
             return this;
@@ -137,6 +146,10 @@ namespace Apos.Gui {
             Parent?.SendToTop(this);
         }
 
+        protected bool _mousePressed = false;
+        protected bool _buttonPressed = false;
+        protected bool _hovered = false;
+
         public static Button Put(string text, int fontSize = 30, Color? color = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
             Button b = Put(id, isAbsoluteId);
             Label.Put(text, fontSize: fontSize, color: color, id: id, isAbsoluteId: isAbsoluteId);
@@ -144,12 +157,7 @@ namespace Apos.Gui {
             return b;
         }
         public static Button Put([CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if button with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Button a;
             if (c is Button) {
@@ -158,20 +166,11 @@ namespace Apos.Gui {
                 a = new Button(id);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a, 1);
 
             return a;
         }
-
-        protected bool _mousePressed = false;
-        protected bool _buttonPressed = false;
-        protected bool _hovered = false;
     }
 }

+ 11 - 22
Source/Checkbox.cs

@@ -24,10 +24,6 @@ namespace Apos.Gui {
             }
         }
 
-        public override void UpdatePrefSize(GameTime gameTime) {
-            PrefWidth = 30f;
-            PrefHeight = 30f;
-        }
         public override void UpdateSetup(GameTime gameTime) {
             if (Clicked) {
                 Clicked = false;
@@ -67,6 +63,11 @@ namespace Apos.Gui {
             }
         }
 
+        public override void UpdatePrefSize(GameTime gameTime) {
+            PrefWidth = 30f;
+            PrefHeight = 30f;
+        }
+
         public override void Draw(GameTime gameTime) {
             GuiHelper.PushScissor(Clip);
 
@@ -92,15 +93,12 @@ namespace Apos.Gui {
             GuiHelper.PopScissor();
         }
 
+        protected bool _mousePressed = false;
+        protected bool _buttonPressed = false;
+        protected bool _hovered = false;
+
         public static Checkbox Put(ref bool isChecked, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if checkbox with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 2. Update values.
-            // 3. Register it with a parent.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Checkbox a;
             if (c is Checkbox) {
@@ -114,18 +112,9 @@ namespace Apos.Gui {
                 a = new Checkbox(id, isChecked);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             return a;
         }
-
-        protected bool _mousePressed = false;
-        protected bool _buttonPressed = false;
-        protected bool _hovered = false;
     }
 }

+ 1 - 1
Source/Component.cs

@@ -37,10 +37,10 @@ namespace Apos.Gui {
             }
         }
 
-        public virtual void UpdatePrefSize(GameTime gameTime) { }
         public virtual void UpdateSetup(GameTime gameTime) { }
         public virtual void UpdateInput(GameTime gameTime) { }
         public virtual void Update(GameTime gameTime) { }
+        public virtual void UpdatePrefSize(GameTime gameTime) { }
         public virtual void Draw(GameTime gameTime) { }
 
         /// <summary>

+ 26 - 27
Source/Dock.cs

@@ -18,6 +18,22 @@ namespace Apos.Gui {
 
         public IComponent? Child { get; set; }
 
+        public override void UpdateSetup(GameTime gameTime) {
+            if (Child != null) {
+                Child.UpdateSetup(gameTime);
+            }
+        }
+        public override void UpdateInput(GameTime gameTime) {
+            if (Child != null) {
+                Child.UpdateInput(gameTime);
+            }
+        }
+        public override void Update(GameTime gameTime) {
+            if (Child != null) {
+                Child.Update(gameTime);
+            }
+        }
+
         public override void UpdatePrefSize(GameTime gameTime) {
             if (Child != null) {
                 Child.UpdatePrefSize(gameTime);
@@ -26,7 +42,7 @@ namespace Apos.Gui {
                 PrefHeight = DockBottom - DockTop;
             }
         }
-        public override void UpdateSetup(GameTime gameTime) {
+        public virtual void UpdateLayout(GameTime gameTime) {
             X = DockLeft;
             Y = DockTop;
 
@@ -37,19 +53,12 @@ namespace Apos.Gui {
                 Child.Height = MathHelper.Min(Height, Child.PrefHeight);
                 Child.Clip = Child.Bounds.Intersection(Clip);
 
-                Child.UpdateSetup(gameTime);
-            }
-        }
-        public override void UpdateInput(GameTime gameTime) {
-            if (Child != null) {
-                Child.UpdateInput(gameTime);
-            }
-        }
-        public override void Update(GameTime gameTime) {
-            if (Child != null) {
-                Child.Update(gameTime);
+                if (Child is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
             }
         }
+
         public override void Draw(GameTime gameTime) {
             if (Child != null) {
                 Child.Draw(gameTime);
@@ -71,8 +80,9 @@ namespace Apos.Gui {
                 Child = null;
             }
         }
-        public void Reset() { }
-        public int NextIndex() => 0;
+        public virtual void Reset() { }
+        public virtual int PeekNextIndex() => 0;
+        public virtual int NextIndex() => 0;
 
         public override IComponent GetPrev() {
             return Parent?.GetPrev(this) ?? Child?.GetLast() ?? this;
@@ -95,13 +105,7 @@ namespace Apos.Gui {
         }
 
         public static Dock Put(float left, float top, float right, float bottom, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if dock with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 3. Push it on the stack.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Dock a;
             if (c is Dock) {
@@ -114,12 +118,7 @@ namespace Apos.Gui {
                 a = new Dock(id, left, top, right, bottom);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a, 1);
 

+ 12 - 38
Source/FloatingWindow.cs

@@ -10,19 +10,6 @@ namespace Apos.Gui {
         public override bool IsFocusable { get; set; } = true;
         public override bool IsFloatable { get; set; } = true;
 
-        public override void UpdatePrefSize(GameTime gameTime) {
-            base.UpdatePrefSize(gameTime);
-
-            PrefHeight += 20;
-        }
-        public override void UpdateSetup(GameTime gameTime) {
-            if (_moveNext) {
-                XY = _nextXY;
-                _moveNext = false;
-            }
-
-            base.UpdateSetup(gameTime);
-        }
         public override void UpdateInput(GameTime gameTime) {
             base.UpdateInput(gameTime);
 
@@ -38,12 +25,17 @@ namespace Apos.Gui {
                         _mousePressed = false;
                     } else {
                         Default.MouseInteraction.Consume();
-                        _moveNext = true;
-                        _nextXY = GuiHelper.Mouse + _dragDelta;
+                        XY = GuiHelper.Mouse + _dragDelta;
                     }
                 }
             }
         }
+        public override void UpdatePrefSize(GameTime gameTime) {
+            base.UpdatePrefSize(gameTime);
+
+            PrefHeight += 20;
+        }
+
         public override void Draw(GameTime gameTime) {
             GuiHelper.PushScissor(Clip);
             GuiHelper.SpriteBatch.FillRectangle(Bounds, Color.Black);
@@ -53,15 +45,11 @@ namespace Apos.Gui {
             base.Draw(gameTime);
         }
 
+        protected bool _mousePressed = false;
+        protected Vector2 _dragDelta = Vector2.Zero;
+
         public static new FloatingWindow Push([CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if window with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 2. Parent it.
-            // 3. Push it on the stack.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             FloatingWindow a;
             if (c is FloatingWindow) {
@@ -70,25 +58,11 @@ namespace Apos.Gui {
                 a = new FloatingWindow(id);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.Reset();
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a);
 
             return a;
         }
-        public static new void Pop() {
-            GuiHelper.CurrentIMGUI.Pop();
-        }
-
-        protected bool _mousePressed = false;
-        protected Vector2 _dragDelta = Vector2.Zero;
-        protected bool _moveNext = false;
-        public Vector2 _nextXY = Vector2.Zero;
     }
 }

+ 39 - 44
Source/Horizontal.cs

@@ -45,6 +45,27 @@ namespace Apos.Gui {
         public float ScrollSpeed { get; set; } = 0.25f;
         public long ScrollMaxDuration { get; set; } = 1000;
 
+        public override void UpdateSetup(GameTime gameTime) {
+            foreach (var c in _children)
+                c.UpdateSetup(gameTime);
+        }
+        public override void UpdateInput(GameTime gameTime) {
+            for (int i = _childrenRenderOrder.Count - 1; i >= 0; i--) {
+                _childrenRenderOrder[i].UpdateInput(gameTime);
+            }
+
+            // TODO: If we don't scroll, don't consume the scroll to bubble the event up. Allows recursive scrolling.
+            if (Clip.Contains(GuiHelper.Mouse) && Track.MouseCondition.Scrolled()) {
+                SetOffset(_offsetXTween, ClampOffsetX(_offsetXTween.B + Math.Sign(MouseCondition.ScrollDelta) * ScrollIncrement));
+            }
+
+            // TODO: Consume clicks on the panel? Otherwise it's possible to click stuff under it.
+        }
+        public override void Update(GameTime gameTime) {
+            foreach (var c in _children)
+                c.Update(gameTime);
+        }
+
         public override void UpdatePrefSize(GameTime gameTime) {
             float maxWidth = 0;
             float maxHeight = 0;
@@ -58,7 +79,7 @@ namespace Apos.Gui {
             PrefWidth = maxWidth;
             PrefHeight = maxHeight;
         }
-        public override void UpdateSetup(GameTime gameTime) {
+        public virtual void UpdateLayout(GameTime gameTime) {
             // TODO: Keep current focus in view if it's in view?
 
             if (_offsetXTween.B != ClampOffsetX(_offsetXTween.B)) {
@@ -81,7 +102,9 @@ namespace Apos.Gui {
                 maxHeight = MathHelper.Max(c.Height, maxHeight);
                 c.Clip = c.Bounds.Intersection(Clip);
 
-                c.UpdateSetup(gameTime);
+                if (c is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
 
                 currentX += c.Width;
             }
@@ -89,22 +112,7 @@ namespace Apos.Gui {
             FullWidth = MathHelper.Max(currentX, maxWidth);
             FullHeight = maxHeight;
         }
-        public override void UpdateInput(GameTime gameTime) {
-            for (int i = _childrenRenderOrder.Count - 1; i >= 0; i--) {
-                _childrenRenderOrder[i].UpdateInput(gameTime);
-            }
-
-            // TODO: If we don't scroll, don't consume the scroll to bubble the event up. Allows recursive scrolling.
-            if (Clip.Contains(GuiHelper.Mouse) && Track.MouseCondition.Scrolled()) {
-                SetOffset(_offsetXTween, ClampOffsetX(_offsetXTween.B + Math.Sign(MouseCondition.ScrollDelta) * ScrollIncrement));
-            }
 
-            // TODO: Consume clicks on the panel? Otherwise it's possible to click stuff under it.
-        }
-        public override void Update(GameTime gameTime) {
-            foreach (var c in _children)
-                c.Update(gameTime);
-        }
         public override void Draw(GameTime gameTime) {
             foreach (var c in _childrenRenderOrder) {
                 if (Clip.Intersects(c.Clip)) {
@@ -141,9 +149,8 @@ namespace Apos.Gui {
         public virtual void Reset() {
             _nextChildIndex = 0;
         }
-        public virtual int NextIndex() {
-            return _nextChildIndex++;
-        }
+        public virtual int PeekNextIndex() => _nextChildIndex + 1;
+        public virtual int NextIndex() => _nextChildIndex++;
 
         /// <summary>
         /// If this component has a parent, it will ask the parent to return this component's previous neighbor.
@@ -224,14 +231,18 @@ namespace Apos.Gui {
             return (long)Math.Min(Math.Abs((b - a) / speed), maxDuration);
         }
 
+        protected int _nextChildIndex = 0;
+        protected List<IComponent> _children = new List<IComponent>();
+        protected List<IComponent> _childrenRenderOrder = new List<IComponent>();
+
+        protected FloatTween _offsetXTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
+        protected FloatTween _offsetYTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
+
+        protected float _offsetX = 0;
+        protected float _offsetY = 0;
+
         public static Horizontal Push([CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if horizontal with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 3. Push it on the stack.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Horizontal a;
             if (c is Horizontal) {
@@ -240,13 +251,7 @@ namespace Apos.Gui {
                 a = new Horizontal(id);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.Reset();
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a);
 
@@ -255,15 +260,5 @@ namespace Apos.Gui {
         public static void Pop() {
             GuiHelper.CurrentIMGUI.Pop();
         }
-
-        protected int _nextChildIndex = 0;
-        protected List<IComponent> _children = new List<IComponent>();
-        protected List<IComponent> _childrenRenderOrder = new List<IComponent>();
-
-        protected FloatTween _offsetXTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
-        protected FloatTween _offsetYTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
-
-        protected float _offsetX = 0;
-        protected float _offsetY = 0;
     }
 }

+ 6 - 7
Source/IComponent.cs

@@ -98,13 +98,7 @@ namespace Apos.Gui {
         float Bottom { get; set; }
 
         /// <summary>
-        /// First pass for layout management.
-        /// Components can determine their preferred sizes.
-        /// </summary>
-        void UpdatePrefSize(GameTime gameTime);
-        /// <summary>
-        /// Second pass for layout management.
-        /// Parents give their child a position and size. Good place for animations that modify a layout.
+        /// Good place to setup states before inputs are processed.
         /// </summary>
         void UpdateSetup(GameTime gameTime);
         /// <summary>
@@ -118,6 +112,11 @@ namespace Apos.Gui {
         /// </summary>
         void Update(GameTime gameTime);
         /// <summary>
+        /// First pass for layout management.
+        /// Components can determine their preferred sizes.
+        /// </summary>
+        void UpdatePrefSize(GameTime gameTime);
+        /// <summary>
         /// Draws the component. The component should draw itself within it's clip rectangle.
         /// </summary>
         void Draw(GameTime gameTime);

+ 77 - 89
Source/IMGUI.cs

@@ -15,11 +15,7 @@ namespace Apos.Gui {
         }
 
         /// <summary>
-        /// Only call this if you didn't call UpdateAll. This should be called at the start of your update loop.
-        /// </summary>
-        public override void UpdatePrefSize(GameTime gameTime) { }
-        /// <summary>
-        /// Only call this if you didn't call UpdateAll. This should be called after UpdatePrefSize.
+        /// Only call this if you didn't call UpdateStart. This should be called after UpdatePrefSize.
         /// </summary>
         public override void UpdateSetup(GameTime gameTime) {
             // 1. Ping ourself to prevent cleanup.
@@ -29,48 +25,15 @@ namespace Apos.Gui {
             // 4. Update pref sizes.
             // 5. Apply pref sizes.
             // 6. Update setup.
-            LastPing = InputHelper.CurrentFrame - 1;
-            Cleanup();
+            LastPing = InputHelper.CurrentFrame;
             _idsUsedThisFrame.Add(Id, 1);
-            while (_pendingComponents.Count > 0) {
-                var pc = _pendingComponents.Dequeue();
-                if (pc.Component.Parent == null) {
-                    _activeComponents.Add(pc.Id, pc.Component);
-                } else {
-                    pc.Component.Parent.Remove(pc.Component);
-                }
-                pc.Parent.Add(pc.Component);
-                pc.Component.GrabFocus = GrabFocus;
-            }
-
-            _isTick0 = !_isTick0;
-            if (!_isTick0) {
-                while (_nextTick0.Count > 0) {
-                    _nextTick0.Dequeue().Invoke();
-                }
-            } else {
-                while (_nextTick1.Count > 0) {
-                    _nextTick1.Dequeue().Invoke();
-                }
-            }
-
-            X = 0f;
-            Y = 0f;
-            Width = GuiHelper.WindowWidth;
-            Height = GuiHelper.WindowHeight;
 
             foreach (var c in _children) {
-                c.UpdatePrefSize(gameTime);
-                c.Width = c.PrefWidth;
-                c.Height = c.PrefHeight;
-
-                c.Clip = c.Bounds.Intersection(Clip);
-
                 c.UpdateSetup(gameTime);
             }
         }
         /// <summary>
-        /// Only call this if you didn't call UpdateAll. This should be called after UpdateSetup.
+        /// Only call this if you didn't call UpdateStart. This should be called after UpdateSetup.
         /// </summary>
         /// <param name="gameTime">Current gametime.</param>
         public override void UpdateInput(GameTime gameTime) {
@@ -101,7 +64,7 @@ namespace Apos.Gui {
             }
         }
         /// <summary>
-        /// Only call this if you didn't call UpdateAll. This should be called after UpdateInput.
+        /// Only call this if you didn't call UpdateStart. This should be called after UpdateInput.
         /// </summary>
         /// <param name="gameTime">Current gametime</param>
         public override void Update(GameTime gameTime) {
@@ -115,12 +78,49 @@ namespace Apos.Gui {
         /// </summary>
         /// <param name="gameTime">Current gametime.</param>
         /// <param name="callUpdateInput">false will skip UpdateInput.</param>
-        public void UpdateAll(GameTime gameTime, bool callUpdateInput = true) {
+        public void UpdateStart(GameTime gameTime, bool callUpdateInput = true) {
             UpdateSetup(gameTime);
             if (callUpdateInput)
                 UpdateInput(gameTime);
             Update(gameTime);
         }
+
+        /// <summary>
+        /// Don't call this.
+        /// </summary>
+        public override void UpdatePrefSize(GameTime gameTime) { }
+        /// <summary>
+        /// Only call this if you didn't call UpdateEnd. This should be called after UpdateInput.
+        /// </summary>
+        /// <param name="gameTime">Current gametime</param>
+        public void UpdateLayout(GameTime gameTime) {
+            // IMGUI manages itself so it can set it's own position and size.
+            X = 0f;
+            Y = 0f;
+            Width = GuiHelper.WindowWidth;
+            Height = GuiHelper.WindowHeight;
+
+            foreach (var c in _children) {
+                c.UpdatePrefSize(gameTime);
+                c.Width = c.PrefWidth;
+                c.Height = c.PrefHeight;
+
+                c.Clip = c.Bounds.Intersection(Clip);
+
+                if (c is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
+            }
+        }
+
+        /// <summary>
+        /// This should be called at the end of your update loop.
+        /// </summary>
+        public void UpdateEnd(GameTime gameTime) {
+            Cleanup();
+            UpdateLayout(gameTime);
+        }
+
         /// <summary>
         /// Draws all components in the UI.
         /// </summary>
@@ -176,38 +176,38 @@ namespace Apos.Gui {
         /// <param name="c">When this method returns, contains the component associated with the specified id.</param>
         /// <returns>true if the component with the specified id is found; otherwise false.</returns>
         public bool TryGetValue(int id, out IComponent c) {
-            if (_activeComponents.TryGetValue(id, out c)) {
-                return true;
-            }
-
-            // TODO: Verify that this is still required.
-            //       This is usually called after CreateId which will always return a new unique id for this frame.
-            foreach (var pc in _pendingComponents) {
-                if (pc.Id == id) {
-                    c = pc.Component;
-                    return true;
-                }
-            }
-
-            return false;
+            return _activeComponents.TryGetValue(id, out c);
         }
         /// <summary>
-        /// Returns the current top parent. Used for parenting a child component to a parent.
-        /// If a child already has a parent, it will be marked for a parent change.
+        /// Used for parenting a child component to a parent.
+        /// If a child already has a parent, it will be adopted by the current parent if the parent is different.
         /// </summary>
         /// <param name="c">The child that will be parented.</param>
-        public IParent GrabParent(IComponent c) {
+        public void GrabParent(IComponent c) {
             IParent current = _currentParent;
 
-            if (c.Parent != current) {
-                _pendingComponents.Enqueue((c.Id, _currentParent, c));
+            if (c.Parent == null) {
+                _activeComponents.Add(c.Id, c);
+            }
+
+            // Note: This might be considered a hack. Is it more proper to reset parent components during their LastPing?
+            if (c is IParent p) {
+                p.Reset();
+            }
+
+            if (c.Parent != current || c.Parent.PeekNextIndex() != c.Index) {
+                c.Parent?.Remove(c);
+                c.Index = current.NextIndex();
+                c.GrabFocus = GrabFocus;
+
+                current.Add(c);
             }
 
             if (_maxChildren > 0 && ++_childrenCount >= _maxChildren) {
                 Pop();
             }
 
-            return current;
+            c.LastPing = InputHelper.CurrentFrame;
         }
         /// <summary>
         /// Gives focus to a component. Can also clear the focus if null is passed.
@@ -241,20 +241,15 @@ namespace Apos.Gui {
 
             return id;
         }
-        /// <summary>
-        /// Used when an action would invalidate the layout which would lead to an invalid draw (flicker).
-        /// Using this, you can delay the action until the next UpdateSetup.
-        /// </summary>
-        /// <param name="a">The action that will be enqueued.</param>
-        public void QueueNextTick(Action a) {
-            if (_isTick0) {
-                _nextTick0.Enqueue(a);
-            } else {
-                _nextTick1.Enqueue(a);
-            }
+
+        public int TryCreateId(int id, bool isAbsoluteId, out IComponent c) {
+            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
+            GuiHelper.CurrentIMGUI.TryGetValue(id, out c);
+
+            return id;
         }
 
-        public virtual void Add(IComponent c) {
+        public void Add(IComponent c) {
             c.Parent = this;
             _children.Insert(c.Index, c);
 
@@ -272,17 +267,16 @@ namespace Apos.Gui {
                 }
             });
         }
-        public virtual void Remove(IComponent c) {
+        public void Remove(IComponent c) {
             c.Parent = null;
             _children.Remove(c);
             _childrenRenderOrder.Remove(c);
         }
-        public virtual void Reset() {
+        public void Reset() {
             _nextChildIndex = 0;
         }
-        public virtual int NextIndex() {
-            return _nextChildIndex++;
-        }
+        public int PeekNextIndex() => _nextChildIndex + 1;
+        public int NextIndex() => _nextChildIndex++;
 
         /// <summary>
         /// If this component has a parent, it will ask the parent to return this component's previous neighbor.
@@ -304,7 +298,7 @@ namespace Apos.Gui {
         /// If the child isn't the first one, it will return the child before it.
         /// Otherwise it will return itself.
         /// </summary>
-        public virtual IComponent GetPrev(IComponent c) {
+        public IComponent GetPrev(IComponent c) {
             int index = c.Index - 1;
             return index >= 0 ? _children[index].GetLast() : this;
         }
@@ -313,7 +307,7 @@ namespace Apos.Gui {
         /// If it has a parent, it will ask the parent to return this component's next neighbor.
         /// Otherwise it will return itself.
         /// </summary>
-        public virtual IComponent GetNext(IComponent c) {
+        public IComponent GetNext(IComponent c) {
             int index = c.Index + 1;
             return index < _children.Count ? _children[index] : Parent?.GetNext(this) ?? this;
         }
@@ -324,7 +318,7 @@ namespace Apos.Gui {
             return _children.Count > 0 ? _children.Last().GetLast() : this;
         }
 
-        public virtual void SendToTop(IComponent c) {
+        public void SendToTop(IComponent c) {
             if (c.IsFloatable) {
                 _childrenRenderOrder.Remove(c);
                 _childrenRenderOrder.Add(c);
@@ -334,7 +328,7 @@ namespace Apos.Gui {
         private void Cleanup() {
             Reset();
             foreach (var kc in _activeComponents.Reverse()) {
-                if (kc.Value.LastPing != InputHelper.CurrentFrame - 1) {
+                if (kc.Value.LastPing != InputHelper.CurrentFrame) {
                     Remove(kc.Key, kc.Value);
                 }
             }
@@ -351,7 +345,6 @@ namespace Apos.Gui {
 
             _activeComponents.Remove(id);
             c.Parent?.Remove(c);
-            // TODO: Remove from PendingComponents? Probably not since that case can't happen?
         }
         private void FindPrevFocus() {
             FindFocus(ExtractPrev);
@@ -402,7 +395,6 @@ namespace Apos.Gui {
 
         private Stack<(IParent Parent, int MaxChildren, int ChildrenCount)> _parents = new Stack<(IParent, int, int)>();
         private Dictionary<int, IComponent> _activeComponents = new Dictionary<int, IComponent>();
-        private Queue<(int Id, IParent Parent, IComponent Component)> _pendingComponents = new Queue<(int, IParent, IComponent)>();
         private int? Focus {
             get => _focus;
             set {
@@ -424,10 +416,6 @@ namespace Apos.Gui {
         private bool _prevPressed = false;
         private bool _nextPressed = false;
 
-        private bool _isTick0 = true;
-        private Queue<Action> _nextTick0 = new Queue<Action>();
-        private Queue<Action> _nextTick1 = new Queue<Action>();
-
         private int _nextChildIndex = 0;
         private List<IComponent> _children = new List<IComponent>();
         private List<IComponent> _childrenRenderOrder = new List<IComponent>();

+ 8 - 0
Source/IParent.cs

@@ -1,11 +1,19 @@
+using Microsoft.Xna.Framework;
+
 namespace Apos.Gui {
     public interface IParent : IComponent {
         void Add(IComponent c);
         void Remove(IComponent c);
         void Reset();
+        int PeekNextIndex();
         int NextIndex();
         IComponent GetPrev(IComponent c);
         IComponent GetNext(IComponent c);
         void SendToTop(IComponent c);
+
+        /// <summary>
+        /// Applies a layout to the component
+        /// </summary>
+        void UpdateLayout(GameTime gameTime);
     }
 }

+ 2 - 12
Source/Icon.cs

@@ -32,12 +32,7 @@ namespace Apos.Gui {
         }
 
         public static Icon Put(TextureRegion2D region, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if Icon with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Icon a;
             if (c is Icon) {
@@ -47,12 +42,7 @@ namespace Apos.Gui {
                 a = new Icon(id, region);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             return a;
         }

+ 20 - 36
Source/Label.cs

@@ -12,31 +12,28 @@ namespace Apos.Gui {
         }
 
         public string Text {
-            get => _nextText;
+            get => _text;
             set {
-                if (value != _nextText) {
-                    _nextText = value;
-                    _isUpdateText = true;
+                if (value != _text) {
+                    _text = value;
+                    _isDirty = true;
                 }
             }
         }
         public int Padding { get; set; } = 10;
         public Color Color { get; set; }
         public int FontSize {
-            get => _nextSize;
+            get => _fontSize;
             set {
-                if (value != _nextSize) {
-                    _nextSize = value;
-                    _isUpdateText = true;
+                if (value != _fontSize) {
+                    _fontSize = value;
+                    _isDirty = true;
                 }
             }
         }
 
         public override void UpdatePrefSize(GameTime gameTime) {
-            if (_isUpdateText) {
-                Cache();
-            }
-
+            Cache();
             PrefWidth = _cachedSize.X + Padding * 2;
             PrefHeight = _cachedSize.Y + Padding * 2;
         }
@@ -51,19 +48,19 @@ namespace Apos.Gui {
         }
 
         protected void Cache() {
-            _text = _nextText;
-            _fontSize = _nextSize;
-            _cachedSize = GuiHelper.MeasureString(_text, _fontSize);
-            _isUpdateText = false;
+            if (_isDirty) {
+                _cachedSize = GuiHelper.MeasureString(_text, _fontSize);
+                _isDirty = false;
+            }
         }
 
+        protected string _text = null!;
+        protected int _fontSize;
+        protected Vector2 _cachedSize;
+        protected bool _isDirty = false;
+
         public static Label Put(string text, int fontSize = 30, Color? color = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if Label with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             color ??= new Color(200, 200, 200);
 
@@ -77,22 +74,9 @@ namespace Apos.Gui {
                 a = new Label(id, text, fontSize, color.Value);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             return a;
         }
-
-        protected string _text = null!;
-        protected int _fontSize;
-        protected Vector2 _cachedSize;
-
-        protected bool _isUpdateText = false;
-        protected string _nextText = "";
-        protected int _nextSize;
     }
 }

+ 8 - 18
Source/MenuPanel.cs

@@ -22,7 +22,7 @@ namespace Apos.Gui {
                 _snap = true;
             }
         }
-        public override void UpdateSetup(GameTime gameTime) {
+        public override void UpdateLayout(GameTime gameTime) {
             // MenuPanel is a root component so it can set it's own position.
             X = 0f;
             Y = 0f;
@@ -54,7 +54,9 @@ namespace Apos.Gui {
 
                 c.Clip = c.Bounds.Intersection(Clip);
 
-                c.UpdateSetup(gameTime);
+                if (c is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
 
                 currentY += c.Height;
             }
@@ -64,14 +66,10 @@ namespace Apos.Gui {
             return MathHelper.Min(MathHelper.Max(y, Height - FullHeight), FullHeight < Height ? Height / 2f - FullHeight / 2f : 0f);
         }
 
+        protected bool _snap = false;
+
         public static new MenuPanel Push([CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if MenuPanel with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 3. Push it on the stack.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             MenuPanel a;
             if (c is MenuPanel) {
@@ -80,19 +78,11 @@ namespace Apos.Gui {
                 a = new MenuPanel(id);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.Reset();
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a);
 
             return a;
         }
-
-        protected bool _snap = false;
     }
 }

+ 29 - 37
Source/Slider.cs

@@ -23,10 +23,6 @@ namespace Apos.Gui {
         }
         public override bool IsFocusable { get; set; } = true;
 
-        public override void UpdatePrefSize(GameTime gameTime) {
-            PrefWidth = 100;
-            PrefHeight = 40;
-        }
         public override void UpdateInput(GameTime gameTime) {
             if (Clip.Contains(GuiHelper.Mouse) && Default.MouseInteraction.Pressed()) {
                 _isPressed = true;
@@ -57,6 +53,12 @@ namespace Apos.Gui {
                 }
             }
         }
+
+        public override void UpdatePrefSize(GameTime gameTime) {
+            PrefWidth = 100;
+            PrefHeight = 40;
+        }
+
         public override void Draw(GameTime gameTime) {
             GuiHelper.PushScissor(Clip);
             Color c;
@@ -71,39 +73,6 @@ namespace Apos.Gui {
             GuiHelper.PopScissor();
         }
 
-        public static Slider Put(ref float value, float min, float max, float? step = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if Textbox with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
-
-            Slider a;
-            if (c is Slider) {
-                a = (Slider)c;
-                if (a.IsFocused) {
-                    value = a.Value;
-                } else {
-                    a.Value = value;
-                    a.Min = min;
-                    a.Max = max;
-                    a.Step = step;
-                }
-            } else {
-                a = new Slider(id, value, min, max, step);
-            }
-
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
-
-            return a;
-        }
-
         protected void SlideValue(ICondition condition, int direction) {
             if (condition.Pressed()) {
                 Value = MathHelper.Max(MathHelper.Min(Value + direction * Step!.Value, Max), Min);
@@ -125,5 +94,28 @@ namespace Apos.Gui {
         protected int _inputDelay = 0;
         protected int _inputDelaySpeed = 50;
         protected int _inputDelayInitialSpeed = 400;
+
+        public static Slider Put(ref float value, float min, float max, float? step = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
+
+            Slider a;
+            if (c is Slider) {
+                a = (Slider)c;
+                if (a.IsFocused) {
+                    value = a.Value;
+                } else {
+                    a.Value = value;
+                    a.Min = min;
+                    a.Max = max;
+                    a.Step = step;
+                }
+            } else {
+                a = new Slider(id, value, min, max, step);
+            }
+
+            GuiHelper.CurrentIMGUI.GrabParent(a);
+
+            return a;
+        }
     }
 }

+ 43 - 42
Source/Textbox.cs

@@ -23,10 +23,7 @@ namespace Apos.Gui {
             set {
                 if (_text != value) {
                     _text = value;
-                    _size = GuiHelper.MeasureString(_text, _fontSize);
-                    if (Cursor > _text.Length) {
-                        Cursor = _text.Length;
-                    }
+                    _isDirty = true;
                 }
             }
         }
@@ -35,10 +32,9 @@ namespace Apos.Gui {
         public int FontSize {
             get => _fontSize;
             set {
-                // TODO: Only change size on next loop since it changes the layout.
                 if (value != _fontSize) {
                     _fontSize = value;
-                    _size = GuiHelper.MeasureString(_text, _fontSize);
+                    _isDirty = true;
                 }
             }
         }
@@ -52,10 +48,6 @@ namespace Apos.Gui {
         }
         public override bool IsFocusable { get; set; } = true;
 
-        public override void UpdatePrefSize(GameTime gameTime) {
-            PrefWidth = MathHelper.Max(_size.X, 100) + Padding * 2;
-            PrefHeight = _size.Y + Padding * 2;
-        }
         public override void UpdateInput(GameTime gameTime) {
             if (Clip.Contains(GuiHelper.Mouse) && Default.MouseInteraction.Pressed()) {
                 _pressed = true;
@@ -111,6 +103,13 @@ namespace Apos.Gui {
                 }
             }
         }
+
+        public override void UpdatePrefSize(GameTime gameTime) {
+            Cache();
+            PrefWidth = MathHelper.Max(_cachedSize.X, 100f) + Padding * 2f;
+            PrefHeight = _cachedSize.Y + Padding * 2f;
+        }
+
         public override void Draw(GameTime gameTime) {
             GuiHelper.PushScissor(Clip);
 
@@ -136,39 +135,14 @@ namespace Apos.Gui {
             GuiHelper.PopScissor();
         }
 
-        public static Textbox Put(ref string text, int fontSize = 30, Color? color = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if Textbox with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
-
-            color ??= new Color(200, 200, 200);
-
-            Textbox a;
-            if (c is Textbox) {
-                a = (Textbox)c;
-                if (a.IsFocused) {
-                    text = a.Text;
-                } else {
-                    a.Text = text;
+        protected void Cache() {
+            if (_isDirty) {
+                _cachedSize = GuiHelper.MeasureString(_text, _fontSize);
+                if (Cursor > _text.Length) {
+                    Cursor = _text.Length;
                 }
-
-                a.Color = color.Value;
-                a.FontSize = fontSize;
-            } else {
-                a = new Textbox(id, text, fontSize, color.Value);
-            }
-
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
+                _isDirty = false;
             }
-
-            return a;
         }
 
         protected void MoveCursor(ICondition condition, int direction) {
@@ -200,9 +174,11 @@ namespace Apos.Gui {
         }
 
         protected string _text = null!;
-        protected Vector2 _size;
+        protected Vector2 _cachedSize;
 
         protected int _fontSize;
+        protected bool _isDirty = false;
+
         protected RectangleF _cursorRect;
         protected int Cursor {
             get => _cursor;
@@ -221,5 +197,30 @@ namespace Apos.Gui {
         protected FloatTween _blink = new FloatTween(0f, 1f, 1500, Easing.Linear);
 
         protected bool _pressed = false;
+
+        public static Textbox Put(ref string text, int fontSize = 30, Color? color = null, [CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
+
+            color ??= new Color(200, 200, 200);
+
+            Textbox a;
+            if (c is Textbox) {
+                a = (Textbox)c;
+                if (a.IsFocused) {
+                    text = a.Text;
+                } else {
+                    a.Text = text;
+                }
+
+                a.Color = color.Value;
+                a.FontSize = fontSize;
+            } else {
+                a = new Textbox(id, text, fontSize, color.Value);
+            }
+
+            GuiHelper.CurrentIMGUI.GrabParent(a);
+
+            return a;
+        }
     }
 }

+ 38 - 44
Source/Vertical.cs

@@ -45,6 +45,27 @@ namespace Apos.Gui {
         public float ScrollSpeed { get; set; } = 0.25f;
         public long ScrollMaxDuration { get; set; } = 1000;
 
+        public override void UpdateSetup(GameTime gameTime) {
+            foreach (var c in _children)
+                c.UpdateSetup(gameTime);
+        }
+        public override void UpdateInput(GameTime gameTime) {
+            for (int i = _childrenRenderOrder.Count - 1; i >= 0; i--) {
+                _childrenRenderOrder[i].UpdateInput(gameTime);
+            }
+
+            // TODO: If we don't scroll, don't consume the scroll to bubble the event up. Allows recursive scrolling.
+            if (Clip.Contains(GuiHelper.Mouse) && Track.MouseCondition.Scrolled()) {
+                SetOffset(_offsetYTween, ClampOffsetY(_offsetYTween.B + Math.Sign(MouseCondition.ScrollDelta) * ScrollIncrement));
+            }
+
+            // TODO: Consume clicks on the panel? Otherwise it's possible to click stuff under it.
+        }
+        public override void Update(GameTime gameTime) {
+            foreach (var c in _children)
+                c.Update(gameTime);
+        }
+
         public override void UpdatePrefSize(GameTime gameTime) {
             float maxWidth = 0;
             float maxHeight = 0;
@@ -58,7 +79,7 @@ namespace Apos.Gui {
             PrefWidth = maxWidth;
             PrefHeight = maxHeight;
         }
-        public override void UpdateSetup(GameTime gameTime) {
+        public virtual void UpdateLayout(GameTime gameTime) {
             // TODO: Keep current focus in view if it's in view?
 
             if (_offsetYTween.B != ClampOffsetY(_offsetYTween.B)) {
@@ -81,7 +102,9 @@ namespace Apos.Gui {
                 maxWidth = MathHelper.Max(c.Width, maxWidth);
                 c.Clip = c.Bounds.Intersection(Clip);
 
-                c.UpdateSetup(gameTime);
+                if (c is IParent p) {
+                    p.UpdateLayout(gameTime);
+                }
 
                 currentY += c.Height;
             }
@@ -89,22 +112,7 @@ namespace Apos.Gui {
             FullWidth = maxWidth;
             FullHeight = MathHelper.Max(currentY, maxHeight);
         }
-        public override void UpdateInput(GameTime gameTime) {
-            for (int i = _childrenRenderOrder.Count - 1; i >= 0; i--) {
-                _childrenRenderOrder[i].UpdateInput(gameTime);
-            }
 
-            // TODO: If we don't scroll, don't consume the scroll to bubble the event up. Allows recursive scrolling.
-            if (Clip.Contains(GuiHelper.Mouse) && Track.MouseCondition.Scrolled()) {
-                SetOffset(_offsetYTween, ClampOffsetY(_offsetYTween.B + Math.Sign(MouseCondition.ScrollDelta) * ScrollIncrement));
-            }
-
-            // TODO: Consume clicks on the panel? Otherwise it's possible to click stuff under it.
-        }
-        public override void Update(GameTime gameTime) {
-            foreach (var c in _children)
-                c.Update(gameTime);
-        }
         public override void Draw(GameTime gameTime) {
             foreach (var c in _childrenRenderOrder) {
                 if (Clip.Intersects(c.Clip)) {
@@ -141,9 +149,8 @@ namespace Apos.Gui {
         public virtual void Reset() {
             _nextChildIndex = 0;
         }
-        public virtual int NextIndex() {
-            return _nextChildIndex++;
-        }
+        public virtual int PeekNextIndex() => _nextChildIndex + 1;
+        public virtual int NextIndex() => _nextChildIndex++;
 
         /// <summary>
         /// If this component has a parent, it will ask the parent to return this component's previous neighbor.
@@ -223,15 +230,18 @@ namespace Apos.Gui {
         protected long GetDuration(float a, float b, float speed, long maxDuration) {
             return (long)Math.Min(Math.Abs((b - a) / speed), maxDuration);
         }
+        protected int _nextChildIndex = 0;
+        protected List<IComponent> _children = new List<IComponent>();
+        protected List<IComponent> _childrenRenderOrder = new List<IComponent>();
+
+        protected FloatTween _offsetXTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
+        protected FloatTween _offsetYTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
+
+        protected float _offsetX = 0;
+        protected float _offsetY = 0;
 
         public static Vertical Push([CallerLineNumber] int id = 0, bool isAbsoluteId = false) {
-            // 1. Check if vertical with id already exists.
-            //      a. If already exists. Get it.
-            //      b  If not, create it.
-            // 3. Push it on the stack.
-            // 4. Ping it.
-            id = GuiHelper.CurrentIMGUI.CreateId(id, isAbsoluteId);
-            GuiHelper.CurrentIMGUI.TryGetValue(id, out IComponent c);
+            id = GuiHelper.CurrentIMGUI.TryCreateId(id, isAbsoluteId, out IComponent c);
 
             Vertical a;
             if (c is Vertical) {
@@ -240,13 +250,7 @@ namespace Apos.Gui {
                 a = new Vertical(id);
             }
 
-            IParent parent = GuiHelper.CurrentIMGUI.GrabParent(a);
-
-            if (a.LastPing != InputHelper.CurrentFrame) {
-                a.Reset();
-                a.LastPing = InputHelper.CurrentFrame;
-                a.Index = parent.NextIndex();
-            }
+            GuiHelper.CurrentIMGUI.GrabParent(a);
 
             GuiHelper.CurrentIMGUI.Push(a);
 
@@ -255,15 +259,5 @@ namespace Apos.Gui {
         public static void Pop() {
             GuiHelper.CurrentIMGUI.Pop();
         }
-
-        protected int _nextChildIndex = 0;
-        protected List<IComponent> _children = new List<IComponent>();
-        protected List<IComponent> _childrenRenderOrder = new List<IComponent>();
-
-        protected FloatTween _offsetXTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
-        protected FloatTween _offsetYTween = new FloatTween(0f, 0f, 0, Easing.ExpoOut);
-
-        protected float _offsetX = 0;
-        protected float _offsetY = 0;
     }
 }

+ 3 - 2
docs/getting-started.md

@@ -40,7 +40,7 @@ In your update loop, call the following functions:
 protected override void Update(GameTime gameTime) {
     // Call UpdateSetup at the start.
     GuiHelper.UpdateSetup(gameTime);
-    _ui.UpdateAll(gameTime);
+    _ui.UpdateStart(gameTime);
 
     // Create your UI.
     MenuPanel.Push();
@@ -55,7 +55,8 @@ protected override void Update(GameTime gameTime) {
     }
     MenuPanel.Pop();
 
-    // Call UpdateCleanup at the end.
+    // Call UpdateEnd and UpdateCleanup at the end.
+    _ui.UpdateEnd(gameTime);
     GuiHelper.UpdateCleanup();
 
     base.Update(gameTime);