Skip to content

Commit 75b5bbc

Browse files
committed
Added the UiLayouter class and ILayoutItem interface to allow using MLEM.Ui's layouting system for custom ui systems
1 parent 27dd95d commit 75b5bbc

File tree

3 files changed

+428
-216
lines changed

3 files changed

+428
-216
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Additions
2424
- Added Tooltip.IgnoreViewport and allow overriding the default viewport using Tooltip.Viewport
2525
- Added the ability for Dropdown to display an arrow texture based on its open state
2626
- Added the ability to specify Dropdown paragraph colors through style properties
27+
- Added the UiLayouter class and ILayoutItem interface to allow using MLEM.Ui's layouting system for custom ui systems
2728

2829
Improvements
2930
- Explicitly return the element type from Dropdown.AddElement overloads

MLEM.Ui/Elements/Element.cs

Lines changed: 26 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace MLEM.Ui.Elements {
1818
/// <summary>
1919
/// This class represents a generic base class for ui elements of a <see cref="UiSystem"/>.
2020
/// </summary>
21-
public abstract class Element : GenericDataHolder {
21+
public abstract class Element : GenericDataHolder, ILayoutItem {
2222

2323
/// <summary>
2424
/// This field holds an epsilon value used in element <see cref="Size"/>, position and resulting <see cref="Area"/> calculations to mitigate floating point rounding inaccuracies.
@@ -510,6 +510,11 @@ protected IList<Element> SortedChildren {
510510
/// </summary>
511511
protected RectangleF ParentArea => this.Parent?.ChildPaddedArea ?? (RectangleF) this.System.Viewport;
512512

513+
ILayoutItem ILayoutItem.Parent => this.Parent;
514+
RectangleF ILayoutItem.ParentArea => this.ParentArea;
515+
IEnumerable<ILayoutItem> ILayoutItem.Children => this.Children;
516+
RectangleF ILayoutItem.AutoAnchorArea => this.GetAreaForAutoAnchors();
517+
513518
private readonly List<Element> children = new List<Element>();
514519
private readonly Stopwatch stopwatch = new Stopwatch();
515520
private bool sortedChildrenDirty;
@@ -666,166 +671,11 @@ public virtual void ForceUpdateArea() {
666671
return;
667672
this.stopwatch.Restart();
668673

669-
var recursion = 0;
670-
UpdateDisplayArea();
674+
UiLayouter.Layout(this, Element.Epsilon);
671675

672676
this.stopwatch.Stop();
673677
this.System.Metrics.ForceAreaUpdateTime += this.stopwatch.Elapsed;
674678
this.System.Metrics.ForceAreaUpdates++;
675-
676-
void UpdateDisplayArea(Vector2? overrideSize = null) {
677-
var parentArea = this.ParentArea;
678-
var parentCenterX = parentArea.X + parentArea.Width / 2;
679-
var parentCenterY = parentArea.Y + parentArea.Height / 2;
680-
681-
var intendedSize = this.CalcActualSize(parentArea);
682-
var newSize = overrideSize ?? intendedSize;
683-
var pos = new Vector2();
684-
685-
switch (this.anchor) {
686-
case Anchor.TopLeft:
687-
case Anchor.AutoLeft:
688-
case Anchor.AutoInline:
689-
case Anchor.AutoInlineCenter:
690-
case Anchor.AutoInlineBottom:
691-
case Anchor.AutoInlineIgnoreOverflow:
692-
case Anchor.AutoInlineCenterIgnoreOverflow:
693-
case Anchor.AutoInlineBottomIgnoreOverflow:
694-
pos.X = parentArea.X + this.ScaledOffset.X;
695-
pos.Y = parentArea.Y + this.ScaledOffset.Y;
696-
break;
697-
case Anchor.TopCenter:
698-
case Anchor.AutoCenter:
699-
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
700-
pos.Y = parentArea.Y + this.ScaledOffset.Y;
701-
break;
702-
case Anchor.TopRight:
703-
case Anchor.AutoRight:
704-
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
705-
pos.Y = parentArea.Y + this.ScaledOffset.Y;
706-
break;
707-
case Anchor.CenterLeft:
708-
pos.X = parentArea.X + this.ScaledOffset.X;
709-
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
710-
break;
711-
case Anchor.Center:
712-
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
713-
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
714-
break;
715-
case Anchor.CenterRight:
716-
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
717-
pos.Y = parentCenterY - newSize.Y / 2 + this.ScaledOffset.Y;
718-
break;
719-
case Anchor.BottomLeft:
720-
pos.X = parentArea.X + this.ScaledOffset.X;
721-
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
722-
break;
723-
case Anchor.BottomCenter:
724-
pos.X = parentCenterX - newSize.X / 2 + this.ScaledOffset.X;
725-
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
726-
break;
727-
case Anchor.BottomRight:
728-
pos.X = parentArea.Right - newSize.X - this.ScaledOffset.X;
729-
pos.Y = parentArea.Bottom - newSize.Y - this.ScaledOffset.Y;
730-
break;
731-
}
732-
733-
if (this.Anchor.IsAuto()) {
734-
if (this.Anchor.IsInline()) {
735-
var anchorEl = this.GetOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
736-
if (anchorEl != null) {
737-
var anchorElArea = anchorEl.GetAreaForAutoAnchors();
738-
var newX = anchorElArea.Right + this.ScaledOffset.X;
739-
// with awkward ui scale values, floating point rounding can cause an element that would usually
740-
// be positioned correctly to be pushed into the next line due to a very small deviation
741-
if (this.Anchor.IsIgnoreOverflow() || newX + newSize.X <= parentArea.Right + Element.Epsilon) {
742-
pos.X = newX;
743-
pos.Y = anchorElArea.Y + this.ScaledOffset.Y;
744-
if (this.Anchor == Anchor.AutoInlineCenter || this.Anchor == Anchor.AutoInlineCenterIgnoreOverflow) {
745-
pos.Y += (anchorElArea.Height - newSize.Y) / 2;
746-
} else if (this.Anchor == Anchor.AutoInlineBottom || this.Anchor == Anchor.AutoInlineBottomIgnoreOverflow) {
747-
pos.Y += anchorElArea.Height - newSize.Y;
748-
}
749-
} else {
750-
// inline anchors that overflow into the next line act like AutoLeft
751-
var newlineAnchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
752-
if (newlineAnchorEl != null)
753-
pos.Y = newlineAnchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y;
754-
}
755-
}
756-
} else {
757-
// auto anchors keep their x coordinates from the switch above
758-
var anchorEl = this.GetLowestOlderSibling(e => !e.IsHidden && e.CanAutoAnchorsAttach);
759-
if (anchorEl != null)
760-
pos.Y = anchorEl.GetAreaForAutoAnchors().Bottom + this.ScaledOffset.Y;
761-
}
762-
}
763-
764-
if (this.PreventParentSpill) {
765-
if (pos.X < parentArea.X)
766-
pos.X = parentArea.X;
767-
if (pos.Y < parentArea.Y)
768-
pos.Y = parentArea.Y;
769-
if (pos.X + newSize.X > parentArea.Right)
770-
newSize.X = parentArea.Right - pos.X;
771-
if (pos.Y + newSize.Y > parentArea.Bottom)
772-
newSize.Y = parentArea.Bottom - pos.Y;
773-
}
774-
775-
this.SetAreaAndUpdateChildren(new RectangleF(pos, newSize));
776-
777-
if (this.SetWidthBasedOnChildren || this.SetHeightBasedOnChildren) {
778-
Element foundChild = null;
779-
var autoSize = this.UnscrolledArea.Size;
780-
781-
if (this.SetHeightBasedOnChildren) {
782-
var lowest = this.GetLowestChild(e => !e.IsHidden);
783-
if (lowest != null) {
784-
if (lowest.Anchor.IsTopAligned()) {
785-
autoSize.Y = lowest.UnscrolledArea.Bottom - pos.Y + this.ScaledChildPadding.Bottom;
786-
} else {
787-
autoSize.Y = lowest.UnscrolledArea.Height + this.ScaledChildPadding.Height;
788-
}
789-
foundChild = lowest;
790-
} else {
791-
autoSize.Y = 0;
792-
}
793-
}
794-
795-
if (this.SetWidthBasedOnChildren) {
796-
var rightmost = this.GetRightmostChild(e => !e.IsHidden);
797-
if (rightmost != null) {
798-
if (rightmost.Anchor.IsLeftAligned()) {
799-
autoSize.X = rightmost.UnscrolledArea.Right - pos.X + this.ScaledChildPadding.Right;
800-
} else {
801-
autoSize.X = rightmost.UnscrolledArea.Width + this.ScaledChildPadding.Width;
802-
}
803-
foundChild = rightmost;
804-
} else {
805-
autoSize.X = 0;
806-
}
807-
}
808-
809-
if (this.TreatSizeAsMinimum) {
810-
autoSize = Vector2.Max(autoSize, intendedSize);
811-
} else if (this.TreatSizeAsMaximum) {
812-
autoSize = Vector2.Min(autoSize, intendedSize);
813-
}
814-
815-
// we want to leave some leeway to prevent float rounding causing an infinite loop
816-
if (!autoSize.Equals(this.UnscrolledArea.Size, Element.Epsilon)) {
817-
recursion++;
818-
819-
this.System.Metrics.SummedRecursionDepth++;
820-
if (recursion > this.System.Metrics.MaxRecursionDepth)
821-
this.System.Metrics.MaxRecursionDepth = recursion;
822-
823-
if (recursion >= 64)
824-
throw new ArithmeticException($"The area of {this} has recursively updated too often. Does its child {foundChild} contain any conflicting auto-sizing settings?");
825-
UpdateDisplayArea(autoSize);
826-
}
827-
}
828-
}
829679
}
830680

831681
/// <summary>
@@ -874,19 +724,7 @@ protected virtual RectangleF GetAreaForAutoAnchors() {
874724
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
875725
/// <returns>The lowest element, or null if no such element exists</returns>
876726
public Element GetLowestChild(Func<Element, bool> condition = null, bool total = false) {
877-
Element lowest = null;
878-
var lowestX = float.MinValue;
879-
foreach (var child in this.Children) {
880-
if (condition != null && !condition(child))
881-
continue;
882-
var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea;
883-
var x = !child.Anchor.IsTopAligned() ? covered.Height : covered.Bottom;
884-
if (x >= lowestX) {
885-
lowest = child;
886-
lowestX = x;
887-
}
888-
}
889-
return lowest;
727+
return UiLayouter.GetLowestChild(this, condition, total);
890728
}
891729

892730
/// <summary>
@@ -896,19 +734,7 @@ public Element GetLowestChild(Func<Element, bool> condition = null, bool total =
896734
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
897735
/// <returns>The rightmost element, or null if no such element exists</returns>
898736
public Element GetRightmostChild(Func<Element, bool> condition = null, bool total = false) {
899-
Element rightmost = null;
900-
var rightmostX = float.MinValue;
901-
foreach (var child in this.Children) {
902-
if (condition != null && !condition(child))
903-
continue;
904-
var covered = total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea;
905-
var x = !child.Anchor.IsLeftAligned() ? covered.Width : covered.Right;
906-
if (x >= rightmostX) {
907-
rightmost = child;
908-
rightmostX = x;
909-
}
910-
}
911-
return rightmost;
737+
return UiLayouter.GetRightmostChild(this, condition, total);
912738
}
913739

914740
/// <summary>
@@ -919,18 +745,7 @@ public Element GetRightmostChild(Func<Element, bool> condition = null, bool tota
919745
/// <param name="total">Whether to evaluate based on the child's <see cref="GetTotalCoveredArea"/>, rather than its <see cref="UnscrolledArea"/>.</param>
920746
/// <returns>The lowest older sibling of this element, or null if no such element exists</returns>
921747
public Element GetLowestOlderSibling(Func<Element, bool> condition = null, bool total = false) {
922-
if (this.Parent == null)
923-
return null;
924-
Element lowest = null;
925-
foreach (var child in this.Parent.Children) {
926-
if (child == this)
927-
break;
928-
if (condition != null && !condition(child))
929-
continue;
930-
if (lowest == null || (total ? child.GetTotalCoveredArea(true) : child.UnscrolledArea).Bottom >= lowest.UnscrolledArea.Bottom)
931-
lowest = child;
932-
}
933-
return lowest;
748+
return UiLayouter.GetLowestOlderSibling(this, condition, total);
934749
}
935750

936751
/// <summary>
@@ -940,17 +755,7 @@ public Element GetLowestOlderSibling(Func<Element, bool> condition = null, bool
940755
/// <param name="condition">The condition to match</param>
941756
/// <returns>The older sibling, or null if no such element exists</returns>
942757
public Element GetOlderSibling(Func<Element, bool> condition = null) {
943-
if (this.Parent == null)
944-
return null;
945-
Element older = null;
946-
foreach (var child in this.Parent.Children) {
947-
if (child == this)
948-
break;
949-
if (condition != null && !condition(child))
950-
continue;
951-
older = child;
952-
}
953-
return older;
758+
return UiLayouter.GetOlderSibling(this, condition);
954759
}
955760

956761
/// <summary>
@@ -980,20 +785,12 @@ public IEnumerable<Element> GetSiblings(Func<Element, bool> condition = null) {
980785
/// <typeparam name="T">The type of children to search for</typeparam>
981786
/// <returns>All children that match the condition</returns>
982787
public IEnumerable<T> GetChildren<T>(Func<T, bool> condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) where T : Element {
983-
foreach (var child in this.Children) {
984-
var applies = child is T t && (condition == null || condition(t));
985-
if (applies)
986-
yield return (T) child;
987-
if (regardGrandchildren && (!ignoreFalseGrandchildren || applies)) {
988-
foreach (var cc in child.GetChildren(condition, true, ignoreFalseGrandchildren))
989-
yield return cc;
990-
}
991-
}
788+
return UiLayouter.GetChildren(this, condition, regardGrandchildren, ignoreFalseGrandchildren);
992789
}
993790

994791
/// <inheritdoc cref="GetChildren{T}"/>
995792
public IEnumerable<Element> GetChildren(Func<Element, bool> condition = null, bool regardGrandchildren = false, bool ignoreFalseGrandchildren = false) {
996-
return this.GetChildren<Element>(condition, regardGrandchildren, ignoreFalseGrandchildren);
793+
return UiLayouter.GetChildren(this, condition, regardGrandchildren, ignoreFalseGrandchildren);
997794
}
998795

999796
/// <summary>
@@ -1281,6 +1078,19 @@ protected internal virtual void RemovedFromUi() {
12811078
root?.InvokeOnElementRemoved(this);
12821079
}
12831080

1081+
void ILayoutItem.OnLayoutRecursion(int recursion, ILayoutItem relevantChild) {
1082+
this.System.Metrics.SummedRecursionDepth++;
1083+
if (recursion > this.System.Metrics.MaxRecursionDepth)
1084+
this.System.Metrics.MaxRecursionDepth = recursion;
1085+
1086+
if (recursion >= 64)
1087+
throw new ArithmeticException($"The area of {this} has recursively updated too often. Does its child {relevantChild} contain any conflicting auto-sizing settings?");
1088+
}
1089+
1090+
Vector2 ILayoutItem.CalcActualSize(RectangleF parentArea) {
1091+
return this.CalcActualSize(parentArea);
1092+
}
1093+
12841094
/// <summary>
12851095
/// A delegate used for the <see cref="Element.OnTextInput"/> event.
12861096
/// </summary>

0 commit comments

Comments
 (0)