Resolve flexible widths in a flex row

Write codeHard45 min

Your task

A layout engine has measured a single-line flex row and now has to resolve each item's width. container is the row's inner width and gap the space between neighbouring items, so n items have n - 1 gaps. Each item is [basis, grow, shrink, min, max]: its flex basis, flex-grow, flex-shrink, minimum width and maximum width, where max is null (None in Python) when the item has no maximum. Every number is at least 0, min <= max whenever max is set, and items have no margins, borders or padding. Return the final widths in item order, following this procedure, a simplified form of the CSS algorithm for resolving flexible lengths:

  1. Hypothetical width: each item's basis clamped to its [min, max].
  2. Grow or shrink: if the hypothetical widths plus the gaps add up to strictly less than container, the row grows and each item's flex factor is its grow. Otherwise it shrinks and the factor is its shrink.
  3. Freeze what cannot flex: an item is frozen at its hypothetical width when its factor is 0, or when the row grows and its basis is larger than its hypothetical width, or when the row shrinks and its basis is smaller than its hypothetical width. Every other item is unfrozen.
  4. Free space always means container - gaps - (frozen items' widths) - (unfrozen items' bases). Compute it once now and keep it as the initial free space.
  5. Loop while any item is unfrozen:
    1. Compute the free space. If the unfrozen items' factors add up to less than 1, multiply the initial free space by that sum, and if the result is smaller in magnitude than the free space, use the result instead.
    2. Distribute it. When growing, each unfrozen item's width becomes basis + free * factor / (sum of unfrozen factors). When shrinking, each unfrozen item's scaled factor is shrink * basis and its width becomes basis - |free| * scaled / (sum of unfrozen scaled factors). If the free space is 0, or the scaled factors add up to 0, each unfrozen item's width is its basis.
    3. Clamp each unfrozen width to its [min, max] and add up the adjustments (clamped - unclamped). If the total is 0, freeze every unfrozen item. If it is positive, freeze only the items that min pushed up. If it is negative, freeze only the items that max pulled down.
  6. Return every item's width.

Widths are compared to within 1e-6.

Examples:

  • flex_widths(600, [[100, 1, 1, 0, None], [100, 2, 1, 0, None], [100, 1, 1, 0, None]], 0) returns [175, 250, 175]: 300 pixels of free space split 1 : 2 : 1.
  • flex_widths(600, [[100, 1, 1, 0, 150], [100, 1, 1, 0, None]], 0) returns [150, 450]: the first item stops at its maximum and the second takes the rest.
  • flex_widths(300, [[200, 0, 1, 0, None], [200, 0, 3, 0, None]], 0) returns [175, 125]: 100 pixels of overflow are taken 1 : 3.

Examples

flex_widths(container: float, items: list[list], gap: float) -> list[float]
Example 1
Inputflex_widths(600, [[100, 1, 1, 0, None], [100, 2, 1, 0, None], [100, 1, 1, 0, None]], 0)
Output[175, 250, 175]
Example 2
Inputflex_widths(600, [[100, 1, 1, 0, 150], [100, 1, 1, 0, None]], 0)
Output[150, 450]
Example 3
Inputflex_widths(300, [[200, 0, 1, 0, None], [200, 0, 3, 0, None]], 0)
Output[175, 125]