Tooster

Untitled

Jul 2nd, 2020
507
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 12.80 KB | None | 0 0
  1. // easily ported to any other language
  2. // based on unity classes
  3.  
  4. // todo: stretch for justify
  5. public class Flex
  6. {
  7.  
  8.     public interface Widget
  9.     {
  10.         float Width { get; set; }
  11.         float Height { get; set; }
  12.         Vector2 Origin { get; set; }
  13.  
  14.     }
  15.  
  16.     // ! do not change bit values ! algorithm uses bitmasks to determine proper directions of widgets
  17.  
  18.     /// orientation: main axis parallel to 0:x 1:y
  19.     /// flow: 0:axis points towards negative cartesian halfplane 1:--||-- positive halfplane
  20.     /// 0b[mainAxisOrientation][mainAxisFlow][crossAxisFlow]
  21.     private const int _PAD = 128; // mask for ALIGN* enums to specify if they should take element/line separation into account
  22.     public enum FLEX_DIRECTION { ROW = 0b010, ROW_REVERSED = 0b000, COLUMN = 0b101, COLUMN_REVERSED = 0b111 }
  23.     public enum FLEX_ALIGN { START = _PAD | 0, CENTER = _PAD | 1, END = _PAD | 2, SPACE_BETWEEN = 2, SPACE_AROUND = 1, SPACE_EVENLY = 0 }
  24.     public enum FLEX_ALIGN_ITEMS { START = 0, CENTER = 1, END = 2, [Tooltip("all origins on the same line")] BASELINE = 4 }
  25.     public enum FLEX_WRAP { NORMAL = 0b000, REVERSED = 0b001 }
  26.     private enum FLEX_AXIS { MAIN_AXIS = 0b010, CROSS_AXIS = 0b101 }
  27.  
  28.     [Tooltip("Direction of main axis (direction of adding items)")]
  29.     public FLEX_DIRECTION direction = FLEX_DIRECTION.ROW;
  30.  
  31.     [Tooltip("Alignment of the whole flexbox along cross axis (direction of adding lines)")]
  32.     public FLEX_ALIGN alignContent = FLEX_ALIGN.START;
  33.  
  34.     [Tooltip("Alignment of items along main axis in current line")]
  35.     public FLEX_ALIGN justifyContent = FLEX_ALIGN.START;
  36.  
  37.     [Tooltip("Alignment of items along cross axis of current line. Origins mark baseline")]
  38.     public FLEX_ALIGN_ITEMS alignItems = FLEX_ALIGN_ITEMS.START;
  39.  
  40.     [Tooltip("Direction of cross axis - default for row is down and for column is right")]
  41.     public FLEX_WRAP wrap = FLEX_WRAP.NORMAL;
  42.  
  43.     // ignored in automatic margin calculation modes such as space around etc.
  44.     public float itemSeparation = 0;
  45.     public float lineSeparation = 0;
  46.  
  47.     #region FLEX - helper bitmask operations
  48.  
  49.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  50.     private bool IsAxisHorizontal(FLEX_AXIS axis) { return (((int)direction ^ (int)axis) & 0b100) == 0; }
  51.  
  52.     // returns size along axis, so if for example direction is COLUMN and axis is MAIN_AXIS then width is widget's height
  53.     // width is always positive.
  54.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  55.     private float GetSizeAlongAxis(Widget widget, FLEX_AXIS axis) { return IsAxisHorizontal(axis) ? widget.Width : widget.Height; }
  56.  
  57.     // returns +1 if axis points towards cartesian positives and -1 otherwise FIXME: think about replacing it
  58.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  59.     private int GetAxisFlow(FLEX_AXIS axis) { return (0b011 & (int)axis & ((int)direction ^ (int)wrap)) == 0 ? -1 : 1; }
  60.  
  61.     // returns the multiplier p for size along axis so that origin is D=p*size away from the first edge along the axis
  62.     // <-----1[   +--D--]0-----main-axis--------   aka origin but counted from the item-start edge
  63.     // reverse specifies if the other direction along axis should be taken
  64.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  65.     private float GetOriginAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false)
  66.     {
  67.         var origin = (IsAxisHorizontal(axis) ? widget.Origin.x : widget.Origin.y);
  68.         return (GetAxisFlow(axis) > 0) ^ reverse ? origin : 1 - origin;
  69.     }
  70.  
  71.     // returns offset from origin of the second edge in axis direction.
  72.     // reverse is for flipping the axis direction.
  73.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  74.     private float GetExtentAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false)
  75.     {
  76.         var size = GetSizeAlongAxis(widget, axis);
  77.         return size - size * GetOriginAlongAxis(widget, axis, reverse);
  78.     }
  79.  
  80.     // returns the extent from origin to the second edge in direction of axis of the box bounding widget and it's origin
  81.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  82.     private float GetBoundingExtentAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false)
  83.     {
  84.         return Mathf.Max(0, GetExtentAlongAxis(widget, axis, reverse));
  85.     }
  86.  
  87.     // returns size of bounding box of widget (widget and origin) along axis. If origin is inside box, the BB = size
  88.     // Otherwise it is size + offset of origin in given axis; unused -maybe for stretch ? idk, leaving it in case...
  89.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  90.     private float GetBoundingSizeAlongAxis(Widget widget, FLEX_AXIS axis)
  91.     {
  92.         var origin = GetOriginAlongAxis(widget, axis);
  93.         return GetSizeAlongAxis(widget, axis) * Mathf.Max(1.0f, origin > 0 ? origin : 1 - origin);
  94.     }
  95.  
  96.     // returns true if padding from line/element separation should be applied according to layout rules
  97.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  98.     private bool ShouldPad(FLEX_ALIGN align)
  99.     {
  100.         return ((int)align & _PAD) == _PAD;
  101.     }
  102.  
  103.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  104.     private Vector2 AxisToVector2(FLEX_AXIS axis)
  105.     {
  106.         return (IsAxisHorizontal(axis) ? Vector2.right : Vector2.up) * GetAxisFlow(axis);
  107.     }
  108.  
  109.     #endregion
  110.  
  111.     public class Line
  112.     {
  113.         // baseline marks the snapping point for widgets in line. for START it snaps START edge of widget in crossAxis direction,
  114.         // for END analogous, CENTER for widget's volume center and BASELINE for origin directly on baseline
  115.         internal float startToBaseline = 0;
  116.         internal float baselineToEnd = 0;
  117.         internal float mainAxisSize = 0;
  118.         internal readonly List<Widget> widgets = new List<Widget>();
  119.  
  120.         public static Line NextLine() { return new Line(); } // replace with Pool fetch
  121.         public void Reset() { startToBaseline = baselineToEnd = mainAxisSize = 0; widgets.Clear(); }
  122.         public float CrossSize => startToBaseline + baselineToEnd;
  123.     }
  124.  
  125.     private readonly List<Line> flexLines = new List<Line>(16); // initial capacity: 16
  126.     private Vector2 flexMainAxisVector; // maps main axis flow to cartesian vector
  127.     private Vector2 flexCrossAxisVector; // maps cross axis flow to cartesian vector
  128.  
  129.     // elementSeparation and distanceBetweenRows functions as margins
  130.     void FlexRevalidate(Widget containerWidget)
  131.     {
  132.         flexMainAxisVector = AxisToVector2(FLEX_AXIS.MAIN_AXIS);
  133.         flexCrossAxisVector = AxisToVector2(FLEX_AXIS.CROSS_AXIS);
  134.  
  135.         float containerWidgetMainAxisSize = GetSizeAlongAxis(containerWidget, FLEX_AXIS.MAIN_AXIS); // available space in main axis
  136.         float containerWidgetCrossAxisSize = GetSizeAlongAxis(containerWidget, FLEX_AXIS.CROSS_AXIS); // available space in cross axis
  137.  
  138.         float itemPad = ShouldPad(justifyContent) ? elementSeparation : 0.0f;
  139.  
  140.         float contentCrossAxisSize = 0; // height of all rows in ROW mode
  141.  
  142.         { // determining lines and in-line positioning
  143.             var line = Line.NextLine();
  144.             float compulsoryItemPad = 0;
  145.             // cross axis align items and lines setup
  146.             foreach (Transform child in transform)
  147.             {
  148.                 var item = child.gameObject;
  149.                 var widget = GetWidget(item);
  150.                 if (widget == null || !item.activeSelf)
  151.                 { // could be replaced for unsafe elvis operator but bypass unity lifetime check
  152.                     if (item.name == "--br--")
  153.                     {
  154.                         contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize;
  155.                         line = Line.NextLine(); // lines are not expensive
  156.                     }
  157.  
  158.                     continue; // add --br-- elements that ProcessLine and don't increase contentCrossAxisSize
  159.                 }
  160.  
  161.                 widget.Revalidate();
  162.  
  163.                 var itemSize = GetSizeAlongAxis(widget, FLEX_AXIS.MAIN_AXIS);
  164.                 // next widget would overflow
  165.                 if (line.mainAxisSize + itemSize + compulsoryItemPad > containerWidgetMainAxisSize + lineOverflowTolerance && line.widgets.Count > 0)
  166.                 {
  167.                     contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize;
  168.                     line = Line.NextLine();
  169.                     compulsoryItemPad = 0;
  170.                 }
  171.  
  172.                 // determine the baseline and size of widget. It may be convertible to another bitmask ops. but branch predictor should do great job
  173.                 switch (alignItems)
  174.                 {
  175.                     case FLEX_ALIGN_ITEMS.START:
  176.                         line.startToBaseline = 0;
  177.                         line.baselineToEnd = Mathf.Max(line.baselineToEnd, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS));
  178.                         break;
  179.                     case FLEX_ALIGN_ITEMS.CENTER:
  180.                         line.startToBaseline = line.baselineToEnd = Mathf.Max(line.startToBaseline, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS) / 2);
  181.                         break;
  182.                     case FLEX_ALIGN_ITEMS.END:
  183.                         line.startToBaseline = Mathf.Max(line.startToBaseline, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS));
  184.                         line.baselineToEnd = 0;
  185.                         break;
  186.                     case FLEX_ALIGN_ITEMS.BASELINE:
  187.                         line.startToBaseline = Mathf.Max(line.startToBaseline, GetBoundingExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, true));
  188.                         line.baselineToEnd = Mathf.Max(line.baselineToEnd, GetBoundingExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, false));
  189.                         break;
  190.                 }
  191.                 line.mainAxisSize += itemSize;
  192.                 line.widgets.Add(widget);
  193.                 compulsoryItemPad += itemPad;
  194.             }
  195.  
  196.             // process leftover line
  197.             if (line.widgets.Count > 0)
  198.                 contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize;
  199.             // else return to Pool
  200.         }
  201.  
  202.         // main axis align lines and content
  203.         SetupAlignCursor(alignContent, flexLines.Count, containerWidgetCrossAxisSize - contentCrossAxisSize,
  204.                          lineSeparation, out var crossCursor, out var padding);
  205.         crossCursor -= GetExtentAlongAxis(containerWidget, FLEX_AXIS.CROSS_AXIS, true);
  206.         float mainCursor = -GetExtentAlongAxis(containerWidget, FLEX_AXIS.MAIN_AXIS, true);
  207.         foreach (var line in flexLines)
  208.         {
  209.             foreach (var widget in line.widgets)
  210.             {
  211.                 var pos = widget.GetLocalPosition();
  212.                 widget.SetLocalPosition(pos + flexMainAxisVector * (mainCursor) + flexCrossAxisVector * (crossCursor));
  213.             }
  214.  
  215.             crossCursor += padding + line.CrossSize;
  216.             // return line to Pool
  217.         }
  218.  
  219.         containerWidget.RevalidateHierarchy();
  220.         flexLines.Clear();
  221.     }
  222.  
  223.     // sets startOffset for cursor from the beginning of axis so that placing item and later pad results in proper layout
  224.     private void SetupAlignCursor(FLEX_ALIGN align, int itemCount, float freeSpace, float preferredPadding, out float startOffset, out float pad)
  225.     {
  226.         pad = ShouldPad(align)
  227.             ? preferredPadding
  228.             : freeSpace / (itemCount == 1 ? 1.0f : itemCount + (1 - (int)align)); // for 1 item pad=freeSpace
  229.         startOffset = pad + (ShouldPad(align)
  230.             ? (freeSpace - (itemCount - 1) * pad) * ((int)align & ~_PAD) / 2.0f - pad
  231.             : -pad * (itemCount == 1 ? .5f : (int)align / 2.0f));
  232.     }
  233.  
  234.     // aligns items properly in lines according to line start and baseline
  235.     private Line AlignLineItems(ref Line line, in float availableMainSize)
  236.     {
  237.         SetupAlignCursor(justifyContent, line.widgets.Count, availableMainSize - line.mainAxisSize, elementSeparation,
  238.                                      out var mainCursor, out var padding);
  239.  
  240.         foreach (Widget widget in line.widgets)
  241.         {
  242.             widget.SetLocalPosition(flexMainAxisVector * (mainCursor + GetExtentAlongAxis(widget, FLEX_AXIS.MAIN_AXIS, true)) // main axis align
  243.                                     + flexCrossAxisVector  // cross axis align
  244.                                     * (line.startToBaseline + (1 - (int)alignItems / (int)FLEX_ALIGN_ITEMS.BASELINE) // baseline mode cancels origin offset
  245.                                        * (GetExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, true) // snaps begin edge of widget to baseline
  246.                                           - GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS) * (int)alignItems / 2.0f)));
  247.             mainCursor += padding + GetSizeAlongAxis(widget, FLEX_AXIS.MAIN_AXIS);
  248.         }
  249.         flexLines.Add(line);
  250.         return line;
  251.     }
  252. }
Add Comment
Please, Sign In to add comment