about summary refs log tree commit diff stats
path: root/src/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/engine.nim504
1 files changed, 379 insertions, 125 deletions
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index ca5e506f..a32decf0 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -88,18 +88,10 @@ type
 
   TableCaptionBoxBuilder = ref object of BlockBoxBuilder
 
-#TODO ?
-func stretch(sc: SizeConstraint): SizeConstraint =
-  case sc.t
-  of MIN_CONTENT, MAX_CONTENT:
-    return sc
-  of STRETCH, FIT_CONTENT:
-    return SizeConstraint(t: STRETCH, u: sc.u)
-
 func fitContent(sc: SizeConstraint): SizeConstraint =
   case sc.t
   of MIN_CONTENT, MAX_CONTENT:
-    return SizeConstraint(t: sc.t)
+    return sc
   of STRETCH, FIT_CONTENT:
     return SizeConstraint(t: FIT_CONTENT, u: sc.u)
 
@@ -1133,12 +1125,12 @@ proc resolveFloatSizes(lctx: LayoutState, containingWidth,
 # differs for the root height (TODO: and all heights in quirks mode) in that
 # it uses the lctx height. Therefore we pass percHeight as a separate
 # parameter. (TODO surely there is a better solution to this?)
-proc resolveSizes(lctx: LayoutState, containingWidth,
-    containingHeight: SizeConstraint, percHeight: Option[LayoutUnit],
-    computed: CSSComputedValues): ResolvedSizes =
+proc resolveSizes(lctx: LayoutState; containingWidth,
+    containingHeight: SizeConstraint; percHeight: Option[LayoutUnit];
+    computed: CSSComputedValues; flexItem = false): ResolvedSizes =
   if computed{"position"} == POSITION_ABSOLUTE:
     return lctx.resolveAbsoluteSizes(computed)
-  elif computed{"float"} != FLOAT_NONE:
+  elif computed{"float"} != FLOAT_NONE or flexItem:
     return lctx.resolveFloatSizes(containingWidth, containingHeight,
       percHeight, computed)
   else:
@@ -1159,12 +1151,14 @@ proc append(a: var Strut, b: LayoutUnit) =
 func sum(a: Strut): LayoutUnit =
   return a.pos + a.neg
 
-proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder],
-  space: AvailableSpace, computed: CSSComputedValues, offset,
-  bfcOffset: Offset): RootInlineFragment
-proc layoutBlock(bctx: var BlockContext, box: BlockBox,
-  builder: BlockBoxBuilder, sizes: ResolvedSizes)
-proc layoutTable(lctx: LayoutState, table: BlockBox, builder: TableBoxBuilder,
+proc layoutRootInline(bctx: var BlockContext; inlines: seq[BoxBuilder];
+  space: AvailableSpace; computed: CSSComputedValues;
+  offset, bfcOffset: Offset): RootInlineFragment
+proc layoutBlock(bctx: var BlockContext; box: BlockBox;
+  builder: BlockBoxBuilder; sizes: ResolvedSizes)
+proc layoutTable(lctx: LayoutState; table: BlockBox; builder: TableBoxBuilder;
+  sizes: ResolvedSizes)
+proc layoutFlex(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
   sizes: ResolvedSizes)
 
 # Note: padding must still be applied after this.
@@ -1335,14 +1329,11 @@ func establishesBFC(computed: CSSComputedValues): bool =
   return computed{"float"} != FLOAT_NONE or
     computed{"position"} == POSITION_ABSOLUTE or
     computed{"display"} in {DISPLAY_INLINE_BLOCK, DISPLAY_FLOW_ROOT} +
-      InternalTableBox
-    #TODO overflow, contain, flex, grid, multicol, column-span
+      InternalTableBox + {DISPLAY_FLEX, DISPLAY_INLINE_FLEX}
+    #TODO overflow, contain, grid, multicol, column-span
 
-proc layoutFlow(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder,
+proc layoutFlow(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
     sizes: ResolvedSizes) =
-  let isBfc = builder.computed.establishesBFC()
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.top)
   if builder.canFlushMargins(sizes):
     bctx.flushMargins(box)
     bctx.positionFloats()
@@ -1354,8 +1345,6 @@ proc layoutFlow(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder,
   else:
     # Builder only contains block boxes.
     bctx.layoutBlock(box, builder, sizes)
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.bottom)
 
 func toperc100(sc: SizeConstraint): Option[LayoutUnit] =
   if sc.isDefinite():
@@ -1381,6 +1370,8 @@ proc addInlineBlock(ictx: var InlineContext, state: var InlineState,
     bctx.layoutFlow(box, builder, sizes)
   of DISPLAY_INLINE_TABLE:
     lctx.layoutTable(box, TableBoxBuilder(builder), sizes)
+  of DISPLAY_INLINE_FLEX:
+    bctx.layoutFlex(box, builder, sizes)
   else:
     assert false, $builder.computed{"display"}
   bctx.positionFloats()
@@ -1451,7 +1442,7 @@ proc layoutInline(ictx: var InlineContext; box: InlineBoxBuilder):
     of DISPLAY_INLINE:
       let child = ictx.layoutInline(InlineBoxBuilder(child))
       state.fragment.children.add(child)
-    of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE:
+    of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_FLEX:
       # Note: we do not need a separate inline fragment here, because the tree
       # generator already does an iflush() before adding inline blocks.
       let w = fitContent(ictx.space.w)
@@ -1519,28 +1510,9 @@ proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder],
   root.xminwidth = ictx.minwidth
   return root
 
-proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace,
-    lctx: LayoutState): RootInlineFragment =
-  let space = AvailableSpace(
-    w: fitContent(space.w),
-    h: space.h
-  )
-  #TODO we should put markers right before the first atom of the parent
-  # list item or something...
-  var bctx = BlockContext(lctx: lctx)
-  let children = @[BoxBuilder(builder)]
-  return bctx.layoutRootInline(children, space, builder.computed, Offset(),
-    Offset())
-
 # Build a block box without establishing a new block formatting context.
-proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let lctx = bctx.lctx
-  let availableWidth = space.w
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildBlock(bctx: var BlockContext; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
   let box = BlockBox(
     computed: builder.computed,
     node: builder.node,
@@ -1550,14 +1522,8 @@ proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder,
   bctx.layoutFlow(box, builder, sizes)
   return box
 
-proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder,
-    space: AvailableSpace, offset: Offset): ListItemBox =
-  let availableWidth = stretch(space.w)
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let lctx = bctx.lctx
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildListItem(bctx: var BlockContext; builder: ListItemBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): ListItemBox =
   let box = ListItemBox(
     computed: builder.computed,
     node: builder.node,
@@ -1565,31 +1531,25 @@ proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder,
     margin: sizes.margin
   )
   if builder.marker != nil:
-    box.marker = buildMarker(builder.marker, sizes.space, lctx)
+    #TODO we should put markers right before the first atom of the parent
+    # list item or something...
+    var bctx = BlockContext(lctx: bctx.lctx)
+    let children = @[BoxBuilder(builder.marker)]
+    let space = AvailableSpace(w: fitContent(sizes.space.w), h: sizes.space.h)
+    box.marker = bctx.layoutRootInline(children, space, builder.marker.computed,
+      Offset(), Offset())
   bctx.layoutFlow(box, builder.content, sizes)
   return box
 
-proc buildTable(bctx: var BlockContext, builder: TableBoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let availableWidth = fitContent(space.w)
-  let availableHeight = maxContent() #TODO fit-content when clip
-  let percHeight = space.h.toPercSize()
-  let lctx = bctx.lctx
-  let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight,
-    builder.computed)
+proc buildTable(bctx: var BlockContext; builder: TableBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
   let box = BlockBox(
     computed: builder.computed,
     node: builder.node,
     offset: Offset(x: offset.x + sizes.margin.left, y: offset.y),
     margin: sizes.margin
   )
-  let isBfc = builder.computed.establishesBFC()
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.top)
-  bctx.flushMargins(box)
-  lctx.layoutTable(box, builder, sizes)
-  if not isBfc:
-    bctx.marginTodo.append(sizes.margin.bottom)
+  bctx.lctx.layoutTable(box, builder, sizes)
   return box
 
 proc positionAbsolute(lctx: LayoutState, box: BlockBox, margin: RelativeRect) =
@@ -1974,9 +1934,8 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit,
     weight: var float64): seq[int] =
   # Spacing for each column:
   var avail = newSeqUninitialized[int](ctx.cols.len)
-  var i = 0
   var j = 0
-  while i < ctx.cols.len:
+  for i in 0 ..< ctx.cols.len:
     if not ctx.cols[i].wspecified:
       avail[j] = i
       let colw = ctx.cols[i].width
@@ -1990,7 +1949,6 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit,
     else:
       W -= ctx.cols[i].width
       avail.del(j)
-    inc i
   return avail
 
 func needsRedistribution(ctx: TableContext, computed: CSSComputedValues): bool =
@@ -2121,36 +2079,295 @@ proc postAlignChild(box, child: BlockBox, width: LayoutUnit) =
   else:
     discard
 
-# Build an outer block box inside an existing block formatting context.
-proc layoutBlockChild(bctx: var BlockContext, builder: BoxBuilder,
-    space: AvailableSpace, offset: Offset): BlockBox =
-  let child = case builder.computed{"display"}
+proc buildFlex(bctx: var BlockContext; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes; offset: Offset): BlockBox =
+  let box = BlockBox(
+    computed: builder.computed,
+    node: builder.node,
+    offset: Offset(x: offset.x + sizes.margin.left, y: offset.y),
+    margin: sizes.margin
+  )
+  bctx.layoutFlex(box, builder, sizes)
+  return box
+
+proc layoutFlexChild(lctx: LayoutState; builder: BoxBuilder;
+    sizes: ResolvedSizes): BlockBox =
+  var bctx = BlockContext(lctx: lctx)
+  # note: we do not append margins here, since those belong to the flex item,
+  # not its inner BFC.
+  var offset = Offset()
+  let box = case builder.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
-    bctx.buildBlock(BlockBoxBuilder(builder), space, offset)
+    bctx.buildBlock(BlockBoxBuilder(builder), sizes, offset)
   of DISPLAY_LIST_ITEM:
-    bctx.buildListItem(ListItemBoxBuilder(builder), space, offset)
+    bctx.buildListItem(ListItemBoxBuilder(builder), sizes, offset)
   of DISPLAY_TABLE:
-    bctx.buildTable(TableBoxBuilder(builder), space, offset)
+    bctx.buildTable(TableBoxBuilder(builder), sizes, offset)
+  of DISPLAY_FLEX:
+    bctx.buildFlex(BlockBoxBuilder(builder), sizes, offset)
   else:
     assert false, "builder.t is " & $builder.computed{"display"}
     BlockBox(nil)
-  return child
+  return box
 
