about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/css/box.nim4
-rw-r--r--src/css/layout.nim276
2 files changed, 154 insertions, 126 deletions
diff --git a/src/css/box.nim b/src/css/box.nim
index 3394d30d..856fd11c 100644
--- a/src/css/box.nim
+++ b/src/css/box.nim
@@ -34,8 +34,8 @@ type
     size*: Size
     # overflow relative to offset
     overflow*: Overflow
-    # minimum content width (usually shortest word)
-    xminwidth*: LayoutUnit
+    # intrinsic minimum size (e.g. longest word)
+    intr*: Size
     # baseline of the first line box of all descendants
     firstBaseline*: LayoutUnit
     # baseline of the last line box of all descendants
diff --git a/src/css/layout.nim b/src/css/layout.nim
index ef712b8a..85d136c0 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -87,6 +87,12 @@ func minHeight(sizes: ResolvedSizes): LayoutUnit =
 func maxHeight(sizes: ResolvedSizes): LayoutUnit =
   return sizes.bounds.a[dtVertical].send
 
+proc addMinWidthClamp(bounds: var Bounds; u: LayoutUnit) =
+  bounds.minClamp[dtHorizontal] = min(bounds.minClamp[dtHorizontal], u)
+
+proc addMinHeightClamp(bounds: var Bounds; u: LayoutUnit) =
+  bounds.minClamp[dtVertical] = min(bounds.minClamp[dtVertical], u)
+
 func sum(span: Span): LayoutUnit =
   return span.start + span.send
 
@@ -246,6 +252,7 @@ type
     availableWidth: LayoutUnit # actual place available after float exclusions
     offsety: LayoutUnit # offset of line in root fragment
     height: LayoutUnit # height used for painting; does not include padding
+    intrh: LayoutUnit # intrinsic minimum height
 
   InlineAtomState = object
     vertalign: CSSVerticalAlign
