diff options
author | bptato <nincsnevem662@gmail.com> | 2024-12-17 01:23:52 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-12-17 19:35:41 +0100 |
commit | 7d91d0dfe6309edeee7d7562d6054a8fb0392573 (patch) | |
tree | dede41714721d9ec6f4d7a61a66afb5b9fa7fe6b /src | |
parent | c514f8c8b2e11557eed512d55321f9c8382ac2cc (diff) | |
download | chawan-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.nim | 4 | ||||
-rw-r--r-- | src/css/layout.nim | 276 |
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) |