-# Establish a new block formatting context and build a block box.
-proc layoutRootBlock(lctx: LayoutState, builder: BoxBuilder,
-    space: AvailableSpace, offset: Offset, marginBottomOut: var LayoutUnit):
-    BlockBox =
-  var bctx = BlockContext(lctx: lctx)
+type
+  FlexWeightType = enum
+    fwtGrow, fwtShrink
+
+  FlexPendingItem = object
+    child: BlockBox
+    builder: BoxBuilder
+    weights: array[FlexWeightType, float64]
+    space: AvailableSpace
+    sizes: ResolvedSizes
+
+  FlexMainContext = object
+    offset: Offset
+    totalSize: Size
+    maxSize: Size
+    totalWeight: array[FlexWeightType, float64]
+    lctx: LayoutState
+    pending: seq[FlexPendingItem]
+
+const FlexReverse = {FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN_REVERSE}
+const FlexRow = {FLEX_DIRECTION_ROW, FLEX_DIRECTION_ROW_REVERSE}
+
+proc redistributeWidth(mctx: var FlexMainContext; sizes: ResolvedSizes) =
+  #TODO actually use flex-basis
+  let lctx = mctx.lctx
+  if sizes.space.w.isDefinite:
+    var diff = sizes.space.w.u - mctx.totalSize.w
+    let wt = if diff > 0: fwtGrow else: fwtShrink
+    var totalWeight = mctx.totalWeight[wt]
+    while (wt == fwtGrow and diff > 0 or wt == fwtShrink and diff < 0) and
+        totalWeight > 0:
+      mctx.maxSize.h = 0 # redo maxSize calculation; we only need height here
+      let unit = diff / totalWeight
+      # reset total weight & available diff for the next iteration (if there is one)
+      totalWeight = 0
+      diff = 0
+      for it in mctx.pending.mitems:
+        let builder = it.builder
+        if it.weights[wt] == 0:
+          mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+          continue
+        var w = it.child.size.w + unit * it.weights[wt]
+        # check for min/max violation
+        let minw = max(it.child.xminwidth, it.sizes.minWidth)
+        if minw > w:
+          # min violation
+          if wt == fwtShrink: # freeze
+            diff += w - minw
+            it.weights[wt] = 0
+          w = minw
+        let maxw = it.sizes.maxWidth
+        if maxw < w:
+          # max violation
+          if wt == fwtGrow: # freeze
+            diff += w - maxw
+            it.weights[wt] = 0
+          w = maxw
+        it.space.w = stretch(w)
+        it.sizes = lctx.resolveSizes(it.space.w, it.space.h,
+          it.space.h.toPercSize(), builder.computed)
+        totalWeight += it.weights[wt]
+        #TODO we should call this only on freeze, and then put another loop to
+        # the end for non-freezed items
+        it.child = lctx.layoutFlexChild(builder, it.sizes)
+        mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+
+proc redistributeHeight(mctx: var FlexMainContext; sizes: ResolvedSizes) =
+  let lctx = mctx.lctx
+  if sizes.space.h.isDefinite and mctx.totalSize.h != sizes.space.h.u:
+    var diff = sizes.space.h.u - mctx.totalSize.h
+    let wt = if diff > 0: fwtGrow else: fwtShrink
+    var totalWeight = mctx.totalWeight[wt]
+    while (wt == fwtGrow and diff > 0 or wt == fwtShrink and diff < 0) and
+        totalWeight > 0:
+      mctx.maxSize.w = 0 # redo maxSize calculation; we only need height here
+      let unit = diff / totalWeight
+      # reset total weight & available diff for the next iteration (if there is one)
+      totalWeight = 0
+      diff = 0
+      for it in mctx.pending.mitems:
+        let builder = it.builder
+        if it.weights[wt] == 0:
+          mctx.maxSize.w = max(mctx.maxSize.w, it.child.size.w)
+          continue
+        var h = max(it.child.size.h + unit * it.weights[wt], 0)
+        # check for min/max violation
+        let minh = it.sizes.minHeight
+        if minh > h:
+          # min violation
+          if wt == fwtShrink: # freeze
+            diff += h - minh
+            it.weights[wt] = 0
+          h = minh
+        let maxh = it.sizes.maxHeight
+        if maxh < h:
+          # max violation
+          if wt == fwtGrow: # freeze
+            diff += h - maxh
+            it.weights[wt] = 0
+          h = maxh
+        it.space.h = stretch(h)
+        it.sizes = lctx.resolveSizes(it.space.w, it.space.h,
+          it.space.h.toPercSize(), builder.computed)
+        totalWeight += it.weights[wt]
+        it.child = lctx.layoutFlexChild(builder, it.sizes)
+        mctx.maxSize.h = max(mctx.maxSize.h, it.child.size.h)
+
+proc flushRow(mctx: var FlexMainContext; box: BlockBox; sizes: ResolvedSizes;
+    totalMaxSize: var Size) =
+  let lctx = mctx.lctx
+  mctx.redistributeWidth(sizes)
+  let h = stretch(mctx.maxSize.h)
+  var offset = mctx.offset
+  for it in mctx.pending.mitems:
+    if it.child.size.h < mctx.maxSize.h and not it.sizes.space.h.isDefinite:
+      # if the max height is greater than our height, then take max height
+      # instead. (if the box's available height is definite, then this will
+      # change nothing, so we skip it as an optimization.)
+      it.sizes.space.h = h
+      it.child = lctx.layoutFlexChild(it.builder, it.sizes)
+    it.child.offset = Offset(
+      x: it.child.offset.x + offset.x,
+      # margins are added here, since they belong to the flex item.
+      y: it.child.offset.y + offset.y + it.child.margin.top +
+        it.child.margin.bottom
+    )
+    offset.x += it.child.size.w
+    box.nested.add(it.child)
+  totalMaxSize.w = max(totalMaxSize.w, offset.x)
+  mctx = FlexMainContext(
+    lctx: mctx.lctx,
+    offset: Offset(
+      x: mctx.offset.x,
+      y: mctx.offset.y + mctx.maxSize.h
+    )
+  )
+
+proc flushColumn(mctx: var FlexMainContext; box: BlockBox;
+    sizes: ResolvedSizes; totalMaxSize: var Size) =
+  let lctx = mctx.lctx
+  mctx.redistributeHeight(sizes)
+  let w = stretch(mctx.maxSize.w)
+  var offset = mctx.offset
+  for it in mctx.pending.mitems:
+    if it.child.size.w < mctx.maxSize.w and not it.sizes.space.w.isDefinite:
+      # see above.
+      it.sizes.space.w = w
+      it.child = lctx.layoutFlexChild(it.builder, it.sizes)
+    # margins belong to the flex item, and influence its positioning
+    offset.y += it.child.margin.top
+    it.child.offset = Offset(
+      x: it.child.offset.x + offset.x,
+      y: it.child.offset.y + offset.y
+    )
+    offset.y += it.child.margin.bottom
+    offset.y += it.child.size.h
+    box.nested.add(it.child)
+  totalMaxSize.h = max(totalMaxSize.h, offset.y)
+  mctx = FlexMainContext(
+    lctx: lctx,
+    offset: Offset(
+      x: mctx.offset.x + mctx.maxSize.w,
+      y: mctx.offset.y
+    )
+  )
+
+proc layoutFlex(bctx: var BlockContext; box: BlockBox; builder: BlockBoxBuilder;
+    sizes: ResolvedSizes) =
+  assert not builder.inlinelayout
+  let lctx = bctx.lctx
+  var i = 0
+  var mctx = FlexMainContext(lctx: lctx)
+  let flexDir = builder.computed{"flex-direction"}
+  let children = if builder.computed{"flex-direction"} in FlexReverse:
+    builder.children.reversed()
+  else:
+    builder.children
+  var totalMaxSize = Size() #TODO find a better name for this
+  let canWrap = box.computed{"flex-wrap"} != FLEX_WRAP_NOWRAP
+  let percHeight = sizes.space.h.toPercSize()
+  while i < children.len:
+    let builder = children[i]
+    let childSizes = lctx.resolveFloatSizes(sizes.space.w, sizes.space.h,
+      percHeight, builder.computed)
+    let child = lctx.layoutFlexChild(builder, childSizes)
+    if flexDir in FlexRow:
+      if canWrap and (sizes.space.w.t == MIN_CONTENT or
+          sizes.space.w.isDefinite and
+          mctx.totalSize.w + child.size.w > sizes.space.w.u):
+        mctx.flushRow(box, sizes, totalMaxSize)
+      mctx.totalSize.w += child.size.w
+    else:
+      if canWrap and (sizes.space.h.t == MIN_CONTENT or
+          sizes.space.h.isDefinite and
+          mctx.totalSize.h + child.size.h > sizes.space.h.u):
+        mctx.flushRow(box, sizes, totalMaxSize)
+      mctx.totalSize.h += child.size.h
+    mctx.maxSize.w = max(mctx.maxSize.w, child.size.w)
+    mctx.maxSize.h = max(mctx.maxSize.h, child.size.h)
+    let grow = builder.computed{"flex-grow"}
+    let shrink = builder.computed{"flex-shrink"}
+    mctx.totalWeight[fwtGrow] += grow
+    mctx.totalWeight[fwtShrink] += shrink
+    mctx.pending.add(FlexPendingItem(
+      child: child,
+      builder: builder,
+      weights: [grow, shrink],
+      space: sizes.space,
+      sizes: childSizes
+    ))
+    inc i # need to increment index here for needsGrow
+  if flexDir in FlexRow:
+    if mctx.pending.len > 0:
+      mctx.flushRow(box, sizes, totalMaxSize)
+    box.applyWidth(sizes, totalMaxSize.w)
+    box.applyHeight(sizes, mctx.offset.y)
+  else:
+    if mctx.pending.len > 0:
+      mctx.flushColumn(box, sizes, totalMaxSize)
+    box.applyWidth(sizes, mctx.offset.x)
+    box.applyHeight(sizes, totalMaxSize.h)
+
+# Build an outer block box inside an existing block formatting context.
+proc layoutBlockChild(bctx: var BlockContext; builder: BoxBuilder;
+    space: AvailableSpace; offset: Offset; appendMargins: bool): BlockBox =
+  let availHeight = maxContent() #TODO also fit-content when clip
+  let availWidth = if builder.computed{"display"} == DISPLAY_TABLE:
+    fitContent(space.w)
+  else:
+    space.w
+  let sizes = bctx.lctx.resolveSizes(availWidth, availHeight,
+    space.h.toPercSize(), builder.computed)
+  if appendMargins:
+    # for nested blocks that do not establish their own BFC, and thus take part
+    # in margin collapsing.
+    bctx.marginTodo.append(sizes.margin.top)
   let box = case builder.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
-    bctx.buildBlock(BlockBoxBuilder(builder), space, offset)
+    bctx.buildBlock(BlockBoxBuilder(builder), sizes, offset)
   of DISPLAY_LIST_ITEM:
-    bctx.buildListItem(ListItemBoxBuilder(builder), space, offset)
+    bctx.buildListItem(ListItemBoxBuilder(builder), sizes, offset)
   of DISPLAY_TABLE:
-    bctx.buildTable(TableBoxBuilder(builder), space, offset)
+    bctx.buildTable(TableBoxBuilder(builder), sizes, offset)
+  of DISPLAY_FLEX:
+    bctx.buildFlex(BlockBoxBuilder(builder), sizes, offset)
   else:
     assert false, "builder.t is " & $builder.computed{"display"}
     BlockBox(nil)
+  if appendMargins:
+    bctx.marginTodo.append(sizes.margin.bottom)
+  return box
+
+# Establish a new block formatting context and build a block box.
+proc layoutRootBlock(lctx: LayoutState; builder: BoxBuilder;
+    space: AvailableSpace; offset: Offset; marginBottomOut: var LayoutUnit):
+    BlockBox =
+  var bctx = BlockContext(lctx: lctx)
+  let box = bctx.layoutBlockChild(builder, space, offset, appendMargins = false)
   bctx.positionFloats()
   marginBottomOut = bctx.marginTodo.sum()
   # If the highest float edge is higher than the box itself, set that as
@@ -2227,7 +2444,8 @@ proc layoutBlockChildren(state: var BlockState, bctx: var BlockContext,
       # of margin todo in bctx2 (margin-bottom) + height.
       dy = child.offset.y - state.offset.y + child.size.h + marginBottomOut
     else:
-      child = bctx.layoutBlockChild(builder, state.space, state.offset)
+      child = bctx.layoutBlockChild(builder, state.space, state.offset,
+        appendMargins = true)
       # delta y is difference between old and new offsets (margin-top),
       # plus height.
       dy = child.offset.y - state.offset.y + child.size.h
@@ -2406,7 +2624,7 @@ proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} =
     DISPLAY_INLINE_BLOCK}, $box.computed{"display"}
   blockgroup.boxes.add(box)
 
-proc flush(blockgroup: var BlockGroup) {.inline.} =
+proc flush(blockgroup: var BlockGroup) =
   if blockgroup.boxes.len > 0:
     assert blockgroup.parent.computed{"display"} != DISPLAY_INLINE
     let computed = blockgroup.parent.computed.inheritProperties()
@@ -2426,7 +2644,7 @@ func canGenerateAnonymousInline(blockgroup: BlockGroup,
 
 proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup =
   assert parent.computed{"display"} != DISPLAY_INLINE
-  result.parent = parent
+  return BlockGroup(parent: parent)
 
 proc generateTableBox(styledNode: StyledNode, lctx: LayoutState,
   parent: var InnerBlockContext): TableBoxBuilder
@@ -2441,12 +2659,18 @@ proc generateTableCaptionBox(styledNode: StyledNode, lctx: LayoutState,
 proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
   marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil):
   BlockBoxBuilder
+proc generateFlexBox(styledNode: StyledNode; lctx: LayoutState;
+  parent: ptr InnerBlockContext = nil): BlockBoxBuilder
 proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode)
 
