@@ -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