@@ -613,7 +620,7 @@ proc finishLine(ictx: var InlineContext; state: var InlineState; wrap: bool;
     let whitespace = state.fragment.computed{"white-space"}
     if whitespace == WhitespacePre:
       ictx.flushWhitespace(state)
-      ictx.state.xminwidth = max(ictx.state.xminwidth, ictx.lbstate.size.w)
+      ictx.state.intr.w = max(ictx.state.intr.w, ictx.lbstate.size.w)
     elif whitespace == WhitespacePreWrap:
       ictx.flushWhitespace(state, hang = true)
     else:
@@ -631,12 +638,16 @@ proc finishLine(ictx: var InlineContext; state: var InlineState; wrap: bool;
       ictx.state.firstBaseline = ictx.lbstate.baseline
       ictx.firstBaselineSet = true
     ictx.state.size.h += ictx.lbstate.size.h
+    ictx.state.intr.h += ictx.lbstate.intrh
     let lineWidth = if wrap:
       ictx.lbstate.availableWidth
     else:
       ictx.lbstate.size.w
     ictx.state.size.w = max(ictx.state.size.w, lineWidth)
-    ictx.lbstate = LineBoxState(offsety: y + ictx.lbstate.size.h)
+    ictx.lbstate = LineBoxState(
+      offsety: y + ictx.lbstate.size.h,
+      intrh: ictx.cellHeight
+    )
     ictx.initLine()
 
 func shouldWrap(ictx: InlineContext; w: LayoutUnit;
@@ -691,34 +702,14 @@ proc addAtom(ictx: var InlineContext; state: var InlineState;
   if atom.size.w > 0 and atom.size.h > 0 or atom.t == iatInlineBlock:
     if shift > 0:
       ictx.addSpacing(shift, state)
-    case atom.t
-    of iatWord:
-      let wordBreak = state.fragment.computed{"word-break"}
-      if ictx.wrappos != -1:
-        # set xminwidth to the first wrapping opportunity
-        ictx.state.xminwidth = max(ictx.state.xminwidth, ictx.wrappos)
-      elif state.prevrw >= 2 and wordBreak != WordBreakKeepAll or
-          wordBreak == WordBreakBreakAll:
-        # last char was double width; we can wrap anywhere.
-        # (I think this isn't quite right when double width + half width
-        # are mixed, but whatever...)
-        ictx.state.xminwidth = max(ictx.state.xminwidth, state.prevrw)
-      else:
-        ictx.state.xminwidth = max(ictx.state.xminwidth, atom.size.w)
-      if ictx.lbstate.atoms.len > 0 and state.fragment.state.atoms.len > 0:
-        let oatom = ictx.lbstate.atoms[^1]
-        if oatom.t == iatWord and oatom == state.fragment.state.atoms[^1]:
-          oatom.str &= atom.str
-          oatom.size.w += atom.size.w
-          ictx.lbstate.size.w += atom.size.w
-          return
-    of iatInlineBlock:
-      ictx.state.xminwidth = max(ictx.state.xminwidth,
-        atom.innerbox.state.xminwidth)
-      ictx.lbstate.charwidth = 0
-    of iatImage:
-      # We calculate xminwidth in addInlineImage instead.
-      ictx.lbstate.charwidth = 0
+    if atom.t == iatWord and ictx.lbstate.atoms.len > 0 and
+        state.fragment.state.atoms.len > 0:
+      let oatom = ictx.lbstate.atoms[^1]
+      if oatom.t == iatWord and oatom == state.fragment.state.atoms[^1]:
+        oatom.str &= atom.str
+        oatom.size.w += atom.size.w
+        ictx.lbstate.size.w += atom.size.w
+        return
     ictx.lbstate.putAtom(atom, iastate, state.fragment)
     atom.offset.x += ictx.lbstate.size.w
     ictx.lbstate.size.w += atom.size.w
@@ -729,13 +720,26 @@ proc addAtom(ictx: var InlineContext; state: var InlineState;
 
 proc addWord(ictx: var InlineContext; state: var InlineState): bool =
   result = false
-  if ictx.word.str != "":
-    ictx.word.str.mnormalize() #TODO this may break on EOL.
+  let atom = ictx.word
+  if atom.str != "":
+    atom.str.mnormalize() #TODO this may break on EOL.
     let iastate = InlineAtomState(
       vertalign: state.fragment.computed{"vertical-align"},
-      baseline: ictx.word.size.h
+      baseline: atom.size.h
     )
-    result = ictx.addAtom(state, iastate, ictx.word)
+    let wordBreak = state.fragment.computed{"word-break"}
+    if ictx.wrappos != -1:
+      # set intr.w to the first wrapping opportunity
+      ictx.state.intr.w = max(ictx.state.intr.w, ictx.wrappos)
+    elif state.prevrw >= 2 and wordBreak != WordBreakKeepAll or
+        wordBreak == WordBreakBreakAll:
+      # last char was double width; we can wrap anywhere.
+      # (I think this isn't quite right when double width + half width
+      # are mixed, but whatever...)
+      ictx.state.intr.w = max(ictx.state.intr.w, state.prevrw)
+    else:
+      ictx.state.intr.w = max(ictx.state.intr.w, atom.size.w)
+    result = ictx.addAtom(state, iastate, atom)
     ictx.newWord()
 
 proc addWordEOL(ictx: var InlineContext; state: var InlineState): bool =
@@ -836,7 +840,7 @@ func initInlineContext(bctx: var BlockContext; space: AvailableSpace;
     space: space,
     computed: computed,
     padding: padding,
-    lbstate: LineBoxState(offsety: padding.top)
+    lbstate: LineBoxState(offsety: padding.top, intrh: bctx.lctx.attrs.ppl)
   )
 
 proc layoutTextLoop(ictx: var InlineContext; state: var InlineState;
@@ -901,33 +905,26 @@ func spx(l: CSSLength; p: SizeConstraint; computed: CSSValues;
     return max(u - padding, 0)
   return max(u, 0)
 
-proc resolveContentWidth(sizes: var ResolvedSizes; widthpx: LayoutUnit;
-    parentWidth: SizeConstraint; computed: CSSValues;
-    isauto = false) =
-  if not sizes.space.w.isDefinite() or parentWidth.t != scStretch:
-    # width is indefinite, so no conflicts can be resolved here.
-    return
-  let total = widthpx + sizes.margin[dtHorizontal].sum() +
-    sizes.padding[dtHorizontal].sum()
-  let underflow = parentWidth.u - total
-  if isauto:
-    if underflow >= 0:
-      sizes.space.w = SizeConstraint(t: sizes.space.w.t, u: underflow)
-    else:
-      sizes.margin[dtHorizontal].send += underflow
-  elif underflow > 0:
-    if computed{"margin-left"}.u != clAuto and
-        computed{"margin-right"}.u != clAuto:
-      sizes.margin[dtHorizontal].send += underflow
-    elif computed{"margin-left"}.u != clAuto and
-        computed{"margin-right"}.u == clAuto:
-      sizes.margin[dtHorizontal].send = underflow
-    elif computed{"margin-left"}.u == clAuto and
-        computed{"margin-right"}.u != clAuto:
-      sizes.margin[dtHorizontal].start = underflow
-    else:
-      sizes.margin[dtHorizontal].start = underflow div 2
-      sizes.margin[dtHorizontal].send = underflow div 2
+proc resolveUnderflow(sizes: var ResolvedSizes; parentWidth: SizeConstraint;
+    computed: CSSValues) =
+  # width must be definite, so that conflicts can be resolved
+  if sizes.space.w.isDefinite() and parentWidth.t == scStretch:
+    let total = sizes.space.w.u + sizes.margin[dtHorizontal].sum() +
+      sizes.padding[dtHorizontal].sum()
+    let underflow = parentWidth.u - total
+    if underflow > 0:
+      if computed{"margin-left"}.u != clAuto and
+          computed{"margin-right"}.u != clAuto:
+        sizes.margin[dtHorizontal].send += underflow
+      elif computed{"margin-left"}.u != clAuto and
+          computed{"margin-right"}.u == clAuto:
+        sizes.margin[dtHorizontal].send = underflow
+      elif computed{"margin-left"}.u == clAuto and
+          computed{"margin-right"}.u != clAuto:
+        sizes.margin[dtHorizontal].start = underflow
+      else:
+        sizes.margin[dtHorizontal].start = underflow div 2
+        sizes.margin[dtHorizontal].send = underflow div 2
 
 proc resolveMargins(lctx: LayoutContext; availableWidth: SizeConstraint;
     computed: CSSValues): RelativeRect =
@@ -1121,12 +1118,17 @@ proc resolveBlockWidth(sizes: var ResolvedSizes; parentWidth: SizeConstraint;
     inlinePadding: LayoutUnit; computed: CSSValues;
     lctx: LayoutContext) =
   let width = computed{"width"}
-  var widthpx: LayoutUnit = 0
   if width.canpx(parentWidth):
-    widthpx = width.spx(parentWidth, computed, inlinePadding)
-    sizes.space.w = stretch(widthpx)
-    sizes.bounds.minClamp[dtHorizontal] = widthpx
-  sizes.resolveContentWidth(widthpx, parentWidth, computed, width.u == clAuto)
+    sizes.space.w = stretch(width.spx(parentWidth, computed, inlinePadding))
+    sizes.bounds.addMinWidthClamp(sizes.space.w.u)
+    sizes.resolveUnderflow(parentWidth, computed)
+  elif sizes.space.w.isDefinite():
+    let underflow = parentWidth.u - sizes.margin[dtHorizontal].sum() -
+      sizes.padding[dtHorizontal].sum()
+    if underflow >= 0:
+      sizes.space.w = SizeConstraint(t: sizes.space.w.t, u: underflow)
+    else:
+      sizes.margin[dtHorizontal].send += underflow
   if sizes.space.w.isDefinite() and sizes.maxWidth < sizes.space.w.u or
       sizes.maxWidth < LayoutUnit.high and sizes.space.w.t == scMaxContent:
     if sizes.space.w.t == scStretch:
@@ -1135,7 +1137,8 @@ proc resolveBlockWidth(sizes: var ResolvedSizes; parentWidth: SizeConstraint;
     else: # scFitContent
       # available width could be higher than max-width (but not necessarily)
       sizes.space.w = fitContent(sizes.maxWidth)
-    sizes.resolveContentWidth(sizes.maxWidth, parentWidth, computed)
+    sizes.bounds.addMinWidthClamp(sizes.space.w.u)
+    sizes.resolveUnderflow(parentWidth, computed)
   if sizes.space.w.isDefinite() and sizes.minWidth > sizes.space.w.u or
       sizes.minWidth > 0 and sizes.space.w.t == scMinContent:
     # two cases:
@@ -1144,16 +1147,19 @@ proc resolveBlockWidth(sizes: var ResolvedSizes; parentWidth: SizeConstraint;
     # * available width is fit under min-width. in this case, stretch to
     #   min-width as well (as we must satisfy min-width >= width).
     sizes.space.w = stretch(sizes.minWidth)
-    sizes.resolveContentWidth(sizes.minWidth, parentWidth, computed)
+    sizes.bounds.addMinWidthClamp(sizes.space.w.u)
+    sizes.resolveUnderflow(parentWidth, computed)
+  sizes.bounds.addMinWidthClamp(sizes.maxWidth)
+  if sizes.minWidth > 0:
+    sizes.bounds.addMinWidthClamp(sizes.minWidth)
 
 proc resolveBlockHeight(sizes: var ResolvedSizes; parentHeight: SizeConstraint;
     blockPadding: LayoutUnit; computed: CSSValues;
     lctx: LayoutContext) =
   let height = computed{"height"}
   if height.canpx(parentHeight):
-    let heightpx = height.spx(parentHeight, computed, blockPadding)
-    sizes.space.h = stretch(heightpx)
-    sizes.bounds.minClamp[dtVertical] = heightpx
+    sizes.space.h = stretch(height.spx(parentHeight, computed, blockPadding))
+    sizes.bounds.addMinHeightClamp(sizes.space.h.u)
   if sizes.space.h.isDefinite() and sizes.maxHeight < sizes.space.h.u or
       sizes.maxHeight < LayoutUnit.high and sizes.space.h.t == scMaxContent:
     # same reasoning as for width.
@@ -1161,10 +1167,15 @@ proc resolveBlockHeight(sizes: var ResolvedSizes; parentHeight: SizeConstraint;
       sizes.space.h = stretch(sizes.maxHeight)
     else: # scFitContent
       sizes.space.h = fitContent(sizes.maxHeight)
+    sizes.bounds.addMinHeightClamp(sizes.space.h.u)
   if sizes.space.h.isDefinite() and sizes.minHeight > sizes.space.h.u or
       sizes.minHeight > 0 and sizes.space.h.t == scMinContent:
     # same reasoning as for width.
     sizes.space.h = stretch(sizes.minHeight)
+    sizes.bounds.addMinHeightClamp(sizes.space.h.u)
+  sizes.bounds.addMinHeightClamp(sizes.maxHeight)
+  if sizes.minHeight > 0:
+    sizes.bounds.addMinHeightClamp(sizes.minHeight)
 
 proc resolveBlockSizes(lctx: LayoutContext; space: AvailableSpace;
     computed: CSSValues): ResolvedSizes =
@@ -1216,14 +1227,13 @@ proc applySize(box: BlockBox; sizes: ResolvedSizes;
   # Then, clamp it to minWidth and maxWidth (if applicable).
   box.state.size[dim] = minClamp(box.state.size[dim], sizes.bounds.a[dim])
 
-proc applyMinWidth(box: BlockBox; sizes: ResolvedSizes) =
-  box.state.xminwidth = min(box.state.xminwidth,
-    sizes.bounds.minClamp[dtHorizontal])
+proc clampIntr(box: BlockBox; sizes: ResolvedSizes) =
+  box.state.intr.w = min(box.state.intr.w, sizes.bounds.minClamp[dtHorizontal])
+  box.state.intr.h = min(box.state.intr.h, sizes.bounds.minClamp[dtVertical])
 
 proc applyWidth(box: BlockBox; sizes: ResolvedSizes;
     maxChildWidth: LayoutUnit; space: AvailableSpace) =
   box.applySize(sizes, maxChildWidth, space, dtHorizontal)
-  box.applyMinWidth(sizes)
 
 proc applyWidth(box: BlockBox; sizes: ResolvedSizes;
     maxChildWidth: LayoutUnit) =
@@ -1235,7 +1245,9 @@ proc applyHeight(box: BlockBox; sizes: ResolvedSizes;
 
 proc applyPadding(box: BlockBox; padding: RelativeRect) =
   box.state.size.w += padding[dtHorizontal].sum()
-  box.state.size.h += padding[dtVertical].sum()
+  let verticalSum = padding[dtVertical].sum()
+  box.state.size.h += verticalSum
+  box.state.intr.h += verticalSum
 
 proc applyBaseline(box: BlockBox) =
   if box.children.len > 0:
@@ -1303,11 +1315,11 @@ proc popPositioned(lctx: LayoutContext; overflow: var Overflow; size: Size) =
     var positioned: RelativeRect
     var sizes = lctx.resolveAbsoluteSizes(size, positioned, child.computed)
     var marginBottom = lctx.layoutRootBlock(child, it.offset, sizes)
-    if sizes.space.w.t == scFitContent and child.state.xminwidth > size.w:
+    if sizes.space.w.t == scFitContent and child.state.intr.w > size.w:
       # In case the width is shrink-to-fit, and the available width is
       # less than the minimum width, then the minimum width overrides
       # the available width, and we must re-layout.
-      sizes.space.w = stretch(child.state.xminwidth)
+      sizes.space.w = stretch(child.state.intr.w)
       marginBottom = lctx.layoutRootBlock(child, it.offset, sizes)
     if child.computed{"left"}.u != clAuto:
       child.state.offset.x = positioned.left + sizes.margin.left
@@ -1340,7 +1352,7 @@ type
     maxChildWidth: LayoutUnit
     totalFloatWidth: LayoutUnit # used for re-layouts
     space: AvailableSpace
-    xminwidth: LayoutUnit
+    intr: Size
     prevParentBps: BlockPositionState
     needsReLayout: bool
     # State kept for when a re-layout is necessary:
@@ -1374,7 +1386,7 @@ func findNextFloatOffset(bctx: BlockContext; offset: Offset; size: Size;
     let w = right - left
     if w >= size.w or miny == high(LayoutUnit):
       # Enough space, or no other exclusions found at this y offset.
-      outw = w
+      outw = min(w, space.w.u) # do not overflow the container.
       if float == FloatLeft:
         return offset(x = left, y = y)
       else: # FloatRight
@@ -1466,10 +1478,11 @@ proc layoutInline(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
         t: iatInlineBlock,
         innerbox: it.box
       ))
-  box.state.xminwidth = max(box.state.xminwidth, ictx.state.xminwidth)
   box.state.size.w = ictx.state.size.w + sizes.padding[dtHorizontal].sum()
   box.applyWidth(sizes, ictx.state.size.w)
   box.applyHeight(sizes, ictx.state.size.h)
+  box.state.intr = ictx.state.intr
+  box.clampIntr(sizes)
   box.applyPadding(sizes.padding)
   box.state.baseline = ictx.state.baseline
   box.state.firstBaseline = ictx.state.firstBaseline
@@ -1573,7 +1586,7 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
   box.state = BoxLayoutState()
   let marginBottom = lctx.layoutRootBlock(box, offset(x = 0, y = 0), sizes)
   # Apply the block box's properties to the atom itself.
-  let iblock = InlineAtom(
+  let atom = InlineAtom(
     t: iatInlineBlock,
     innerbox: box,
     offset: offset(x = 0, y = 0),
@@ -1585,11 +1598,13 @@ proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
     marginTop: sizes.margin.top,
     marginBottom: marginBottom
   )
-  discard ictx.addAtom(state, iastate, iblock)
+  discard ictx.addAtom(state, iastate, atom)
+  ictx.state.intr.w = max(ictx.state.intr.w, atom.innerbox.state.intr.w)
+  ictx.lbstate.intrh = max(ictx.lbstate.intrh, atom.size.h)
+  ictx.lbstate.charwidth = 0
   ictx.whitespacenum = 0
 
-proc addBox(ictx: var InlineContext; state: var InlineState;
-    box: BlockBox) =
+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
     # handle its positioning here.
@@ -1604,7 +1619,7 @@ proc addBox(ictx: var InlineContext; state: var InlineState;
     assert box.computed{"display"} in DisplayOuterInline
     ictx.addInlineBlock(state, box)
 
-proc addInlineImage(ictx: var InlineContext; state: var InlineState;
+proc addImage(ictx: var InlineContext; state: var InlineState;
     bmp: NetworkBitmap; padding: LayoutUnit) =
   let atom = InlineAtom(
     t: iatImage,
@@ -1651,15 +1666,20 @@ proc addInlineImage(ictx: var InlineContext; state: var InlineState;
     baseline: atom.size.h
   )
   discard ictx.addAtom(state, iastate, atom)
+  ictx.lbstate.charwidth = 0
   if atom.size.h > 0:
-    # Setting the atom size as xminwidth might result in a circular dependency
+    # Setting the atom size as intr.w might result in a circular dependency
     # between table cell sizing and image sizing when we don't have a definite
     # parent size yet. e.g. <img width=100% ...> with an indefinite containing
-    # size (i.e. the first table cell pass) would resolve to an xminwidth of
+    # size (i.e. the first table cell pass) would resolve to an intr.w of
     # image.width, stretching out the table to an uncomfortably large size.
-    if ictx.space.w.isDefinite() or computed{"width"}.u != clPerc and
+    if ictx.space.w.t == scStretch or computed{"width"}.u != clPerc and
         computed{"min-width"}.u != clPerc:
-      ictx.state.xminwidth = max(ictx.state.xminwidth, atom.size.w)
+      ictx.state.intr.w = max(ictx.state.intr.w, atom.size.w)
+    # Similarly for height, except in this case it happens in flex sizing.
+    if computed{"height"}.canpx(ictx.space.h) or
+        computed{"min-height"}.canpx(ictx.space.h):
+      ictx.lbstate.intrh = max(ictx.lbstate.intrh, atom.size.h)
 
 proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
   let lctx = ictx.lctx
@@ -1694,7 +1714,7 @@ proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
     ictx.finishLine(state, wrap = false, force = true,
       fragment.computed{"clear"})
   of iftBox: ictx.addBox(state, fragment.box)
-  of iftBitmap: ictx.addInlineImage(state, fragment.bmp, padding.sum())
+  of iftBitmap: ictx.addImage(state, fragment.bmp, padding.sum())
   of iftText: ictx.layoutText(state, fragment.text.textData)
   of iftParent:
     for child in fragment.children:
@@ -1808,7 +1828,7 @@ proc layoutTableCell(lctx: LayoutContext; box: BlockBox;
     box.state.size.h = max(box.state.size.h, space.h.u -
       sizes.padding[dtVertical].sum())
   # A table cell's minimum width overrides its width.
-  box.state.size.w = max(box.state.size.w, box.state.xminwidth)
+  box.state.size.w = max(box.state.size.w, box.state.intr.w)
 
 # Sort growing cells, and filter out cells that have grown to their intended
 # rowspan.
@@ -1885,7 +1905,7 @@ proc preLayoutTableRow(pctx: var TableContext; row, parent: BlockBox;
       pctx.cols.setLen(n + colspan)
     if ctx.reflow.len < n + colspan:
       ctx.reflow.setLen(n + colspan)
-    let minw = box.state.xminwidth div colspan
+    let minw = box.state.intr.w div colspan
     let w = box.state.size.w div colspan
     for i in n ..< n + colspan:
       # Add spacing.
@@ -2171,9 +2191,10 @@ proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
     table.state.offset.y += outerHeight
   of CaptionSideBottom, CaptionSideBlockEnd:
     box.state.offset.y += table.state.size.h
-  parent.state.size.h += outerHeight
   parent.state.size.w = max(parent.state.size.w, outerWidth)
-  parent.state.xminwidth = max(parent.state.xminwidth, box.state.xminwidth)
+  parent.state.intr.w = max(parent.state.intr.w, box.state.intr.w)
+  parent.state.size.h += outerHeight
+  parent.state.intr.h += outerHeight - box.state.size.h + box.state.intr.h
 
 # Table layout. We try to emulate w3m's behavior here:
 # 1. Calculate minimum and preferred width of each column
@@ -2187,7 +2208,7 @@ proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
 proc layoutTable(tctx: var TableContext; table: BlockBox;
     sizes: ResolvedSizes) =
   if tctx.space.w.t == scStretch:
-    table.state.xminwidth = tctx.space.w.u
+    table.state.intr.w = tctx.space.w.u
   if table.computed{"border-collapse"} != BorderCollapseCollapse:
     let spc = table.computed{"border-spacing"}
     if spc != nil:
@@ -2200,6 +2221,9 @@ proc layoutTable(tctx: var TableContext; table: BlockBox;
     table.state.size.w += col.width
   tctx.reflowTableCells()
   tctx.layoutTableRows(table, sizes) # second pass
+  # Table height is minimum by default, and non-negotiable when
+  # specified, ergo it always equals the intrinisc minimum height.
+  table.state.intr.h = table.state.size.h
 
 # As per standard, we must put the caption outside the actual table, inside a
 # block-level wrapper box.
@@ -2212,7 +2236,7 @@ proc layoutTableWrapper(bctx: BlockContext; box: BlockBox;
   box.state.size = table.state.size
   box.state.baseline = table.state.size.h
   box.state.firstBaseline = table.state.size.h
-  box.state.xminwidth = table.state.xminwidth
+  box.state.intr = table.state.intr
   if box.children.len > 1:
     # do it here, so that caption's box can stretch to our width
     let caption = box.children[1]
@@ -2233,7 +2257,7 @@ proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   else:
     assert false
 
-proc layoutFlexChild(lctx: LayoutContext; box: BlockBox; sizes: ResolvedSizes) =
+proc layoutFlexItem(lctx: LayoutContext; box: BlockBox; sizes: ResolvedSizes) =
   var bctx = BlockContext(lctx: lctx)
   # note: we do not append margins here, since those belong to the flex item,
   # not its inner BFC.
@@ -2243,6 +2267,7 @@ proc layoutFlexChild(lctx: LayoutContext; box: BlockBox; sizes: ResolvedSizes) =
   # If the highest float edge is higher than the box itself, set that as
   # the box height.
   box.state.size.h = max(box.state.size.h, bctx.maxFloatHeight)
+  box.state.intr.h = max(box.state.intr.h, bctx.maxFloatHeight)
 
 type
   FlexWeightType = enum
@@ -2272,14 +2297,6 @@ type
 
 const FlexRow = {FlexDirectionRow, FlexDirectionRowReverse}
 
-# This is practically the min-content size. For height, we just take the
-# output height of the previous pass; for width, we take the shortest word's
-# width (xminwidth).
-func minFlexItemSize(state: BoxLayoutState; dim: DimensionType): LayoutUnit =
-  case dim
-  of dtHorizontal: return state.xminwidth
-  of dtVertical: return state.size.h
-
 proc updateMaxSizes(mctx: var FlexMainContext; child: BlockBox;
     sizes: ResolvedSizes) =
   for dim in DimensionType:
@@ -2318,8 +2335,7 @@ proc redistributeMainSize(mctx: var FlexMainContext; diff: LayoutUnit;
         uw *= it.child.state.size[dim].toFloat64()
       var u = it.child.state.size[dim] + uw.toLayoutUnit()
       # check for min/max violation
-      var minu = it.sizes.bounds.a[dim].start
-      minu = max(it.child.state.minFlexItemSize(dim), minu)
+      let minu = max(it.child.state.intr[dim], it.sizes.bounds.a[dim].start)
       if minu > u:
         # min violation
         if wt == fwtShrink: # freeze
@@ -2338,7 +2354,7 @@ proc redistributeMainSize(mctx: var FlexMainContext; diff: LayoutUnit;
       totalWeight += it.weights[wt]
       #TODO we should call this only on freeze, and then put another loop to
       # the end for non-frozen items
-      lctx.layoutFlexChild(it.child, it.sizes)
+      lctx.layoutFlexItem(it.child, it.sizes)
       mctx.updateMaxSizes(it.child, it.sizes)
 
 proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext;
@@ -2358,7 +2374,9 @@ proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext;
     if sizes.bounds.a[dim].start > mctx.totalSize[dim]:
       let diff = sizes.bounds.a[dim].start - mctx.totalSize[dim]
       mctx.redistributeMainSize(diff, fwtGrow, dim, lctx)
-  let h = mctx.maxSize[odim] + mctx.maxMargin[odim].sum()
+  let maxMarginSum = mctx.maxMargin[odim].sum()
+  let h = mctx.maxSize[odim] + maxMarginSum
+  var intr = size(w = 0, h = 0)
   var offset = fctx.offset
   for it in mctx.pending.mitems:
     if it.child.state.size[odim] < h and not it.sizes.space[odim].isDefinite:
@@ -2366,18 +2384,23 @@ proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext;
       # instead. (if the box's available height is definite, then this will
       # change nothing, so we skip it as an optimization.)
       it.sizes.space[odim] = stretch(h - it.sizes.margin[odim].sum())
-      lctx.layoutFlexChild(it.child, it.sizes)
+      lctx.layoutFlexItem(it.child, it.sizes)
     it.child.state.offset[dim] += offset[dim]
     # margins are added here, since they belong to the flex item.
     it.child.state.offset[odim] += offset[odim] + it.sizes.margin[odim].start
     offset[dim] += it.child.state.size[dim]
     offset[dim] += it.sizes.margin[dim].send
+    intr[dim] += it.child.state.intr[dim]
+    intr[dim] += it.sizes.margin[dim].send
+    intr[odim] = max(it.child.state.intr[odim], intr[odim])
     if it.child.computed{"position"} == PositionRelative:
       fctx.relativeChildren.add(it.child)
     else:
       fctx.box.applyOverflowDimensions(it.child)
   fctx.totalMaxSize[dim] = max(fctx.totalMaxSize[dim], offset[dim])
   fctx.mains.add(mctx)
+  fctx.box.state.intr[dim] = max(fctx.box.state.intr[dim], intr[dim])
+  fctx.box.state.intr[odim] += intr[odim] + maxMarginSum
   mctx = FlexMainContext()
   fctx.offset[odim] += h
 
@@ -2404,11 +2427,11 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   for child in box.children:
     var childSizes = lctx.resolveFlexItemSizes(sizes.space, dim, child.computed)
     let flexBasis = child.computed{"flex-basis"}
-    lctx.layoutFlexChild(child, childSizes)
+    lctx.layoutFlexItem(child, childSizes)
     if flexBasis.u != clAuto and sizes.space[dim].isDefinite:
       # we can't skip this pass; it is needed to calculate the minimum
       # height.
-      let minu = child.state.minFlexItemSize(dim)
+      let minu = child.state.intr[dim]
       childSizes.space[dim] = stretch(flexBasis.spx(sizes.space[dim],
         child.computed, childSizes.padding[dim].sum()))
       if minu > childSizes.space[dim].u:
@@ -2417,7 +2440,7 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
         # because the initial flex basis was e.g. 0. Try to resize it to
         # something more usable.
         childSizes.space[dim] = stretch(minu)
-      lctx.layoutFlexChild(child, childSizes)
+      lctx.layoutFlexItem(child, childSizes)
     if child.computed{"position"} in {PositionAbsolute, PositionFixed}:
       # Absolutely positioned flex children do not participate in flex layout.
       lctx.queueAbsolute(child, offset(x = 0, y = 0))
@@ -2445,7 +2468,7 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   box.applyBaseline()
   box.applySize(sizes, fctx.totalMaxSize[dim], sizes.space, dim)
   box.applySize(sizes, fctx.offset[odim], sizes.space, odim)
-  box.applyMinWidth(sizes)
+  box.clampIntr(sizes)
   for child in fctx.relativeChildren:
     lctx.positionRelative(box, child)
     box.applyOverflowDimensions(child)
@@ -2468,6 +2491,7 @@ proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset;
   # If the highest float edge is higher than the box itself, set that as
   # the box height.
   box.state.size.h = max(box.state.size.h, bctx.maxFloatHeight - marginBottom)
+  box.state.intr.h = max(box.state.intr.h, bctx.maxFloatHeight - marginBottom)
   return marginBottom
 
 proc initBlockPositionStates(state: var BlockState; bctx: var BlockContext;
@@ -2552,7 +2576,7 @@ proc layoutBlockChildBFC(state: var BlockState; bctx: var BlockContext;
       # ...thanks for nothing. So here's what we do:
       #
       # * run a normal pass
-      # * place the longest word (i.e. xminwidth) somewhere
+      # * place the longest word (i.e. intr.w) somewhere
       # * run another pass with the placement we got
       #
       # Some browsers prefer to try again until they find enough
@@ -2566,13 +2590,16 @@ proc layoutBlockChildBFC(state: var BlockState; bctx: var BlockContext;
         x = pbfcOffset.x + child.state.offset.x,
         y = max(pbfcOffset.y + child.state.offset.y, bctx.clearOffset)
       )
-      let minSize = size(w = child.state.xminwidth, h = bctx.lctx.attrs.ppl)
+      let minSize = size(w = child.state.intr.w, h = bctx.lctx.attrs.ppl)
       var outw: LayoutUnit
       let offset = bctx.findNextBlockOffset(bfcOffset, minSize, state.space,
         outw)
-      space = availableSpace(w = stretch(outw), h = state.space.h)
-      sizes = lctx.resolveBlockSizes(space, child.computed)
-      marginBottom = lctx.layoutRootBlock(child, offset - pbfcOffset, sizes)
+      let roffset = offset - pbfcOffset
+      # skip relayout if we can
+      if outw != state.space.w.u or roffset != child.state.offset:
+        space = availableSpace(w = stretch(outw), h = state.space.h)
+        sizes = lctx.resolveBlockSizes(space, child.computed)
+        marginBottom = lctx.layoutRootBlock(child, roffset, sizes)
     # delta y is difference between old and new offsets (margin-top
     # plus any movement caused by floats), sum of margin todo in bctx
     # (margin-bottom) + height.
@@ -2634,7 +2661,7 @@ proc layoutBlockChildren(state: var BlockState; bctx: var BlockContext;
       state.layoutBlockChildBFC(bctx, child, sizes, space)
     else:
       state.layoutBlockChild(bctx, child, sizes)
-    state.xminwidth = max(state.xminwidth, child.state.xminwidth)
+    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.
@@ -2648,6 +2675,7 @@ proc layoutBlockChildren(state: var BlockState; bctx: var BlockContext;
         state.relativeChildren.add(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
       parent.applyOverflowDimensions(child)
     elif state.space.w.t == scFitContent:
       # Float position depends on the available width, but in this case
@@ -2754,12 +2782,12 @@ proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
     state.initReLayout(bctx, box, sizes)
     state.layoutBlockChildren(bctx, box)
   box.applyBaseline()
-  # Set xminwidth now, so that it can be clamped by minWidthClamp.
-  box.state.xminwidth = state.xminwidth
   # Apply width, and height. For height, temporarily remove padding we have
   # applied before so that percentage resolution works correctly.
   box.applyWidth(sizes, state.maxChildWidth, state.space)
   box.applyHeight(sizes, state.offset.y - sizes.padding.top)
+  box.state.intr = state.intr
+  box.clampIntr(sizes)
   # `position: relative' percentages can now be resolved.
   for child in state.relativeChildren:
     lctx.positionRelative(box, child)