Created
February 8, 2026 14:21
-
-
Save T3sT3ro/ff855f5b53af8f39deb23cd1075df204 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // By Tooster | |
| // easily ported to any other language | |
| // based on unity classes | |
| // todo: stretch for justify | |
| public class Flex | |
| { | |
| public interface Widget | |
| { | |
| float Width { get; set; } | |
| float Height { get; set; } | |
| Vector2 Origin { get; set; } | |
| } | |
| // ! do not change bit values ! algorithm uses bitmasks to determine proper directions of widgets | |
| /// orientation: main axis parallel to 0:x 1:y | |
| /// flow: 0:axis points towards negative cartesian halfplane 1:--||-- positive halfplane | |
| /// 0b[mainAxisOrientation][mainAxisFlow][crossAxisFlow] | |
| private const int _PAD = 128; // mask for ALIGN* enums to specify if they should take element/line separation into account | |
| public enum FLEX_DIRECTION { ROW = 0b010, ROW_REVERSED = 0b000, COLUMN = 0b101, COLUMN_REVERSED = 0b111 } | |
| public enum FLEX_ALIGN { START = _PAD | 0, CENTER = _PAD | 1, END = _PAD | 2, SPACE_BETWEEN = 2, SPACE_AROUND = 1, SPACE_EVENLY = 0 } | |
| public enum FLEX_ALIGN_ITEMS { START = 0, CENTER = 1, END = 2, [Tooltip("all origins on the same line")] BASELINE = 4 } | |
| public enum FLEX_WRAP { NORMAL = 0b000, REVERSED = 0b001 } | |
| private enum FLEX_AXIS { MAIN_AXIS = 0b010, CROSS_AXIS = 0b101 } | |
| [Tooltip("Direction of main axis (direction of adding items)")] | |
| public FLEX_DIRECTION direction = FLEX_DIRECTION.ROW; | |
| [Tooltip("Alignment of the whole flexbox along cross axis (direction of adding lines)")] | |
| public FLEX_ALIGN alignContent = FLEX_ALIGN.START; | |
| [Tooltip("Alignment of items along main axis in current line")] | |
| public FLEX_ALIGN justifyContent = FLEX_ALIGN.START; | |
| [Tooltip("Alignment of items along cross axis of current line. Origins mark baseline")] | |
| public FLEX_ALIGN_ITEMS alignItems = FLEX_ALIGN_ITEMS.START; | |
| [Tooltip("Direction of cross axis - default for row is down and for column is right")] | |
| public FLEX_WRAP wrap = FLEX_WRAP.NORMAL; | |
| // ignored in automatic margin calculation modes such as space around etc. | |
| public float itemSeparation = 0; | |
| public float lineSeparation = 0; | |
| #region FLEX - helper bitmask operations | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private bool IsAxisHorizontal(FLEX_AXIS axis) { return (((int)direction ^ (int)axis) & 0b100) == 0; } | |
| // returns size along axis, so if for example direction is COLUMN and axis is MAIN_AXIS then width is widget's height | |
| // width is always positive. | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private float GetSizeAlongAxis(Widget widget, FLEX_AXIS axis) { return IsAxisHorizontal(axis) ? widget.Width : widget.Height; } | |
| // returns +1 if axis points towards cartesian positives and -1 otherwise FIXME: think about replacing it | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private int GetAxisFlow(FLEX_AXIS axis) { return (0b011 & (int)axis & ((int)direction ^ (int)wrap)) == 0 ? -1 : 1; } | |
| // returns the multiplier p for size along axis so that origin is D=p*size away from the first edge along the axis | |
| // <-----1[ +--D--]0-----main-axis-------- aka origin but counted from the item-start edge | |
| // reverse specifies if the other direction along axis should be taken | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private float GetOriginAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false) | |
| { | |
| var origin = (IsAxisHorizontal(axis) ? widget.Origin.x : widget.Origin.y); | |
| return (GetAxisFlow(axis) > 0) ^ reverse ? origin : 1 - origin; | |
| } | |
| // returns offset from origin of the second edge in axis direction. | |
| // reverse is for flipping the axis direction. | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private float GetExtentAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false) | |
| { | |
| var size = GetSizeAlongAxis(widget, axis); | |
| return size - size * GetOriginAlongAxis(widget, axis, reverse); | |
| } | |
| // returns the extent from origin to the second edge in direction of axis of the box bounding widget and it's origin | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private float GetBoundingExtentAlongAxis(Widget widget, FLEX_AXIS axis, bool reverse = false) | |
| { | |
| return Mathf.Max(0, GetExtentAlongAxis(widget, axis, reverse)); | |
| } | |
| // returns size of bounding box of widget (widget and origin) along axis. If origin is inside box, the BB = size | |
| // Otherwise it is size + offset of origin in given axis; unused -maybe for stretch ? idk, leaving it in case... | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private float GetBoundingSizeAlongAxis(Widget widget, FLEX_AXIS axis) | |
| { | |
| var origin = GetOriginAlongAxis(widget, axis); | |
| return GetSizeAlongAxis(widget, axis) * Mathf.Max(1.0f, origin > 0 ? origin : 1 - origin); | |
| } | |
| // returns true if padding from line/element separation should be applied according to layout rules | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private bool ShouldPad(FLEX_ALIGN align) | |
| { | |
| return ((int)align & _PAD) == _PAD; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| private Vector2 AxisToVector2(FLEX_AXIS axis) | |
| { | |
| return (IsAxisHorizontal(axis) ? Vector2.right : Vector2.up) * GetAxisFlow(axis); | |
| } | |
| #endregion | |
| public class Line | |
| { | |
| // baseline marks the snapping point for widgets in line. for START it snaps START edge of widget in crossAxis direction, | |
| // for END analogous, CENTER for widget's volume center and BASELINE for origin directly on baseline | |
| internal float startToBaseline = 0; | |
| internal float baselineToEnd = 0; | |
| internal float mainAxisSize = 0; | |
| internal readonly List<Widget> widgets = new List<Widget>(); | |
| public static Line NextLine() { return new Line(); } // replace with Pool fetch | |
| public void Reset() { startToBaseline = baselineToEnd = mainAxisSize = 0; widgets.Clear(); } | |
| public float CrossSize => startToBaseline + baselineToEnd; | |
| } | |
| private readonly List<Line> flexLines = new List<Line>(16); // initial capacity: 16 | |
| private Vector2 flexMainAxisVector; // maps main axis flow to cartesian vector | |
| private Vector2 flexCrossAxisVector; // maps cross axis flow to cartesian vector | |
| // elementSeparation and distanceBetweenRows functions as margins | |
| void FlexRevalidate(Widget containerWidget) | |
| { | |
| flexMainAxisVector = AxisToVector2(FLEX_AXIS.MAIN_AXIS); | |
| flexCrossAxisVector = AxisToVector2(FLEX_AXIS.CROSS_AXIS); | |
| float containerWidgetMainAxisSize = GetSizeAlongAxis(containerWidget, FLEX_AXIS.MAIN_AXIS); // available space in main axis | |
| float containerWidgetCrossAxisSize = GetSizeAlongAxis(containerWidget, FLEX_AXIS.CROSS_AXIS); // available space in cross axis | |
| float itemPad = ShouldPad(justifyContent) ? elementSeparation : 0.0f; | |
| float contentCrossAxisSize = 0; // height of all rows in ROW mode | |
| { // determining lines and in-line positioning | |
| var line = Line.NextLine(); | |
| float compulsoryItemPad = 0; | |
| // cross axis align items and lines setup | |
| foreach (Transform child in transform) | |
| { | |
| var item = child.gameObject; | |
| var widget = GetWidget(item); | |
| if (widget == null || !item.activeSelf) | |
| { // could be replaced for unsafe elvis operator but bypass unity lifetime check | |
| if (item.name == "--br--") | |
| { | |
| contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize; | |
| line = Line.NextLine(); // lines are not expensive | |
| } | |
| continue; // add --br-- elements that ProcessLine and don't increase contentCrossAxisSize | |
| } | |
| widget.Revalidate(); | |
| var itemSize = GetSizeAlongAxis(widget, FLEX_AXIS.MAIN_AXIS); | |
| // next widget would overflow | |
| if (line.mainAxisSize + itemSize + compulsoryItemPad > containerWidgetMainAxisSize + lineOverflowTolerance && line.widgets.Count > 0) | |
| { | |
| contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize; | |
| line = Line.NextLine(); | |
| compulsoryItemPad = 0; | |
| } | |
| // determine the baseline and size of widget. It may be convertible to another bitmask ops. but branch predictor should do great job | |
| switch (alignItems) | |
| { | |
| case FLEX_ALIGN_ITEMS.START: | |
| line.startToBaseline = 0; | |
| line.baselineToEnd = Mathf.Max(line.baselineToEnd, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS)); | |
| break; | |
| case FLEX_ALIGN_ITEMS.CENTER: | |
| line.startToBaseline = line.baselineToEnd = Mathf.Max(line.startToBaseline, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS) / 2); | |
| break; | |
| case FLEX_ALIGN_ITEMS.END: | |
| line.startToBaseline = Mathf.Max(line.startToBaseline, GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS)); | |
| line.baselineToEnd = 0; | |
| break; | |
| case FLEX_ALIGN_ITEMS.BASELINE: | |
| line.startToBaseline = Mathf.Max(line.startToBaseline, GetBoundingExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, true)); | |
| line.baselineToEnd = Mathf.Max(line.baselineToEnd, GetBoundingExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, false)); | |
| break; | |
| } | |
| line.mainAxisSize += itemSize; | |
| line.widgets.Add(widget); | |
| compulsoryItemPad += itemPad; | |
| } | |
| // process leftover line | |
| if (line.widgets.Count > 0) | |
| contentCrossAxisSize += AlignLineItems(ref line, containerWidgetMainAxisSize).CrossSize; | |
| // else return to Pool | |
| } | |
| // main axis align lines and content | |
| SetupAlignCursor(alignContent, flexLines.Count, containerWidgetCrossAxisSize - contentCrossAxisSize, | |
| lineSeparation, out var crossCursor, out var padding); | |
| crossCursor -= GetExtentAlongAxis(containerWidget, FLEX_AXIS.CROSS_AXIS, true); | |
| float mainCursor = -GetExtentAlongAxis(containerWidget, FLEX_AXIS.MAIN_AXIS, true); | |
| foreach (var line in flexLines) | |
| { | |
| foreach (var widget in line.widgets) | |
| { | |
| var pos = widget.GetLocalPosition(); | |
| widget.SetLocalPosition(pos + flexMainAxisVector * (mainCursor) + flexCrossAxisVector * (crossCursor)); | |
| } | |
| crossCursor += padding + line.CrossSize; | |
| // return line to Pool | |
| } | |
| containerWidget.RevalidateHierarchy(); | |
| flexLines.Clear(); | |
| } | |
| // sets startOffset for cursor from the beginning of axis so that placing item and later pad results in proper layout | |
| private void SetupAlignCursor(FLEX_ALIGN align, int itemCount, float freeSpace, float preferredPadding, out float startOffset, out float pad) | |
| { | |
| pad = ShouldPad(align) | |
| ? preferredPadding | |
| : freeSpace / (itemCount == 1 ? 1.0f : itemCount + (1 - (int)align)); // for 1 item pad=freeSpace | |
| startOffset = pad + (ShouldPad(align) | |
| ? (freeSpace - (itemCount - 1) * pad) * ((int)align & ~_PAD) / 2.0f - pad | |
| : -pad * (itemCount == 1 ? .5f : (int)align / 2.0f)); | |
| } | |
| // aligns items properly in lines according to line start and baseline | |
| private Line AlignLineItems(ref Line line, in float availableMainSize) | |
| { | |
| SetupAlignCursor(justifyContent, line.widgets.Count, availableMainSize - line.mainAxisSize, elementSeparation, | |
| out var mainCursor, out var padding); | |
| foreach (Widget widget in line.widgets) | |
| { | |
| widget.SetLocalPosition(flexMainAxisVector * (mainCursor + GetExtentAlongAxis(widget, FLEX_AXIS.MAIN_AXIS, true)) // main axis align | |
| + flexCrossAxisVector // cross axis align | |
| * (line.startToBaseline + (1 - (int)alignItems / (int)FLEX_ALIGN_ITEMS.BASELINE) // baseline mode cancels origin offset | |
| * (GetExtentAlongAxis(widget, FLEX_AXIS.CROSS_AXIS, true) // snaps begin edge of widget to baseline | |
| - GetSizeAlongAxis(widget, FLEX_AXIS.CROSS_AXIS) * (int)alignItems / 2.0f))); | |
| mainCursor += padding + GetSizeAlongAxis(widget, FLEX_AXIS.MAIN_AXIS); | |
| } | |
| flexLines.Add(line); | |
| return line; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment