about summary refs log tree commit diff stats
path: root/src/css
diff options
context:
space:
mode:
Diffstat (limited to 'src/css')
-rw-r--r--src/css/box.nim4
-rw-r--r--src/css/cascade.nim21
-rw-r--r--src/css/cssvalues.nim18
-rw-r--r--src/css/layout.nim583
-rw-r--r--src/css/render.nim8
5 files changed, 356 insertions, 278 deletions
diff --git a/src/css/box.nim b/src/css/box.nim
index 37d9a2d3..6ec56c38 100644
--- a/src/css/box.nim
+++ b/src/css/box.nim
@@ -39,9 +39,6 @@ type
     # bottom margin result
     marginBottom*: LUnit
 
-  SplitType* = enum
-    stSplitStart, stSplitEnd
-
   Area* = object
     offset*: Offset
     size*: Size
@@ -59,7 +56,6 @@ type
     render*: BoxRenderState
     computed*: CSSValues
     node*: Element
-    splitType*: set[SplitType]
     case t*: InlineBoxType
     of ibtParent:
       children*: seq[InlineBox]
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 97066a51..20575f0d 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -405,7 +405,7 @@ when defined(debug):
   func `$`*(node: StyledNode): string =
     case node.t
     of stText:
-      return "#text " & node.text.data
+      return node.text.data
     of stElement:
       if node.pseudo != peNone:
         return $node.element.tagType & "::" & $node.pseudo
@@ -484,3 +484,22 @@ iterator children*(styledNode: StyledNode): StyledNode {.closure.} =
     let parent = styledNode.element
     for content in parent.computedMap[styledNode.pseudo]{"content"}:
       yield parent.initStyledReplacement(content)
+
+when defined(debug):
+  proc computedTree*(styledNode: StyledNode): string =
+    result = ""
+    if styledNode.t != stElement:
+      result &= $styledNode
+    else:
+      result &= "<"
+      if styledNode.computed{"display"} != DisplayInline:
+        result &= "div"
+      else:
+        result &= "span"
+      let computed = styledNode.computed.copyProperties()
+      if computed{"display"} == DisplayBlock:
+        computed{"display"} = DisplayInline
+      result &= " style='" & $computed.serializeEmpty() & "'>\n"
+      for it in styledNode.children:
+        result &= it.computedTree()
+      result &= "\n</div>"
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index 2d4151b4..2c02ce14 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -537,6 +537,12 @@ const InheritedProperties = {
 const OverflowScrollLike* = {OverflowScroll, OverflowAuto, OverflowOverlay}
 const OverflowHiddenLike* = {OverflowHidden, OverflowClip}
 const FlexReverse* = {FlexDirectionRowReverse, FlexDirectionColumnReverse}
+const DisplayOuterInline* = {
+  DisplayInlineBlock, DisplayInlineTableWrapper, DisplayInlineFlex
+}
+const DisplayInnerFlex* = {
+  DisplayFlex, DisplayInlineFlex
+}
 
 # Forward declarations
 proc parseValue(cvals: openArray[CSSComponentValue]; t: CSSPropertyType;
@@ -1868,3 +1874,15 @@ func splitTable*(computed: CSSValues):
       outerComputed.setInitial(t)
   outerComputed{"display"} = computed{"display"}
   return (outerComputed, innerComputed)
+
+when defined(debug):
+  func `serializeEmpty`*(computed: CSSValues): string =
+    let default = rootProperties()
+    result = ""
+    for p in CSSPropertyType:
+      let a = computed.serialize(p)
+      let b = default.serialize(p)
+      if a != b:
+        result &= $p & ':'
+        result &= a
+        result &= ';'
diff --git a/src/css/layout.nim b/src/css/layout.nim
index 96f40a30..d4861f19 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -116,6 +116,15 @@ func fitContent(sc: SizeConstraint): SizeConstraint =
 func isDefinite(sc: SizeConstraint): bool =
   return sc.t in {scStretch, scFitContent}
 
+# Note: this does not include display types that cannot appear as block
+# children.
+func establishesBFC(computed: CSSValues): bool =
+  return computed{"float"} != FloatNone or
+    computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayTableWrapper,
+      DisplayFlex} or
+    computed{"overflow-x"} notin {OverflowVisible, OverflowClip}
+    #TODO contain, grid, multicol, column-span
+
 # 2nd pass: layout
 func canpx(l: CSSLength; sc: SizeConstraint): bool =
   return l.u != clAuto and (l.u != clPerc or sc.t == scStretch)
@@ -256,13 +265,26 @@ type
 
   InlineState = object
     box: InlineBox
-    startOffsetTop: Offset
     # we do not want to collapse newlines over tag boundaries, so these are
     # in state
     lastrw: int # last rune width of the previous word
     firstrw: int # first rune width of the current word
     prevrw: int # last processed rune's width
 
+  BlockState = object
+    offset: Offset
+    maxChildWidth: LUnit
+    totalFloatWidth: LUnit # used for re-layouts
+    space: AvailableSpace
+    intr: Size
+    prevParentBps: BlockPositionState
+    # State kept for when a re-layout is necessary:
+    oldMarginTodo: Strut
+    oldExclusionsLen: int
+    initialMarginTarget: BlockPositionState
+    initialTargetOffset: Offset
+    initialParentOffset: Offset
+
 func whitespacepre(computed: CSSValues): bool =
   computed{"white-space"} in {WhitespacePre, WhitespacePreLine,
     WhitespacePreWrap}
@@ -587,7 +609,7 @@ proc finishLine(ictx: var InlineContext; state: var InlineState; wrap: bool;
       ictx.firstBaselineSet = true
     ictx.state.size.h += ictx.lbstate.size.h
     ictx.state.intr.h += ictx.lbstate.intrh
-    let lineWidth = if wrap:
+    let lineWidth = if wrap and ictx.space.w.isDefinite():
       ictx.lbstate.availableWidth
     else:
       ictx.lbstate.size.w
@@ -1178,9 +1200,14 @@ func sum(a: Strut): LUnit =
 proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes)
 proc layoutTableWrapper(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes)
 proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes)
-proc layoutInline(ictx: var InlineContext; box: InlineBox)
+proc layoutInline(ictx: var InlineContext; ibox: InlineBox)
 proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset;
   sizes: ResolvedSizes; includeMargin = false)
+proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
+    canClear: bool)
+proc initBlockPositionStates(state: var BlockState; bctx: var BlockContext;
+  box: BlockBox)
+func isParentResolved(state: BlockState; bctx: BlockContext): bool
 
 # Note: padding must still be applied after this.
 proc applySize(box: BlockBox; sizes: ResolvedSizes;
@@ -1235,11 +1262,27 @@ func canFlushMargins(box: BlockBox; sizes: ResolvedSizes): bool =
     box.inline != nil or box.computed{"display"} notin DisplayBlockLike or
     box.computed{"clear"} != ClearNone
 
-proc flushMargins(bctx: var BlockContext; box: BlockBox) =
+# Return true if no more margin collapsing can occur for the current strut.
+func canFlushMargins(ibox: InlineBox; padding: Span): bool =
+  if ibox.computed{"position"} in {PositionAbsolute, PositionFixed}:
+    return false
+  if padding.start != 0 or padding.send != 0:
+    return true
+  case ibox.t
+  of ibtParent:
+    return ibox.children.len == 0
+  of ibtBox:
+    return ibox.box.computed{"display"} in DisplayOuterInline and
+      ibox.box.computed{"float"} == FloatNone and
+      ibox.box.computed{"position"} notin {PositionAbsolute, PositionFixed}
+  of ibtBitmap, ibtText, ibtNewline:
+    return true
+
+proc flushMargins(bctx: var BlockContext; offsety: var LUnit) =
   # Apply uncommitted margins.
   let margin = bctx.marginTodo.sum()
   if bctx.marginTarget == nil:
-    box.state.offset.y += margin
+    offsety += margin
   else:
     if bctx.marginTarget.box != nil:
       bctx.marginTarget.box.state.offset.y += margin
@@ -1302,21 +1345,18 @@ proc queueAbsolute(lctx: LayoutContext; box: BlockBox; offset: Offset) =
     lctx.positioned[0].queue.add(QueuedAbsolute(child: box, offset: offset))
   else: assert false
 
-type
-  BlockState = object
-    offset: Offset
-    maxChildWidth: LUnit
-    totalFloatWidth: LUnit # used for re-layouts
-    space: AvailableSpace
-    intr: Size
-    prevParentBps: BlockPositionState
-    # State kept for when a re-layout is necessary:
-    oldMarginTodo: Strut
-    oldExclusionsLen: int
-    initialMarginTarget: BlockPositionState
-    initialTargetOffset: Offset
-    initialParentOffset: Offset
-    relativeChildren: seq[BlockBox]
+proc positionRelative(lctx: LayoutContext; space: AvailableSpace;
+    box: BlockBox) =
+  # Interestingly, relative percentages don't actually work when the
+  # parent's height is auto.
+  if box.computed{"left"}.canpx(space.w):
+    box.state.offset.x += box.computed{"left"}.px(space.w)
+  elif box.computed{"right"}.canpx(space.w):
+    box.state.offset.x -= box.computed{"right"}.px(space.w)
+  if box.computed{"top"}.canpx(space.h):
+    box.state.offset.y += box.computed{"top"}.px(space.h)
+  elif box.computed{"bottom"}.canpx(space.h):
+    box.state.offset.y -= box.computed{"bottom"}.px(space.h)
 
 func findNextFloatOffset(bctx: BlockContext; offset: Offset; size: Size;
     space: AvailableSpace; float: CSSFloat; outw: var LUnit): Offset =
@@ -1384,10 +1424,14 @@ proc positionFloats(bctx: var BlockContext) =
 proc layoutInline(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   if box.computed{"position"} != PositionStatic:
     bctx.lctx.pushPositioned()
-  let bfcOffset = if bctx.parentBps != nil:
-    bctx.parentBps.offset + box.state.offset
-  else: # this block establishes a new BFC.
-    offset(0, 0)
+  var state = BlockState(
+    offset: sizes.padding.topLeft,
+    space: sizes.space,
+    oldMarginTodo: bctx.marginTodo,
+    oldExclusionsLen: bctx.exclusions.len
+  )
+  state.initBlockPositionStates(bctx, box)
+  let bfcOffset = state.initialParentOffset
   var ictx = bctx.initInlineContext(sizes.space, bfcOffset, sizes.padding,
     box.computed)
   ictx.initLine()
@@ -1434,6 +1478,14 @@ proc layoutInline(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   box.state.size += paddingSum
   box.state.baseline = ictx.state.baseline
   box.state.firstBaseline = ictx.state.firstBaseline
+  if state.isParentResolved(bctx):
+    # Our offset has already been resolved, ergo any margins in marginTodo will
+    # be passed onto the next box. Set marginTarget to nil, so that if we (or
+    # one of our ancestors) were still set as a marginTarget, we no longer are.
+    bctx.positionFloats()
+    bctx.marginTarget = nil
+  # Reset parentBps to the previous node.
+  bctx.parentBps = state.prevParentBps
   if box.computed{"position"} != PositionStatic:
     bctx.lctx.popPositioned(box.state.size)
 
@@ -1444,7 +1496,7 @@ proc layoutInline(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
 proc layoutFlow(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
     canClear: bool) =
   if box.canFlushMargins(sizes):
-    bctx.flushMargins(box)
+    bctx.flushMargins(box.state.offset.y)
     bctx.positionFloats()
   if canClear and box.computed{"clear"} != ClearNone:
     box.state.offset.y.clearFloats(bctx, bctx.bfcOffset.y,
@@ -1509,10 +1561,6 @@ proc addInlineFloat(ictx: var InlineContext; state: var InlineState;
     marginOffset: sizes.margin.startOffset()
   ))
 
-const DisplayOuterInline = {
-  DisplayInlineBlock, DisplayInlineTableWrapper, DisplayInlineFlex
-}
-
 proc addInlineAbsolute(ictx: var InlineContext; state: var InlineState;
     box: BlockBox) =
   let lctx = ictx.lctx
@@ -1564,6 +1612,52 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
   ictx.lbstate.charwidth = 0
   ictx.whitespacenum = 0
 
+proc addOuterBlock(ictx: var InlineContext; state: var InlineState;
+    child: BlockBox) =
+  ictx.finishLine(state, wrap = true)
+  let lctx = ictx.lctx
+  let bctx = ictx.bctx
+  let sizes = lctx.resolveBlockSizes(ictx.space, child.computed)
+  let offset = offset(
+    x = sizes.margin.left + ictx.padding.left,
+    y = ictx.lbstate.offsety
+  )
+  if child.computed.establishesBFC():
+    lctx.layoutRootBlock(child, offset, sizes)
+  else:
+    bctx.marginTodo.append(sizes.margin.top)
+    child.state = BoxLayoutState(offset: offset)
+    bctx[].layout(child, sizes, canClear = true)
+    bctx.marginTodo.append(sizes.margin.bottom)
+    let textAlign = state.box.computed{"text-align"}
+    if textAlign == TextAlignChaCenter:
+      child.state.offset.x += max(ictx.space.w.u div 2 -
+        child.state.size.w div 2, 0)
+    elif textAlign == TextAlignChaRight:
+      child.state.offset.x += max(ictx.space.w.u - child.state.size.w -
+        sizes.margin.right, 0)
+    if child.computed{"position"} == PositionRelative:
+      lctx.positionRelative(ictx.space, child)
+  # Apply the block box's properties to the atom itself.
+  let atom = InlineAtom(
+    t: iatInlineBlock,
+    innerbox: child,
+    offset: offset(x = 0, y = 0),
+    size: size(
+      w = child.outerSize(dtHorizontal, sizes),
+      # delta y is difference between old and new offsets (margin-top),
+      # plus height.
+      h = child.state.offset.y - ictx.lbstate.offsety + child.state.size.h
+    )
+  )
+  state.box.state.atoms.add(atom)
+  ictx.lbstate.offsety += atom.size.h
+  ictx.state.size.h += atom.size.h
+  ictx.state.size.w = max(ictx.state.size.w, child.state.size.w)
+  ictx.state.intr.w = max(ictx.state.intr.w, child.state.intr.w)
+  ictx.state.intr.h += atom.size.h - child.state.size.h + child.state.intr.h
+  ictx.whitespacenum = 0
+
 proc addBox(ictx: var InlineContext; state: var InlineState; box: BlockBox) =
   if box.computed{"position"} in {PositionAbsolute, PositionFixed}:
     # This doesn't really have to be an inline block. I just want to
@@ -1574,10 +1668,12 @@ proc addBox(ictx: var InlineContext; state: var InlineState; box: BlockBox) =
     # This will trigger a re-layout for this inline root.
     if not ictx.secondPass:
       ictx.addInlineFloat(state, box)
-  else:
+  elif box.computed{"display"} in DisplayOuterInline:
     # This is an inline block.
-    assert box.computed{"display"} in DisplayOuterInline
     ictx.addInlineBlock(state, box)
+  else:
+    # This is an outer block.
+    ictx.addOuterBlock(state, box)
 
 proc addImage(ictx: var InlineContext; state: var InlineState;
     bmp: NetworkBitmap; padding: LUnit) =
@@ -1652,11 +1748,11 @@ proc addImage(ictx: var InlineContext; state: var InlineState;
     if computed{"height"}.u != clPerc or computed{"min-height"}.u != clPerc:
       ictx.lbstate.intrh = max(ictx.lbstate.intrh, atom.size.h)
 
-proc layoutInline(ictx: var InlineContext; box: InlineBox) =
+proc layoutInline(ictx: var InlineContext; ibox: InlineBox) =
   let lctx = ictx.lctx
-  let computed = box.computed
+  let computed = ibox.computed
   var padding = Span()
-  if stSplitStart in box.splitType:
+  if ibox.t == ibtParent:
     let w = computed{"margin-left"}.px(ictx.space.w)
     ictx.lbstate.size.w += w
     ictx.lbstate.widthAfterWhitespace += w
@@ -1664,64 +1760,63 @@ proc layoutInline(ictx: var InlineContext; box: InlineBox) =
       start: computed{"padding-left"}.px(ictx.space.w),
       send: computed{"padding-right"}.px(ictx.space.w)
     )
-  box.state = InlineBoxState()
+  ibox.state = InlineBoxState()
+  if ibox.canFlushMargins(padding):
+    var offsety = ictx.lbstate.offsety
+    ictx.bctx[].flushMargins(offsety)
+    # Don't forget to add it to intrinsic height...
+    ictx.state.size.h += offsety - ictx.lbstate.offsety
+    ictx.lbstate.offsety = offsety
+    ictx.bctx[].positionFloats()
   if padding.start != 0:
-    box.state.areas.add(Area(
+    ibox.state.areas.add(Area(
       offset: offset(x = ictx.lbstate.widthAfterWhitespace, y = 0),
       size: size(w = padding.start, h = ictx.cellHeight)
     ))
-    ictx.lbstate.paddingTodo.add((box, 0))
-  box.state.startOffset = offset(
+    ictx.lbstate.paddingTodo.add((ibox, 0))
+  ibox.state.startOffset = offset(
     x = ictx.lbstate.widthAfterWhitespace,
     y = ictx.lbstate.offsety
   )
   ictx.lbstate.size.w += padding.start
-  var state = InlineState(box: box)
-  if stSplitStart in box.splitType and computed{"position"} != PositionStatic:
+  var state = InlineState(box: ibox)
+  if ibox.t == ibtParent and computed{"position"} != PositionStatic:
     lctx.pushPositioned()
-  case box.t
+  case ibox.t
   of ibtNewline:
     ictx.finishLine(state, wrap = false, force = true,
-      box.computed{"clear"})
-  of ibtBox: ictx.addBox(state, box.box)
-  of ibtBitmap: ictx.addImage(state, box.bmp, padding.sum())
-  of ibtText: ictx.layoutText(state, box.text.data)
+      ibox.computed{"clear"})
+  of ibtBox: ictx.addBox(state, ibox.box)
+  of ibtBitmap: ictx.addImage(state, ibox.bmp, padding.sum())
+  of ibtText: ictx.layoutText(state, ibox.text.data)
   of ibtParent:
-    for child in box.children:
+    for child in ibox.children:
       ictx.layoutInline(child)
   if padding.send != 0:
-    box.state.areas.add(Area(
+    ibox.state.areas.add(Area(
       offset: offset(x = ictx.lbstate.size.w, y = 0),
       size: size(w = padding.send, h = ictx.cellHeight)
     ))
-    ictx.lbstate.paddingTodo.add((box, box.state.areas.high))
-  if stSplitEnd in box.splitType:
+    ictx.lbstate.paddingTodo.add((ibox, ibox.state.areas.high))
+  if ibox.t == ibtParent:
     ictx.lbstate.size.w += padding.send
     ictx.lbstate.size.w += computed{"margin-right"}.px(ictx.space.w)
-  if box.t != ibtParent:
+  if ibox.t != ibtParent:
     if not ictx.textFragmentSeen:
       ictx.textFragmentSeen = true
-    ictx.lastTextFragment = box
-  if stSplitEnd in box.splitType and computed{"position"} != PositionStatic:
+    ictx.lastTextFragment = ibox
+  if ibox.t == ibtParent and computed{"position"} != PositionStatic:
     # This is UB in CSS 2.1, I can't find a newer spec about it,
     # and Gecko can't even layout it consistently (???)
     #
-    # So I'm trying to follow Blink, though it's still not quite right.
-    # For one, space should really be the sum of all splits of this
-    # inline box, but I've wasted enough time on this already so I'm
-    # gonna stop here and say "good enough".
-    lctx.popPositioned(size(w = 0, h = ictx.state.size.h))
-
-proc positionRelative(lctx: LayoutContext; parent, box: BlockBox) =
-  let positioned = lctx.resolvePositioned(parent.state.size, box.computed)
-  if box.computed{"left"}.u != clAuto:
-    box.state.offset.x += positioned.left
-  elif box.computed{"right"}.u != clAuto:
-    box.state.offset.x -= positioned.right
-  if box.computed{"top"}.u != clAuto:
-    box.state.offset.y += positioned.top
-  elif box.computed{"bottom"}.u != clAuto:
-    box.state.offset.y -= positioned.bottom
+    # So I'm trying to follow Blink, though it's still not quite right,
+    # since it uses cellHeight instead of the actual line height for the
+    # last line.
+    # Well, it seems good enough.
+    lctx.popPositioned(size(
+      w = 0,
+      h = ictx.lbstate.offsety + ictx.cellHeight - ibox.state.startOffset.y
+    ))
 
 # Note: caption is not included here
 const RowGroupBox = {
@@ -2222,7 +2317,7 @@ proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
     bctx.layoutListItem(box, sizes)
   of DisplayTableWrapper, DisplayInlineTableWrapper:
     bctx.layoutTableWrapper(box, sizes)
-  of DisplayFlex, DisplayInlineFlex:
+  of DisplayInnerFlex:
     bctx.layoutFlex(box, sizes)
   else:
     assert false
@@ -2457,7 +2552,7 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   box.applySize(sizes, size, sizes.space)
   box.applyIntr(sizes, fctx.intr)
   for child in fctx.relativeChildren:
-    lctx.positionRelative(box, child)
+    lctx.positionRelative(sizes.space, child)
   if box.computed{"position"} != PositionStatic:
     lctx.popPositioned(box.state.size)
 
@@ -2550,7 +2645,7 @@ proc layoutBlockChildBFC(state: var BlockState; bctx: var BlockContext;
     offset.x += sizes.margin.left
     bctx.lctx.layoutRootBlock(child, offset, sizes)
     bctx.marginTodo.append(sizes.margin.top)
-    bctx.flushMargins(child)
+    bctx.flushMargins(child.state.offset.y)
     bctx.positionFloats()
     bctx.marginTodo.append(sizes.margin.bottom)
     if child.computed{"clear"} != ClearNone:
@@ -2611,15 +2706,6 @@ proc layoutBlockChildBFC(state: var BlockState; bctx: var BlockContext;
     h = outerHeight
   )
 
-# Note: this does not include display types that cannot appear as block
-# children.
-func establishesBFC(computed: CSSValues): bool =
-  return computed{"float"} != FloatNone or
-    computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayTableWrapper,
-      DisplayFlex} or
-    computed{"overflow-x"} notin {OverflowVisible, OverflowClip}
-    #TODO contain, grid, multicol, column-span
-
 # Layout and place all children in the block box.
 # Box placement must occur during this pass, since child box layout in the
 # same block formatting context depends on knowing where the box offset is
@@ -2655,16 +2741,14 @@ proc layoutBlockChildren(state: var BlockState; bctx: var BlockContext;
       state.layoutBlockChild(bctx, child, sizes)
     state.intr.w = max(state.intr.w, child.state.intr.w)
     if child.computed{"float"} == FloatNone:
-      # Assume we will stretch to the maximum width, and re-layout if
-      # this assumption turns out to be wrong.
-      if parent.computed{"text-align"} == TextAlignChaCenter:
+      if textAlign == TextAlignChaCenter:
         child.state.offset.x += max(state.space.w.u div 2 -
           child.state.size.w div 2, 0)
       elif textAlign == TextAlignChaRight:
         child.state.offset.x += max(state.space.w.u - child.state.size.w -
           sizes.margin.right, 0)
       if child.computed{"position"} == PositionRelative:
-        state.relativeChildren.add(child)
+        bctx.lctx.positionRelative(state.space, child)
       state.maxChildWidth = max(state.maxChildWidth, outerSize.w)
       state.offset.y += outerSize.h
       state.intr.h += outerSize.h - child.state.size.h + child.state.intr.h
@@ -2777,9 +2861,6 @@ proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   # Intrinsic minimum size includes the sum of our padding.  (However,
   # this padding must also be clamped to the same bounds.)
   box.applyIntr(sizes, state.intr + paddingSum)
-  # `position: relative' percentages can now be resolved.
-  for child in state.relativeChildren:
-    lctx.positionRelative(box, child)
   # Add padding; we cannot do this further up without influencing
   # relative positioning.
   box.state.size += paddingSum
@@ -2796,6 +2877,37 @@ proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
 
 # 1st pass: build tree
 
+type
+  BlockBuilderContext = object
+    styledNode: StyledNode
+    outer: BlockBox
+    lctx: LayoutContext
+    anonRow: BlockBox
+    anonTableWrapper: BlockBox
+    quoteLevel: int
+    listItemCounter: int
+    listItemReset: bool
+    parent: ptr BlockBuilderContext
+    inlineStack: seq[InlineBox]
+    # active group of inline boxes to be flushed
+    inlineGroup: seq[InlineBox]
+
+  ParentBoxType = enum
+    pbtBlock, pbtInline
+
+  ParentBox = object
+    case t: ParentBoxType
+    of pbtBlock:
+      box: BlockBox
+    of pbtInline:
+      ibox: InlineBox
+
+func parentBox(box: BlockBox): ParentBox =
+  return ParentBox(t: pbtBlock, box: box)
+
+func parentBox(ibox: InlineBox): ParentBox =
+  return ParentBox(t: pbtInline, ibox: ibox)
+
 proc newMarkerBox(computed: CSSValues; listItemCounter: int):
     InlineBox =
   let computed = computed.inheritProperties()
@@ -2809,45 +2921,10 @@ proc newMarkerBox(computed: CSSValues; listItemCounter: int):
     text: newCharacterData(s)
   )
 
-type BlockBuilderContext = object
-  styledNode: StyledNode
-  outer: BlockBox
-  lctx: LayoutContext
-  anonRow: BlockBox
-  anonTableWrapper: BlockBox
-  inlineAnonRow: BlockBox
-  inlineAnonTableWrapper: BlockBox
-  quoteLevel: int
-  listItemCounter: int
-  listItemReset: bool
-  parent: ptr BlockBuilderContext
-  inlineStack: seq[StyledNode]
-  inlineStackFragments: seq[InlineBox]
-  # if inline is not nil, then inline.children.len > 0
-  inline: InlineBox
-
-proc flushTable(ctx: var BlockBuilderContext)
-
-proc flushInlineGroup(ctx: var BlockBuilderContext) =
-  if ctx.inline != nil:
-    ctx.flushTable()
-    let computed = ctx.outer.computed.inheritProperties()
-    computed{"display"} = DisplayBlock
-    let box = BlockBox(computed: computed, inline: ctx.inline)
-    ctx.outer.children.add(box)
-    ctx.inline = nil
-
-# Don't build empty anonymous inline blocks between block boxes
-func canBuildAnonInline(ctx: BlockBuilderContext; computed: CSSValues;
-    str: string): bool =
-  return ctx.inline != nil and ctx.inline.children.len > 0 or
-    computed.whitespacepre or not str.onlyWhitespace()
-
 # Forward declarations
-proc buildBlock(ctx: var BlockBuilderContext)
-proc buildTable(ctx: var BlockBuilderContext)
-proc buildFlex(ctx: var BlockBuilderContext)
-proc buildInlineBoxes(ctx: var BlockBuilderContext; styledNode: StyledNode)
+proc build(ctx: var BlockBuilderContext)
+proc buildInline(ctx: var BlockBuilderContext; styledNode: StyledNode;
+  parent: ParentBox)
 proc buildTableRowGroup(parent: var BlockBuilderContext;
   styledNode: StyledNode): BlockBox
 proc buildTableRow(parent: var BlockBuilderContext; styledNode: StyledNode):
@@ -2858,8 +2935,31 @@ proc buildTableCaption(parent: var BlockBuilderContext; styledNode: StyledNode):
   BlockBox
 proc initBlockBuilderContext(styledNode: StyledNode; box: BlockBox;
   lctx: LayoutContext; parent: ptr BlockBuilderContext): BlockBuilderContext
-proc pushInline(ctx: var BlockBuilderContext; box: InlineBox)
+proc pushInline(ctx: var BlockBuilderContext; ibox: InlineBox)
 proc pushInlineBlock(ctx: var BlockBuilderContext; styledNode: StyledNode)
+proc flushTable(ctx: var BlockBuilderContext)
+
+proc flushInlineGroup(ctx: var BlockBuilderContext) =
+  if ctx.inlineGroup.len > 0:
+    ctx.flushTable()
+    let computed = ctx.outer.computed.inheritProperties()
+    computed{"display"} = DisplayBlock
+    let box = BlockBox(
+      computed: computed,
+      inline: InlineBox(
+        t: ibtParent,
+        children: move(ctx.inlineGroup),
+        computed: ctx.lctx.myRootProperties
+      )
+    )
+    ctx.inlineGroup = @[]
+    ctx.outer.children.add(box)
+
+# Don't build empty anonymous inline blocks between block boxes
+func canBuildAnonInline(ctx: BlockBuilderContext; computed: CSSValues;
+    str: string): bool =
+  return ctx.inlineGroup.len > 0 or computed.whitespacepre or
+    not str.onlyWhitespace()
 
 func toTableWrapper(display: CSSDisplay): CSSDisplay =
   if display == DisplayTable:
@@ -2870,8 +2970,7 @@ func toTableWrapper(display: CSSDisplay): CSSDisplay =
 proc createAnonTable(ctx: var BlockBuilderContext; computed: CSSValues):
     BlockBox =
   let inline = ctx.inlineStack.len > 0
-  if not inline and ctx.anonTableWrapper == nil or
-      inline and ctx.inlineAnonTableWrapper == nil:
+  if ctx.anonTableWrapper == nil:
     let inherited = computed.inheritProperties()
     let (outerComputed, innerComputed) = inherited.splitTable()
     outerComputed{"display"} = if inline:
@@ -2883,29 +2982,17 @@ proc createAnonTable(ctx: var BlockBuilderContext; computed: CSSValues):
       computed: outerComputed,
       children: @[innerTable]
     )
-    if inline:
-      ctx.inlineAnonTableWrapper = box
-    else:
-      ctx.anonTableWrapper = box
+    ctx.anonTableWrapper = box
     return box
-  if inline:
-    return ctx.inlineAnonTableWrapper
   return ctx.anonTableWrapper
 
 proc createAnonRow(ctx: var BlockBuilderContext): BlockBox =
-  let inline = ctx.inlineStack.len > 0
-  if not inline and ctx.anonRow == nil or
-      inline and ctx.inlineAnonRow == nil:
+  if ctx.anonRow == nil:
     let wrapperVals = ctx.outer.computed.inheritProperties()
     wrapperVals{"display"} = DisplayTableRow
     let box = BlockBox(computed: wrapperVals)
-    if inline:
-      ctx.inlineAnonRow = box
-    else:
-      ctx.anonRow = box
+    ctx.anonRow = box
     return box
-  if inline:
-    return ctx.inlineAnonRow
   return ctx.anonRow
 
 proc flushTableRow(ctx: var BlockBuilderContext) =
@@ -2924,25 +3011,22 @@ proc flushTable(ctx: var BlockBuilderContext) =
     ctx.anonTableWrapper = nil
 
 proc flushInlineTableRow(ctx: var BlockBuilderContext) =
-  if ctx.inlineAnonRow != nil:
+  if ctx.anonRow != nil:
     # There is no way an inline anonymous row could be a child of an inline
     # table, since inline tables still act like blocks inside.
     let anonTableWrapper = ctx.createAnonTable(ctx.outer.computed)
-    anonTableWrapper.children[0].children.add(ctx.inlineAnonRow)
-    ctx.inlineAnonRow = nil
+    anonTableWrapper.children[0].children.add(ctx.anonRow)
+    ctx.anonRow = nil
 
 proc flushInlineTable(ctx: var BlockBuilderContext) =
   ctx.flushInlineTableRow()
-  if ctx.inlineAnonTableWrapper != nil:
+  if ctx.anonTableWrapper != nil:
     ctx.pushInline(InlineBox(
       t: ibtBox,
-      computed: ctx.inlineAnonTableWrapper.computed.inheritProperties(),
-      box: ctx.inlineAnonTableWrapper
+      computed: ctx.anonTableWrapper.computed.inheritProperties(),
+      box: ctx.anonTableWrapper
     ))
-    ctx.inlineAnonTableWrapper = nil
-
-proc iflush(ctx: var BlockBuilderContext) =
-  ctx.inlineStackFragments.setLen(0)
+    ctx.anonTableWrapper = nil
 
 proc flushInherit(ctx: var BlockBuilderContext) =
   if ctx.parent != nil:
@@ -2955,72 +3039,39 @@ proc flush(ctx: var BlockBuilderContext) =
   ctx.flushTable()
   ctx.flushInherit()
 
-proc addInlineRoot(ctx: var BlockBuilderContext; box: InlineBox) =
-  if ctx.inline == nil:
-    ctx.inline = InlineBox(
-      t: ibtParent,
-      computed: ctx.lctx.myRootProperties,
-      children: @[box]
-    )
-  else:
-    ctx.inline.children.add(box)
-
-proc reconstructInlineParents(ctx: var BlockBuilderContext) =
-  if ctx.inlineStackFragments.len == 0:
-    var parent = InlineBox(
-      t: ibtParent,
-      computed: ctx.inlineStack[0].computed,
-      node: ctx.inlineStack[0].element
-    )
-    ctx.inlineStackFragments.add(parent)
-    ctx.addInlineRoot(parent)
-    for i in 1 ..< ctx.inlineStack.len:
-      let node = ctx.inlineStack[i]
-      let child = InlineBox(
-        t: ibtParent,
-        computed: node.computed,
-        node: node.element
-      )
-      parent.children.add(child)
-      ctx.inlineStackFragments.add(child)
-      parent = child
-
-proc buildSomeBlock(ctx: var BlockBuilderContext; styledNode: StyledNode):
+proc buildSomeBlock(pctx: var BlockBuilderContext; styledNode: StyledNode):
     BlockBox =
   let box = BlockBox(computed: styledNode.computed, node: styledNode.element)
-  var childCtx = initBlockBuilderContext(styledNode, box, ctx.lctx, addr ctx)
-  case styledNode.computed{"display"}
-  of DisplayBlock, DisplayFlowRoot, DisplayInlineBlock: childCtx.buildBlock()
-  of DisplayFlex, DisplayInlineFlex: childCtx.buildFlex()
-  of DisplayTable, DisplayInlineTable: childCtx.buildTable()
-  else: discard
+  var ctx = initBlockBuilderContext(styledNode, box, pctx.lctx, addr pctx)
+  ctx.build()
   return box
 
 # Note: these also pop
-proc pushBlock(ctx: var BlockBuilderContext; styledNode: StyledNode) =
-  if (styledNode.computed{"position"} == PositionAbsolute or
-        styledNode.computed{"float"} != FloatNone) and
-      (ctx.inline != nil or ctx.inlineStack.len > 0):
+proc pushBlock(ctx: var BlockBuilderContext; styledNode: StyledNode;
+    parent: ParentBox) =
+  case parent.t
+  of pbtInline:
+    assert ctx.inlineGroup.len > 0 or ctx.inlineStack.len > 0
     ctx.pushInlineBlock(styledNode)
-  else:
-    ctx.iflush()
+  of pbtBlock:
+    assert ctx.inlineGroup.len == 0 and ctx.inlineStack.len == 0 or
+      ctx.outer.computed{"display"} in DisplayInnerFlex
     ctx.flush()
     let box = ctx.buildSomeBlock(styledNode)
     ctx.outer.children.add(box)
 
-proc pushInline(ctx: var BlockBuilderContext; box: InlineBox) =
+proc pushInline(ctx: var BlockBuilderContext; ibox: InlineBox) =
   if ctx.inlineStack.len == 0:
-    ctx.addInlineRoot(box)
+    ctx.inlineGroup.add(ibox)
   else:
-    ctx.reconstructInlineParents()
-    ctx.inlineStackFragments[^1].children.add(box)
+    ctx.inlineStack[^1].children.add(ibox)
 
 proc pushInlineText(ctx: var BlockBuilderContext; computed: CSSValues;
-    parent: Element; text: CharacterData) =
+    parent: StyledNode; text: CharacterData) =
   ctx.pushInline(InlineBox(
     t: ibtText,
     computed: computed,
-    node: parent,
+    node: parent.element,
     text: text
   ))
 
@@ -3033,7 +3084,6 @@ proc pushInlineBlock(ctx: var BlockBuilderContext; styledNode: StyledNode) =
   ))
 
 proc pushListItem(ctx: var BlockBuilderContext; styledNode: StyledNode) =
-  ctx.iflush()
   ctx.flush()
   inc ctx.listItemCounter
   let marker = newMarkerBox(styledNode.computed, ctx.listItemCounter)
@@ -3046,7 +3096,7 @@ proc pushListItem(ctx: var BlockBuilderContext; styledNode: StyledNode) =
     addr ctx)
   case position
   of ListStylePositionOutside:
-    contentCtx.buildBlock()
+    contentCtx.build()
     content.computed = content.computed.copyProperties()
     content.computed{"display"} = DisplayBlock
     let markerComputed = marker.computed.copyProperties()
@@ -3062,13 +3112,12 @@ proc pushListItem(ctx: var BlockBuilderContext; styledNode: StyledNode) =
     ctx.outer.children.add(wrapper)
   of ListStylePositionInside:
     contentCtx.pushInline(marker)
-    contentCtx.buildBlock()
+    contentCtx.build()
     ctx.outer.children.add(content)
 
 proc pushTableRow(ctx: var BlockBuilderContext; styledNode: StyledNode) =
   let child = ctx.buildTableRow(styledNode)
   if ctx.inlineStack.len == 0:
-    ctx.iflush()
     ctx.flushInlineGroup()
     ctx.flushTableRow()
   else:
@@ -3083,7 +3132,6 @@ proc pushTableRow(ctx: var BlockBuilderContext; styledNode: StyledNode) =
 proc pushTableRowGroup(ctx: var BlockBuilderContext; styledNode: StyledNode) =
   let child = ctx.buildTableRowGroup(styledNode)
   if ctx.inlineStack.len == 0:
-    ctx.iflush()
     ctx.flushInlineGroup()
     ctx.flushTableRow()
   else:
@@ -3100,7 +3148,6 @@ proc pushTableCell(ctx: var BlockBuilderContext; styledNode: StyledNode) =
   let child = ctx.buildTableCell(styledNode)
   if ctx.inlineStack.len == 0 and
       ctx.outer.computed{"display"} == DisplayTableRow:
-    ctx.iflush()
     ctx.flushInlineGroup()
     ctx.outer.children.add(child)
   else:
@@ -3108,7 +3155,6 @@ proc pushTableCell(ctx: var BlockBuilderContext; styledNode: StyledNode) =
     anonRow.children.add(child)
 
 proc pushTableCaption(ctx: var BlockBuilderContext; styledNode: StyledNode) =
-  ctx.iflush()
   ctx.flushInlineGroup()
   ctx.flushTableRow()
   let child = ctx.buildTableCaption(styledNode)
@@ -3120,16 +3166,17 @@ proc pushTableCaption(ctx: var BlockBuilderContext; styledNode: StyledNode) =
     if anonTableWrapper.children.len == 1:
       anonTableWrapper.children.add(child)
 
-proc buildFromElem(ctx: var BlockBuilderContext; styledNode: StyledNode) =
+proc buildFromElem(ctx: var BlockBuilderContext; styledNode: StyledNode;
+    parent: ParentBox) =
   case styledNode.computed{"display"}
   of DisplayBlock, DisplayFlowRoot, DisplayFlex, DisplayTable:
-    ctx.pushBlock(styledNode)
+    ctx.pushBlock(styledNode, parent)
   of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex:
     ctx.pushInlineBlock(styledNode)
   of DisplayListItem:
     ctx.pushListItem(styledNode)
   of DisplayInline:
-    ctx.buildInlineBoxes(styledNode)
+    ctx.buildInline(styledNode, parent)
   of DisplayTableRow:
     ctx.pushTableRow(styledNode)
   of DisplayTableRowGroup, DisplayTableHeaderGroup, DisplayTableFooterGroup:
@@ -3143,11 +3190,11 @@ proc buildFromElem(ctx: var BlockBuilderContext; styledNode: StyledNode) =
   of DisplayNone: discard
   of DisplayTableWrapper, DisplayInlineTableWrapper: assert false
 
-proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
-    parent: Element; computed: CSSValues) =
+proc buildReplacement(ctx: var BlockBuilderContext; child, parent: StyledNode;
+    computed: CSSValues) =
   case child.content.t
   of ContentOpenQuote:
-    let quotes = parent.computed{"quotes"}
+    let quotes = computed{"quotes"}
     let s = if quotes == nil:
       quoteStart(ctx.quoteLevel)
     elif quotes.qs.len > 0:
@@ -3160,7 +3207,7 @@ proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
   of ContentCloseQuote:
     if ctx.quoteLevel > 0:
       dec ctx.quoteLevel
-    let quotes = parent.computed{"quotes"}
+    let quotes = computed{"quotes"}
     let s = if quotes == nil:
       quoteEnd(ctx.quoteLevel)
     elif quotes.qs.len > 0:
@@ -3180,8 +3227,8 @@ proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
     if child.content.bmp != nil:
       ctx.pushInline(InlineBox(
         t: ibtBitmap,
-        computed: parent.computed,
-        node: parent,
+        computed: computed,
+        node: parent.element,
         bmp: child.content.bmp
       ))
     else:
@@ -3193,35 +3240,33 @@ proc buildReplacement(ctx: var BlockBuilderContext; child: StyledNode;
       node: child.element
     ))
 
-proc buildInlineBoxes(ctx: var BlockBuilderContext; styledNode: StyledNode) =
-  let parent = InlineBox(
+proc buildInline(ctx: var BlockBuilderContext; styledNode: StyledNode;
+    parent: ParentBox) =
+  let ibox = InlineBox(
     t: ibtParent,
     computed: styledNode.computed,
-    splitType: {stSplitStart},
     node: styledNode.element
   )
-  if ctx.inlineStack.len == 0:
-    ctx.addInlineRoot(parent)
+  if parent.t == pbtInline and parent.ibox != nil:
+    parent.ibox.children.add(ibox)
   else:
-    ctx.reconstructInlineParents()
-    ctx.inlineStackFragments[^1].children.add(parent)
-  ctx.inlineStack.add(styledNode)
-  ctx.inlineStackFragments.add(parent)
+    ctx.inlineGroup.add(ibox)
+  #TODO this inline stack thing is a mess. I wish we could just pass
+  # through the parent as a parameter...
+  ctx.inlineStack.add(ibox)
   for child in styledNode.children:
     case child.t
     of stElement:
-      ctx.buildFromElem(child)
+      ctx.buildFromElem(child, parentBox(ibox))
     of stText:
       ctx.flushInlineTable()
-      ctx.pushInlineText(styledNode.computed, styledNode.element, child.text)
+      ctx.pushInlineText(styledNode.computed, styledNode, child.text)
     of stReplacement:
       ctx.flushInlineTable()
-      ctx.buildReplacement(child, styledNode.element, styledNode.computed)
-  ctx.reconstructInlineParents()
+      ctx.buildReplacement(child, styledNode, styledNode.computed)
   ctx.flushInlineTable()
-  let box = ctx.inlineStackFragments.pop()
-  box.splitType.incl(stSplitEnd)
-  ctx.inlineStack.setLen(ctx.inlineStack.high)
+  let x = ctx.inlineStack.pop()
+  assert x == ibox
 
 proc initBlockBuilderContext(styledNode: StyledNode; box: BlockBox;
     lctx: LayoutContext; parent: ptr BlockBuilderContext): BlockBuilderContext =
@@ -3245,38 +3290,18 @@ proc buildInnerBlock(ctx: var BlockBuilderContext) =
   for child in ctx.styledNode.children:
     case child.t
     of stElement:
-      ctx.buildFromElem(child)
+      let parent = if ctx.inlineGroup.len > 0 and
+          ctx.outer.computed{"display"} notin DisplayInnerFlex:
+        ParentBox(t: pbtInline, ibox: nil)
+      else:
+        parentBox(ctx.outer)
+      ctx.buildFromElem(child, parent)
     of stText:
       let text = child.text
       if ctx.canBuildAnonInline(ctx.outer.computed, text.data):
-        ctx.pushInlineText(inlineComputed, ctx.styledNode.element, text)
+        ctx.pushInlineText(inlineComputed, ctx.styledNode, text)
     of stReplacement:
-      ctx.buildReplacement(child, ctx.styledNode.element, inlineComputed)
-  ctx.iflush()
-
-proc buildBlock(ctx: var BlockBuilderContext) =
-  ctx.buildInnerBlock()
-  # Flush anonymous tables here, to avoid setting inline layout with tables.
-  ctx.flushTable()
-  ctx.flushInherit() # (flush here, because why not)
-  # Avoid unnecessary anonymous block boxes. This also helps set our layout to
-  # inline even if no inner anonymous block was built.
-  if ctx.outer.children.len == 0:
-    ctx.outer.inline = if ctx.inline != nil:
-      ctx.inline
-    else:
-      InlineBox(t: ibtParent, computed: ctx.lctx.myRootProperties)
-    ctx.inline = nil
-  ctx.flushInlineGroup()
-
-proc buildFlex(ctx: var BlockBuilderContext) =
-  ctx.buildInnerBlock()
-  # Flush anonymous tables here, to avoid setting inline layout with tables.
-  ctx.flushTable()
-  # (flush here, because why not)
-  ctx.flushInherit()
-  ctx.flushInlineGroup()
-  assert ctx.outer.inline == nil
+      ctx.buildReplacement(child, ctx.styledNode, inlineComputed)
 
 proc buildTableCell(parent: var BlockBuilderContext; styledNode: StyledNode):
     BlockBox =
@@ -3386,13 +3411,33 @@ proc buildTableChildWrappers(box: BlockBox; computed: CSSValues) =
   if caption != nil:
     box.children.add(caption)
 
-proc buildTable(ctx: var BlockBuilderContext) =
+proc build(ctx: var BlockBuilderContext) =
   ctx.buildInnerBlock()
-  ctx.flush()
-  let (outerComputed, innerComputed) = ctx.outer.computed.splitTable()
-  ctx.outer.computed = outerComputed
-  outerComputed{"display"} = outerComputed{"display"}.toTableWrapper()
-  ctx.outer.buildTableChildWrappers(innerComputed)
+  ctx.flushInherit()
+  let display = ctx.outer.computed{"display"}
+  if display in {DisplayTable, DisplayInlineTable}:
+    ctx.flushInlineGroup()
+    ctx.flushTable()
+    let (outerComputed, innerComputed) = ctx.outer.computed.splitTable()
+    ctx.outer.computed = outerComputed
+    outerComputed{"display"} = outerComputed{"display"}.toTableWrapper()
+    ctx.outer.buildTableChildWrappers(innerComputed)
+  else:
+    # Flush anonymous tables here, to avoid setting inline layout with
+    # tables.
+    ctx.flushTable()
+    if display notin DisplayInnerFlex:
+      # Avoid unnecessary anonymous block boxes. This also helps set our
+      # layout to inline even if no inner anonymous block was built.
+      if ctx.outer.children.len == 0:
+        let computed = ctx.outer.computed.inheritProperties()
+        ctx.outer.inline = InlineBox(
+          t: ibtParent,
+          children: move(ctx.inlineGroup),
+          computed: computed
+        )
+        ctx.inlineGroup = @[]
+    ctx.flushInlineGroup()
 
 proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
   let space = availableSpace(
@@ -3409,7 +3454,7 @@ proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
   )
   let box = BlockBox(computed: root.computed, node: root.element)
   var ctx = initBlockBuilderContext(root, box, lctx, nil)
-  ctx.buildBlock()
+  ctx.build()
   let sizes = lctx.resolveBlockSizes(space, box.computed)
   # the bottom margin is unused.
   lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes)
diff --git a/src/css/render.nim b/src/css/render.nim
index 0e34dce3..4d86c73c 100644
--- a/src/css/render.nim
+++ b/src/css/render.nim
@@ -377,11 +377,13 @@ proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState;
         bgcolor0.rgb.cellColor(), bgcolor0.a)
   let startOffset = offset + box.state.startOffset
   box.render.offset = startOffset
-  if position != PositionStatic and stSplitStart in box.splitType:
-    state.absolutePos.add(startOffset)
   if box.t == ibtParent:
+    if position != PositionStatic:
+      state.absolutePos.add(startOffset)
     for child in box.children:
       grid.renderInlineBox(state, child, offset, bgcolor0)
+    if position != PositionStatic:
+      discard state.absolutePos.pop()
   else:
     let format = box.computed.toFormat()
     for atom in box.state.atoms:
@@ -420,8 +422,6 @@ proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState;
               height: atom.size.h.toInt,
               bmp: atom.bmp
             ))
-  if position != PositionStatic and stSplitEnd in box.splitType:
-    discard state.absolutePos.pop()
 
 proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
     box: BlockBox; offset: Offset; pass2 = false) =