worldhello'|cha -T text/html
# is rendered as:
# hello
# world
var thead: seq[BlockBox] = @[]
var tbody: seq[BlockBox] = @[]
var tfoot: seq[BlockBox] = @[]
for child in table.children:
let child = BlockBox(child)
let display = child.computed{"display"}
if display == DisplayTableRow:
tbody.add(child)
else:
for it in child.children:
case display
of DisplayTableHeaderGroup: thead.add(BlockBox(it))
of DisplayTableRowGroup: tbody.add(BlockBox(it))
of DisplayTableFooterGroup: tfoot.add(BlockBox(it))
else: assert false, $child.computed{"display"}
tctx.preLayoutTableRows(thead, table)
tctx.preLayoutTableRows(tbody, table)
tctx.preLayoutTableRows(tfoot, table)
func calcSpecifiedRatio(tctx: TableContext; W: LUnit): LUnit =
var totalSpecified: LUnit = 0
var hasUnspecified = false
for col in tctx.cols:
if col.wspecified:
totalSpecified += col.width
else:
hasUnspecified = true
totalSpecified += col.minwidth
# Only grow specified columns if no unspecified column exists to take the
# rest of the space.
if totalSpecified == 0 or W > totalSpecified and hasUnspecified:
return 1
return W div totalSpecified
proc calcUnspecifiedColIndices(tctx: var TableContext; W: var LUnit;
weight: var float32): seq[int] =
let specifiedRatio = tctx.calcSpecifiedRatio(W)
# Spacing for each column:
var avail = newSeqOfCap[int](tctx.cols.len)
for i, col in tctx.cols.mpairs:
if not col.wspecified:
avail.add(i)
let w = if col.width < W:
toFloat32(col.width)
else:
toFloat32(W) * (ln(toFloat32(col.width) / toFloat32(W)) + 1)
col.weight = w
weight += w
else:
if specifiedRatio != 1:
col.width *= specifiedRatio
col.reflow = true
W -= col.width
return avail
func needsRedistribution(tctx: TableContext; computed: CSSValues):
bool =
case tctx.space.w.t
of scMinContent, scMaxContent:
return false
of scStretch:
return tctx.space.w.u != tctx.maxwidth
of scFitContent:
return tctx.space.w.u > tctx.maxwidth and computed{"width"}.u != clAuto or
tctx.space.w.u < tctx.maxwidth
proc redistributeWidth(tctx: var TableContext) =
# Remove inline spacing from distributable width.
var W = max(tctx.space.w.u - tctx.cols.len * tctx.inlineSpacing * 2, 0)
var weight = 0f32
var avail = tctx.calcUnspecifiedColIndices(W, weight)
var redo = true
while redo and avail.len > 0 and weight != 0:
if weight == 0: break # zero weight; nothing to distribute
if W < 0:
W = 0
redo = false
# divide delta width by sum of ln(width) for all elem in avail
let unit = toFloat32(W) / weight
weight = 0
for i in countdown(avail.high, 0):
let j = avail[i]
let x = (unit * tctx.cols[j].weight).toLUnit()
let mw = tctx.cols[j].minwidth
tctx.cols[j].width = x
if mw > x:
W -= mw
tctx.cols[j].width = mw
avail.del(i)
redo = true
else:
weight += tctx.cols[j].weight
tctx.cols[j].reflow = true
proc reflowTableCells(tctx: var TableContext) =
for i in countdown(tctx.rows.high, 0):
var row = addr tctx.rows[i]
var n = tctx.cols.len - 1
for j in countdown(row.cells.high, 0):
let m = n - row.cells[j].colspan
while n > m:
if tctx.cols[n].reflow:
row.cells[j].reflow = true
if n < row.reflow.len and row.reflow[n]:
tctx.cols[n].reflow = true
dec n
proc layoutTableRows(tctx: TableContext; table: BlockBox;
sizes: ResolvedSizes) =
var y: LUnit = 0
for roww in tctx.rows:
if roww.box.computed{"visibility"} == VisibilityCollapse:
continue
y += tctx.blockSpacing
let row = roww.box
tctx.layoutTableRow(roww, table, row)
row.state.offset.y += y
row.state.offset.x += sizes.padding.left
row.state.size.w += sizes.padding[dtHorizontal].sum()
y += tctx.blockSpacing
y += row.state.size.h
table.state.size.w = max(row.state.size.w, table.state.size.w)
table.state.intr.w = max(row.state.intr.w, table.state.intr.w)
# Note: we can't use applySizeConstraint here; in CSS, "height" on tables just
# sets the minimum height.
case tctx.space.h.t
of scStretch:
table.state.size.h = max(tctx.space.h.u, y)
of scMinContent, scMaxContent, scFitContent:
# I don't think these are ever used here; not that they make much sense for
# min-height...
table.state.size.h = y
proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
let lctx = tctx.lctx
let space = availableSpace(w = stretch(parent.state.size.w), h = maxContent())
let sizes = lctx.resolveBlockSizes(space, box.computed)
lctx.layoutRootBlock(box, offset(x = sizes.margin.left, y = 0), sizes)
box.state.offset.x += sizes.margin.left
box.state.offset.y += sizes.margin.top
let outerHeight = box.outerSize(dtVertical, sizes)
let outerWidth = box.outerSize(dtHorizontal, sizes)
let table = BlockBox(parent.firstChild)
case box.computed{"caption-side"}
of CaptionSideTop, CaptionSideBlockStart:
table.state.offset.y += outerHeight
of CaptionSideBottom, CaptionSideBlockEnd:
box.state.offset.y += table.state.size.h
parent.state.size.w = max(parent.state.size.w, outerWidth)
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
proc layoutInnerTable(tctx: var TableContext; table, parent: BlockBox;
sizes: ResolvedSizes) =
if table.computed{"border-collapse"} != BorderCollapseCollapse:
let spc = table.computed{"border-spacing"}
if spc != nil:
tctx.inlineSpacing = spc.a.px(0)
tctx.blockSpacing = spc.b.px(0)
tctx.preLayoutTableRows(table) # first pass
# Percentage sizes have been resolved; switch the table's space to
# fit-content if its width is auto.
# (Note that we call canpx on space, which might have been changed by
# specified width. This isn't a problem however, because canpx will
# still return true after that.)
if tctx.space.w.t == scStretch:
if not parent.computed{"width"}.canpx(tctx.space.w):
tctx.space.w = fitContent(tctx.space.w.u)
else:
table.state.intr.w = tctx.space.w.u
if tctx.needsRedistribution(table.computed):
tctx.redistributeWidth()
for col in tctx.cols:
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.
proc layoutTable(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes) =
let table = BlockBox(box.firstChild)
table.resetState()
var tctx = TableContext(lctx: bctx.lctx, space: sizes.space)
tctx.layoutInnerTable(table, box, sizes)
box.state.size = table.state.size
box.state.baseline = table.state.size.h
box.state.firstBaseline = table.state.size.h
box.state.intr = table.state.intr
if table.next != nil:
# do it here, so that caption's box can stretch to our width
let caption = BlockBox(table.next)
#TODO also count caption width in table width
tctx.layoutCaption(box, caption)
proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
canClear: bool) =
case box.computed{"display"}
of DisplayInnerBlock:
bctx.layoutFlow(box, sizes, canClear)
of DisplayInnerTable:
bctx.layoutTable(box, sizes)
of DisplayInnerFlex:
bctx.layoutFlex(box, sizes)
of DisplayInnerGrid:
bctx.layoutGrid(box, sizes)
else:
assert false
type
FlexWeightType = enum
fwtGrow, fwtShrink
FlexPendingItem = object
child: BlockBox
weights: array[FlexWeightType, float32]
sizes: ResolvedSizes
FlexContext = object
mains: seq[FlexMainContext]
offset: Offset
lctx: LayoutContext
totalMaxSize: Size
intr: Size # intrinsic minimum size
relativeChildren: seq[BlockBox]
redistSpace: SizeConstraint
firstBaseline: LUnit
baseline: LUnit
canWrap: bool
reverse: bool
dim: DimensionType # main dimension
firstBaselineSet: bool
FlexMainContext = object
totalSize: Size
maxSize: Size
shrinkSize: LUnit
maxMargin: RelativeRect
totalWeight: array[FlexWeightType, float32]
pending: seq[FlexPendingItem]
proc layoutFlexItem(lctx: LayoutContext; box: BlockBox; sizes: ResolvedSizes) =
lctx.layoutRootBlock(box, offset(x = 0, y = 0), sizes)
const FlexRow = {FlexDirectionRow, FlexDirectionRowReverse}
proc updateMaxSizes(mctx: var FlexMainContext; child: BlockBox;
sizes: ResolvedSizes) =
for dim in DimensionType:
mctx.maxSize[dim] = max(mctx.maxSize[dim], child.state.size[dim])
mctx.maxMargin[dim].start = max(mctx.maxMargin[dim].start,
sizes.margin[dim].start)
mctx.maxMargin[dim].send = max(mctx.maxMargin[dim].send,
sizes.margin[dim].send)
proc redistributeMainSize(mctx: var FlexMainContext; diff: LUnit;
wt: FlexWeightType; dim: DimensionType; lctx: LayoutContext) =
var diff = diff
var totalWeight = mctx.totalWeight[wt]
let odim = dim.opposite
var relayout: seq[int] = @[]
while (wt == fwtGrow and diff > 0 or wt == fwtShrink and diff < 0) and
totalWeight > 0:
# redo maxSize calculation; we only need height here
mctx.maxSize[odim] = 0
var udiv = totalWeight
if wt == fwtShrink:
udiv *= mctx.shrinkSize.toFloat32() / totalWeight
let unit = if udiv != 0:
diff.toFloat32() / udiv
else:
0
# reset total weight & available diff for the next iteration (if there is
# one)
totalWeight = 0
diff = 0
relayout.setLen(0)
for i, it in mctx.pending.mpairs:
if it.weights[wt] == 0:
mctx.updateMaxSizes(it.child, it.sizes)
continue
var uw = unit * it.weights[wt]
if wt == fwtShrink:
uw *= it.child.state.size[dim].toFloat32()
var u = it.child.state.size[dim] + uw.toLUnit()
# check for min/max violation
let minu = max(it.child.state.intr[dim], it.sizes.bounds.a[dim].start)
if minu > u:
# min violation
if wt == fwtShrink: # freeze
diff += u - minu
it.weights[wt] = 0
mctx.shrinkSize -= it.child.state.size[dim]
u = minu
it.sizes.bounds.mi[dim].start = u
let maxu = max(minu, it.sizes.bounds.a[dim].send)
if maxu < u:
# max violation
if wt == fwtGrow: # freeze
diff += u - maxu
it.weights[wt] = 0
u = maxu
it.sizes.bounds.mi[dim].send = u
u -= it.sizes.padding[dim].sum()
it.sizes.space[dim] = stretch(u)
# override minimum intrinsic size clamping too
totalWeight += it.weights[wt]
if it.weights[wt] == 0: # frozen, relayout immediately
lctx.layoutFlexItem(it.child, it.sizes)
mctx.updateMaxSizes(it.child, it.sizes)
else: # delay relayout
relayout.add(i)
for i in relayout:
let child = mctx.pending[i].child
lctx.layoutFlexItem(child, mctx.pending[i].sizes)
mctx.updateMaxSizes(child, mctx.pending[i].sizes)
proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext;
sizes: ResolvedSizes) =
let dim = fctx.dim
let odim = dim.opposite
let lctx = fctx.lctx
if fctx.redistSpace.isDefinite:
let diff = fctx.redistSpace.u - mctx.totalSize[dim]
let wt = if diff > 0: fwtGrow else: fwtShrink
# Do not grow shrink-to-fit sizes.
if wt == fwtShrink or fctx.redistSpace.t == scStretch:
mctx.redistributeMainSize(diff, wt, dim, lctx)
elif sizes.bounds.a[dim].start > 0:
# Override with min-width/min-height, but *only* if we are smaller
# than the desired size. (Otherwise, we would incorrectly limit
# max-content size when only a min-width is requested.)
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 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:
# if the max height is greater than our height, then take max height
# instead. (if the box's available height is definite, then this will
# change nothing, so we skip it as an optimization.)
it.sizes.space[odim] = stretch(h - it.sizes.margin[odim].sum())
if odim == dtVertical:
# Exclude the bottom margin; space only applies to the actual
# height.
it.sizes.space[odim].u -= it.child.state.marginBottom
lctx.layoutFlexItem(it.child, it.sizes)
offset[dim] += it.sizes.margin[dim].start
it.child.state.offset[dim] += offset[dim]
# resolve auto cross margins for shrink-to-fit items
if sizes.space[odim].t == scStretch:
let start = it.child.computed.getLength(MarginStartMap[odim])
let send = it.child.computed.getLength(MarginEndMap[odim])
# We can get by without adding offset, because flex items are
# always layouted at (0, 0).
let underflow = sizes.space[odim].u - it.child.state.size[odim] -
it.sizes.margin[odim].sum()
if underflow > 0 and start.u == clAuto:
# we don't really care about the end margin, because that is
# already taken into account by AvailableSpace
if send.u != clAuto:
it.sizes.margin[odim].start = underflow
else:
it.sizes.margin[odim].start = underflow div 2
# 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
let intru = it.child.state.intr[dim] + it.sizes.margin[dim].sum()
if fctx.canWrap:
intr[dim] = max(intr[dim], intru)
else:
intr[dim] += intru
intr[odim] = max(it.child.state.intr[odim], intr[odim])
if it.child.computed{"position"} == PositionRelative:
fctx.relativeChildren.add(it.child)
let baseline = it.child.state.offset.y + it.child.state.baseline
if not fctx.firstBaselineSet:
fctx.firstBaselineSet = true
fctx.firstBaseline = baseline
fctx.baseline = baseline
if fctx.reverse:
for it in mctx.pending:
let child = it.child
child.state.offset[dim] = offset[dim] - child.state.offset[dim] -
child.state.size[dim]
fctx.totalMaxSize[dim] = max(fctx.totalMaxSize[dim], offset[dim])
fctx.mains.add(mctx)
fctx.intr[dim] = max(fctx.intr[dim], intr[dim])
fctx.intr[odim] += intr[odim] + maxMarginSum
mctx = FlexMainContext()
fctx.offset[odim] += h
proc layoutFlexIter(fctx: var FlexContext; mctx: var FlexMainContext;
child: BlockBox; sizes: ResolvedSizes) =
let lctx = fctx.lctx
let dim = fctx.dim
var childSizes = lctx.resolveFlexItemSizes(sizes.space, dim, child.computed)
let flexBasis = child.computed{"flex-basis"}
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.intr[dim]
childSizes.space[dim] = stretch(flexBasis.spx(sizes.space[dim],
child.computed, childSizes.padding[dim].sum()))
if minu > childSizes.space[dim].u:
# First pass gave us a box that is thinner than the minimum
# acceptable width for whatever reason; this may have happened
# because the initial flex basis was e.g. 0. Try to resize it to
# something more usable.
childSizes.space[dim] = stretch(minu)
lctx.layoutFlexItem(child, childSizes)
if child.computed{"position"} in PositionAbsoluteFixed:
# Absolutely positioned flex children do not participate in flex layout.
lctx.queueAbsolute(child, offset(x = 0, y = 0))
else:
if fctx.canWrap and (sizes.space[dim].t == scMinContent or
sizes.space[dim].isDefinite and
mctx.totalSize[dim] + child.state.size[dim] > sizes.space[dim].u):
fctx.flushMain(mctx, sizes)
let outerSize = child.outerSize(dim, childSizes)
mctx.updateMaxSizes(child, childSizes)
let grow = child.computed{"flex-grow"}
let shrink = child.computed{"flex-shrink"}
mctx.totalWeight[fwtGrow] += grow
mctx.totalWeight[fwtShrink] += shrink
mctx.totalSize[dim] += outerSize
if shrink != 0:
mctx.shrinkSize += outerSize
mctx.pending.add(FlexPendingItem(
child: child,
weights: [grow, shrink],
sizes: childSizes
))
proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
let lctx = bctx.lctx
if box.computed{"position"} != PositionStatic:
lctx.pushPositioned(box)
let flexDir = box.computed{"flex-direction"}
let dim = if flexDir in FlexRow: dtHorizontal else: dtVertical
let odim = dim.opposite()
var fctx = FlexContext(
lctx: lctx,
offset: sizes.padding.topLeft,
redistSpace: sizes.space[dim],
canWrap: box.computed{"flex-wrap"} != FlexWrapNowrap,
reverse: box.computed{"flex-direction"} in FlexReverse,
dim: dim
)
if fctx.redistSpace.t == scFitContent and sizes.bounds.a[dim].start > 0:
fctx.redistSpace = stretch(sizes.bounds.a[dim].start)
if fctx.redistSpace.isDefinite:
fctx.redistSpace.u = fctx.redistSpace.u.minClamp(sizes.bounds.a[dim])
var mctx = FlexMainContext()
for child in box.children:
let child = BlockBox(child)
fctx.layoutFlexIter(mctx, child, sizes)
if mctx.pending.len > 0:
fctx.flushMain(mctx, sizes)
var size = fctx.totalMaxSize
size[odim] = fctx.offset[odim]
box.applySize(sizes, size, sizes.space)
box.applyIntr(sizes, fctx.intr)
box.state.firstBaseline = fctx.firstBaseline
box.state.baseline = fctx.baseline
for child in fctx.relativeChildren:
lctx.positionRelative(sizes.space, child)
if box.computed{"position"} != PositionStatic:
lctx.popPositioned(box, box.state.size)
proc layoutGrid(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
#TODO implement grid
bctx.layoutFlow(box, sizes, canClear = false)
# Inner layout for boxes that establish a new block formatting context.
proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset;
sizes: ResolvedSizes) =
if box.sizes == sizes:
box.state.offset = offset
return
box.sizes = sizes
var bctx = BlockContext(lctx: lctx)
box.resetState()
box.state.offset = offset
bctx.layout(box, sizes, canClear = false)
assert bctx.unpositionedFloats.len == 0
let marginBottom = bctx.marginTodo.sum()
# 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 + marginBottom, bctx.maxFloatHeight)
box.state.intr.h = max(box.state.intr.h + marginBottom, bctx.maxFloatHeight)
box.state.marginBottom = marginBottom
proc layout*(box: BlockBox; attrsp: ptr WindowAttributes): StackItem =
let space = availableSpace(
w = stretch(attrsp[].widthPx),
h = stretch(attrsp[].heightPx)
)
let stack = StackItem(box: box)
let lctx = LayoutContext(
attrsp: attrsp,
cellSize: size(w = attrsp.ppc, h = attrsp.ppl),
positioned: @[
# add another to catch fixed boxes pushed to the stack
PositionedItem(stack: stack),
PositionedItem(stack: stack),
PositionedItem(stack: stack)
],
luctx: LUContext()
)
let sizes = lctx.resolveBlockSizes(space, box.computed)
# the bottom margin is unused.
lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes)
var size = size(w = attrsp[].widthPx, h = attrsp[].heightPx)
# Last absolute layer.
lctx.popPositioned(nil, size)
# Fixed containing block.
# The idea is to move fixed boxes to the real edges of the page,
# so that they do not overlap with other boxes *and* we don't have
# to move them on scroll. It's still not compatible with what desktop
# browsers do, but the alternative would completely break search (and
# slow down the renderer to a crawl.)
size.w = max(size.w, box.state.size.w)
size.h = max(size.h, box.state.size.h)
lctx.popPositioned(nil, size)
# Now we can sort the root box's stacking context list.
lctx.popPositioned(box, size)
return stack
| |