about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-12-17 01:23:52 +0100
committerbptato <nincsnevem662@gmail.com>2024-12-17 19:35:41 +0100
commit7d91d0dfe6309edeee7d7562d6054a8fb0392573 (patch)
treedede41714721d9ec6f4d7a61a66afb5b9fa7fe6b /src
parentc514f8c8b2e11557eed512d55321f9c8382ac2cc (diff)
downloadchawan-7d91d0dfe6309edeee7d7562d6054a8fb0392573.tar.gz
layout: propagate intrinsic minimum height
Necessary for flex.  Previously we just used the actual height, but that
didn't account for boxes that size themselves depending on the available
height (mainly just images for now).

This also irons out intrinsic min width calculation somewhat, squashing
several bugs.

I hope it works well.  It is a significant change in size calculation,
so maybe there are still new bugs lurking.
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)