-proc generateBlockBox(pctx: var InnerBlockContext, styledNode: StyledNode,
+proc generateBlockBox(pctx: var InnerBlockContext; styledNode: StyledNode;
     marker = none(MarkerBoxBuilder)): BlockBoxBuilder =
   return generateBlockBox(styledNode, pctx.lctx, marker, addr pctx)
 
+proc generateFlexBox(pctx: var InnerBlockContext; styledNode: StyledNode):
+    BlockBoxBuilder =
+  return generateFlexBox(styledNode, pctx.lctx, addr pctx)
+
 proc flushTableRow(ctx: var InnerBlockContext) =
   if ctx.anonRow != nil:
     if ctx.blockgroup.parent.computed{"display"} == DISPLAY_TABLE_ROW:
@@ -2502,15 +2726,19 @@ proc reconstructInlineParents(ctx: var InnerBlockContext): InlineBoxBuilder =
     parent = nbox
   return parent
 
-proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
+proc generateFromElem(ctx: var InnerBlockContext; styledNode: StyledNode) =
   let box = ctx.blockgroup.parent
-
   case styledNode.computed{"display"}
   of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT:
     ctx.iflush()
     ctx.flush()
     let childbox = ctx.generateBlockBox(styledNode)
     box.children.add(childbox)
+  of DISPLAY_FLEX:
+    ctx.iflush()
+    ctx.flush()
+    let childbox = ctx.generateFlexBox(styledNode)
+    box.children.add(childbox)
   of DISPLAY_LIST_ITEM:
     ctx.flush()
     inc ctx.listItemCounter
@@ -2528,7 +2756,7 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
     box.children.add(childbox)
   of DISPLAY_INLINE:
     ctx.generateInlineBoxes(styledNode)
-  of DISPLAY_INLINE_BLOCK:
+  of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_FLEX:
     # create a new inline box that we can safely put our inline block into
     ctx.iflush()
     let computed = styledNode.computed.inheritProperties()
@@ -2539,7 +2767,14 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
       ctx.iroot = iparent
     else:
       ctx.iroot = ctx.ibox
-    let childbox = ctx.generateBlockBox(styledNode)
+    var childbox: BoxBuilder
+    if styledNode.computed{"display"} == DISPLAY_INLINE_BLOCK:
+      childbox = ctx.generateBlockBox(styledNode)
+    elif styledNode.computed{"display"} == DISPLAY_INLINE_TABLE:
+      childbox = styledNode.generateTableBox(ctx.lctx, ctx)
+    else:
+      assert styledNode.computed{"display"} == DISPLAY_INLINE_FLEX
+      childbox = ctx.generateFlexBox(styledNode)
     ctx.ibox.children.add(childbox)
     ctx.iflush()
   of DISPLAY_TABLE:
@@ -2584,20 +2819,6 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
         wrappervals{"display"} = DISPLAY_TABLE_ROW
         ctx.anonRow = TableRowBoxBuilder(computed: wrappervals)
       ctx.anonRow.children.add(childbox)
-  of DISPLAY_INLINE_TABLE:
-    # create a new inline box that we can safely put our inline block into
-    ctx.iflush()
-    let computed = styledNode.computed.inheritProperties()
-    ctx.ibox = InlineBoxBuilder(computed: computed, node: styledNode)
-    if ctx.inlineStack.len > 0:
-      let iparent = ctx.reconstructInlineParents()
-      iparent.children.add(ctx.ibox)
-      ctx.iroot = iparent
-    else:
-      ctx.iroot = ctx.ibox
-    let childbox = styledNode.generateTableBox(ctx.lctx, ctx)
-    ctx.ibox.children.add(childbox)
-    ctx.iflush()
   of DISPLAY_TABLE_CAPTION:
     ctx.bflush()
     ctx.flushTableRow()
@@ -2703,19 +2924,20 @@ proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) =
 
 proc newInnerBlockContext(styledNode: StyledNode, box: BlockBoxBuilder,
     lctx: LayoutState, parent: ptr InnerBlockContext): InnerBlockContext =
-  result = InnerBlockContext(
+  var ctx = InnerBlockContext(
     styledNode: styledNode,
     blockgroup: newBlockGroup(box),
     lctx: lctx,
     parent: parent
   )
   if parent != nil:
-    result.listItemCounter = parent[].listItemCounter
-    result.quoteLevel = parent[].quoteLevel
+    ctx.listItemCounter = parent[].listItemCounter
+    ctx.quoteLevel = parent[].quoteLevel
   for reset in styledNode.computed{"counter-reset"}:
     if reset.name == "list-item":
-      result.listItemCounter = reset.num
-      result.listItemReset = true
+      ctx.listItemCounter = reset.num
+      ctx.listItemReset = true
+  return ctx
 
 proc generateInnerBlockBox(ctx: var InnerBlockContext) =
   let box = ctx.blockgroup.parent
@@ -2732,25 +2954,21 @@ proc generateInnerBlockBox(ctx: var InnerBlockContext) =
       ctx.generateReplacement(child, ctx.styledNode)
   ctx.iflush()
 
-proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
-    marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil):
+proc generateBlockBox(styledNode: StyledNode; lctx: LayoutState;
+    marker = none(MarkerBoxBuilder); parent: ptr InnerBlockContext = nil):
     BlockBoxBuilder =
   let box = BlockBoxBuilder(computed: styledNode.computed)
   box.node = styledNode
   var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
-
   if marker.isSome:
     ctx.ibox = marker.get
     ctx.iflush()
-
   ctx.generateInnerBlockBox()
-
   # Flush anonymous tables here, to avoid setting inline layout with tables.
   ctx.flushTableRow()
   ctx.flushTable()
   # (flush here, because why not)
   ctx.flushInherit()
-
   # Avoid unnecessary anonymous block boxes. This also helps set our layout to
   # inline even if no inner anonymous block was generated.
   if box.children.len == 0:
@@ -2760,6 +2978,42 @@ proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState,
   ctx.blockgroup.flush()
   return box
 
+proc generateFlexBox(styledNode: StyledNode; lctx: LayoutState;
+    parent: ptr InnerBlockContext = nil): BlockBoxBuilder =
+  let box = BlockBoxBuilder(computed: styledNode.computed, node: styledNode)
+  var ctx = newInnerBlockContext(styledNode, box, lctx, parent)
+  assert box.computed{"display"} != DISPLAY_INLINE
+  for child in ctx.styledNode.children:
+    case child.t
+    of STYLED_ELEMENT:
+      ctx.iflush()
+      let display = child.computed{"display"}.blockify()
+      if display != child.computed{"display"}:
+        #TODO this is a hack.
+        # it exists because passing down a different `computed' would need
+        # changes in way too many procedures, which I am not ready to make yet.
+        let newChild = StyledNode()
+        newChild[] = child[]
+        newChild.computed = child.computed.copyProperties()
+        newChild.computed{"display"} = display
+        ctx.generateFromElem(newChild)
+      else:
+        ctx.generateFromElem(child)
+    of STYLED_TEXT:
+      if ctx.blockgroup.canGenerateAnonymousInline(box.computed, child.text):
+        ctx.generateAnonymousInlineText(child.text, ctx.styledNode)
+    of STYLED_REPLACEMENT:
+      ctx.generateReplacement(child, ctx.styledNode)
+  ctx.iflush()
+  # Flush anonymous tables here, to avoid setting inline layout with tables.
+  ctx.flushTableRow()
+  ctx.flushTable()
+  # (flush here, because why not)
+  ctx.flushInherit()
+  ctx.blockgroup.flush()
+  assert not box.inlinelayout
+  return box
+
 proc generateTableCellBox(styledNode: StyledNode, lctx: LayoutState,
     parent: var InnerBlockContext): TableCellBoxBuilder =
   let box = TableCellBoxBuilder(computed: styledNode.computed)