import std/algorithm
import std/math
import std/unicode
import css/cssvalues
import css/stylednode
import img/bitmap
import layout/box
import layout/layoutunit
import types/winattrs
import utils/luwrap
import utils/strwidth
import utils/twtstr
import utils/widthconv
type
LayoutContext = ref object
attrsp: ptr WindowAttributes
positioned: seq[AvailableSpace]
myRootProperties: CSSComputedValues
# min-content: box width is longest word's width
# max-content: box width is content width without wrapping
# stretch: box width is n px wide
# fit-content: also known as shrink-to-fit, box width is
# min(max-content, stretch(availableWidth))
# in other words, as wide as needed, but wrap if wider than allowed
# (note: I write width here, but it can apply for any constraint)
SizeConstraintType = enum
scStretch, scFitContent, scMinContent, scMaxContent
SizeConstraint = object
t: SizeConstraintType
u: LayoutUnit
AvailableSpace = array[DimensionType, SizeConstraint]
ResolvedSizes = object
margin: RelativeRect
padding: RelativeRect
positioned: RelativeRect
space: AvailableSpace
minMaxSizes: array[DimensionType, Span]
const DefaultSpan = Span(start: 0, send: LayoutUnit.high)
func minWidth(sizes: ResolvedSizes): LayoutUnit =
return sizes.minMaxSizes[dtHorizontal].start
func maxWidth(sizes: ResolvedSizes): LayoutUnit =
return sizes.minMaxSizes[dtHorizontal].send
func minHeight(sizes: ResolvedSizes): LayoutUnit =
return sizes.minMaxSizes[dtVertical].start
func maxHeight(sizes: ResolvedSizes): LayoutUnit =
return sizes.minMaxSizes[dtVertical].send
func sum(span: Span): LayoutUnit =
return span.start + span.send
func opposite(dim: DimensionType): DimensionType =
case dim
of dtHorizontal: return dtVertical
of dtVertical: return dtHorizontal
func availableSpace(w, h: SizeConstraint): AvailableSpace =
return [dtHorizontal: w, dtVertical: h]
func w(space: AvailableSpace): SizeConstraint {.inline.} =
return space[dtHorizontal]
func w(space: var AvailableSpace): var SizeConstraint {.inline.} =
return space[dtHorizontal]
func `w=`(space: var AvailableSpace; w: SizeConstraint) {.inline.} =
space[dtHorizontal] = w
func h(space: var AvailableSpace): var SizeConstraint {.inline.} =
return space[dtVertical]
func h(space: AvailableSpace): SizeConstraint {.inline.} =
return space[dtVertical]
func `h=`(space: var AvailableSpace; h: SizeConstraint) {.inline.} =
space[dtVertical] = h
template attrs(state: LayoutContext): WindowAttributes =
state.attrsp[]
func maxContent(): SizeConstraint =
return SizeConstraint(t: scMaxContent)
func stretch(u: LayoutUnit): SizeConstraint =
return SizeConstraint(t: scStretch, u: u)
func fitContent(u: LayoutUnit): SizeConstraint =
return SizeConstraint(t: scFitContent, u: u)
func fitContent(sc: SizeConstraint): SizeConstraint =
case sc.t
of scMinContent, scMaxContent:
return sc
of scStretch, scFitContent:
return SizeConstraint(t: scFitContent, u: sc.u)
func isDefinite(sc: SizeConstraint): bool =
return sc.t in {scStretch, scFitContent}
# 2nd pass: layout
func px(l: CSSLength; lctx: LayoutContext; p: LayoutUnit = 0):
LayoutUnit {.inline.} =
return px(l, lctx.attrs, p)
func canpx(l: CSSLength; sc: SizeConstraint): bool =
return not l.auto and (l.unit != cuPerc or sc.isDefinite())
# Note: for margins only
# For percentages, use 0 for indefinite, and containing box's size for
# definite.
func px(l: CSSLength; lctx: LayoutContext; p: SizeConstraint): LayoutUnit =
if l.unit == cuPerc:
case p.t
of scMinContent, scMaxContent:
return 0
of scStretch, scFitContent:
return l.px(lctx, p.u)
return px(l, lctx.attrs, 0)
func stretchOrMaxContent(l: CSSLength; lctx: LayoutContext; sc: SizeConstraint):
SizeConstraint =
if l.canpx(sc):
return stretch(l.px(lctx, sc))
return maxContent()
func applySizeConstraint(u: LayoutUnit; availableSize: SizeConstraint):
LayoutUnit =
case availableSize.t
of scStretch:
return availableSize.u
of scMinContent, scMaxContent:
# must be calculated elsewhere...
return u
of scFitContent:
return min(u, availableSize.u)
func outerSize(box: BlockBox; dim: DimensionType): LayoutUnit =
return box.state.margin[dim].sum() + box.state.size[dim]
type
BlockContext = object
lctx: LayoutContext
marginTodo: Strut
# We use a linked list to set the correct BFC offset and relative offset
# for every block with an unresolved y offset on margin resolution.
# marginTarget is a pointer to the last un-resolved ancestor.
# ancestorsHead is a pointer to the last element of the ancestor list
# (which may in fact be a pointer to the BPS of a previous sibling's
# child).
# parentBps is a pointer to the currently layouted parent block's BPS.
marginTarget: BlockPositionState
ancestorsHead: BlockPositionState
parentBps: BlockPositionState
exclusions: seq[Exclusion]
unpositionedFloats: seq[UnpositionedFloat]
maxFloatHeight: LayoutUnit
clearOffset: LayoutUnit
UnpositionedFloat = object
parentBps: BlockPositionState
space: AvailableSpace
box: BlockBox
# to propagate float overflow
parentBox: BlockBox
BlockPositionState = ref object
next: BlockPositionState
box: BlockBox
offset: Offset # offset relative to the block formatting context
resolved: bool # has the position been resolved yet?
Exclusion = object
offset: Offset
size: Size
t: CSSFloat
Strut = object
pos: LayoutUnit
neg: LayoutUnit
type
LineBoxState = object
atomstates: seq[InlineAtomState]
baseline: LayoutUnit
lineHeight: LayoutUnit
paddingTop: LayoutUnit
paddingBottom: LayoutUnit
line: LineBox
availableWidth: LayoutUnit
hasExclusion: bool
charwidth: int
# Set at the end of layoutText. It helps determine the beginning of the
# next inline fragment.
widthAfterWhitespace: LayoutUnit
# minimum height to fit all inline atoms
minHeight: LayoutUnit
LineBox = ref object
atoms: seq[InlineAtom]
size: Size
offsety: LayoutUnit # offset of line in root fragment
height: LayoutUnit # height used for painting; does not include padding
InlineAtomState = object
vertalign: CSSVerticalAlign
baseline: LayoutUnit
marginTop: LayoutUnit
marginBottom: LayoutUnit
InlineContext = object
root: RootInlineFragment
computed: CSSComputedValues
bctx: ptr BlockContext
bfcOffset: Offset
currentLine: LineBoxState
hasshy: bool
lctx: LayoutContext
lines: seq[LineBox]
space: AvailableSpace
whitespacenum: int
whitespaceIsLF: bool
whitespaceFragment: InlineFragment
word: InlineAtom
wordstate: InlineAtomState
wrappos: int # position of last wrapping opportunity, or -1
textFragmentSeen: bool
lastTextFragment: InlineFragment
InlineState = object
fragment: InlineFragment
firstLine: bool
startOffsetTop: Offset
# computed line-height of fragment
lineHeight: LayoutUnit
# we do not want to collapse newlines over tag boundaries, so these are
# in state
lastrw: int # last rune width of the previous word
firstrw: int # first rune width of the current word
prevrw: int # last processed rune's width
func whitespacepre(computed: CSSComputedValues): bool =
computed{"white-space"} in {WhitespacePre, WhitespacePreLine,
WhitespacePreWrap}
func nowrap(computed: CSSComputedValues): bool =
computed{"white-space"} in {WhitespaceNowrap, WhitespacePre}
func cellWidth(lctx: LayoutContext): int =
lctx.attrs.ppc
func cellWidth(ictx: InlineContext): int =
ictx.lctx.cellWidth
func cellHeight(lctx: LayoutContext): int =
lctx.attrs.ppl
func cellHeight(ictx: InlineContext): int =
ictx.lctx.attrs.ppl
template atoms(state: LineBoxState): untyped =
state.line.atoms
template size(state: LineBoxState): untyped =
state.line.size
template offsety(state: LineBoxState): untyped =
state.line.offsety
func size(ictx: var InlineContext): var Size =
ictx.root.state.size
# Whitespace between words
func computeShift(ictx: InlineContext; state: InlineState): LayoutUnit =
if ictx.whitespacenum == 0:
return 0
if ictx.whitespaceIsLF and state.lastrw == 2 and state.firstrw == 2:
# skip line feed between double-width characters
return 0
if not state.fragment.computed.whitespacepre:
if ictx.currentLine.atoms.len == 0 or
ictx.currentLine.atoms[^1].t == iatSpacing:
return 0
return ictx.cellWidth * ictx.whitespacenum
proc applyLineHeight(ictx: InlineContext; state: var LineBoxState;
computed: CSSComputedValues) =
let lctx = ictx.lctx
let lineHeight = if computed{"line-height"}.auto: # ergo normal
lctx.cellHeight.toLayoutUnit()
else:
# Percentage: refers to the font size of the element itself.
computed{"line-height"}.px(lctx, lctx.cellHeight)
let paddingTop = computed{"padding-top"}.px(lctx, ictx.space.w)
let paddingBottom = computed{"padding-bottom"}.px(lctx, ictx.space.w)
state.paddingTop = max(paddingTop, state.paddingTop)
state.paddingBottom = max(paddingBottom, state.paddingBottom)
state.lineHeight = max(lineHeight, state.lineHeight)
proc newWord(ictx: var InlineContext; state: var InlineState) =
ictx.word = InlineAtom(
t: iatWord,
size: size(w = 0, h = ictx.cellHeight)
)
ictx.wordstate = InlineAtomState(
vertalign: state.fragment.computed{"vertical-align"},
baseline: ictx.cellHeight
)
ictx.wrappos = -1
ictx.hasshy = false
func overflow(atom: InlineAtom; dim: DimensionType): Span =
if atom.t == iatInlineBlock:
let u = atom.offset[dim]
return Span(
start: u + atom.innerbox.state.overflow[dim].start,
send: u + atom.innerbox.state.overflow[dim].send
)
return Span(
start: atom.offset[dim],
send: atom.offset[dim] + atom.size[dim]
)
proc expand(a: var Span; b: Span) =
a.start = min(a.start, b.start)
a.send = max(a.send, b.send)
#TODO start & justify would be nice to have
const TextAlignNone = {
TextAlignStart, TextAlignLeft, TextAlignChaLeft, TextAlignJustify
}
proc horizontalAlignLines(ictx: var InlineContext) =
#TODO this is not quite correct, fit-content should use overflow width
# (without the min()).
let width = case ictx.space.w.t
of scMinContent, scMaxContent:
ictx.size.w
of scFitContent:
min(ictx.size.w, ictx.space.w.u)
of scStretch:
max(ictx.size.w, ictx.space.w.u)
let root = ictx.root
case ictx.computed{"text-align"}
of TextAlignNone:
discard
of TextAlignEnd, TextAlignRight, TextAlignChaRight:
# move everything
for line in ictx.lines:
let x = max(width, line.size.w) - line.size.w
for atom in line.atoms:
atom.offset.x += x
ictx.size.w = max(atom.offset.x + atom.size.w, ictx.size.w)
root.state.overflow[dtHorizontal].expand(atom.overflow(dtHorizontal))
of TextAlignCenter, TextAlignChaCenter:
# NOTE if we need line x offsets, use:
#let width = width - line.offset.x
for line in ictx.lines:
let x = max((max(width, line.size.w)) div 2 - line.size.w div 2, 0)
for atom in line.atoms:
atom.offset.x += x
ictx.size.w = max(atom.offset.x + atom.size.w, ictx.size.w)
root.state.overflow[dtHorizontal].expand(atom.overflow(dtHorizontal))
# Resize the line's height based on atoms' height and baseline.
# The line height should be at least as high as the highest baseline used by
# an atom plus that atom's height.
func resizeLine(currentLine: LineBoxState; lctx: LayoutContext): LayoutUnit =
let baseline = currentLine.baseline
var h = currentLine.size.h
for i, atom in currentLine.atoms:
let iastate = currentLine.atomstates[i]
# In all cases, the line's height must at least equal the atom's height.
# (Where the atom is actually placed is irrelevant here.)
h = max(h, atom.size.h)
case iastate.vertalign.keyword
of VerticalAlignBaseline:
# Line height must be at least as high as
# (atom baseline) + (atom height) + (extra height) - (line baseline).
h = max(atom.offset.y + atom.size.h - baseline, h)
of VerticalAlignMiddle:
# Line height must be at least
# (line baseline) + (atom height / 2).
h = max(baseline + atom.size.h div 2, h)
of VerticalAlignTop, VerticalAlignBottom:
# Line height must be at least atom height (already ensured above.)
discard
else:
# See baseline (with len = 0).
h = max(baseline - iastate.baseline + atom.size.h, h)
return h
# returns marginTop
proc positionAtoms(currentLine: LineBoxState; lctx: LayoutContext): LayoutUnit =
let baseline = currentLine.baseline
var marginTop: LayoutUnit = 0
for i, atom in currentLine.atoms:
let iastate = currentLine.atomstates[i]
case iastate.vertalign.keyword
of VerticalAlignBaseline:
# Atom is placed at (line baseline) - (atom baseline) - len
atom.offset.y = baseline - atom.offset.y
of VerticalAlignMiddle:
# Atom is placed at (line baseline) - ((atom height) / 2)
atom.offset.y = baseline - atom.size.h div 2
of VerticalAlignTop:
# Atom is placed at the top of the line.
atom.offset.y = 0
of VerticalAlignBottom:
# Atom is placed at the bottom of the line.
atom.offset.y = currentLine.size.h - atom.size.h
else:
# See baseline (with len = 0).
atom.offset.y = baseline - iastate.baseline
# Find the best top margin of all atoms.
# We are looking for the lowest top edge of the line, so we have to do this
# after we know where the atoms will be placed.
# Note: we used to calculate the bottom edge based on margins too, but this
# generated pointless empty lines so I removed it.
marginTop = max(iastate.marginTop - atom.offset.y, marginTop)
return marginTop
proc shiftAtoms(ictx: var InlineContext; marginTop: LayoutUnit;
cellHeight: int) =
let offsety = ictx.currentLine.offsety
let shiftTop = marginTop + ictx.currentLine.paddingTop
let root = ictx.root
let noAlign = ictx.computed{"text-align"} in TextAlignNone
for atom in ictx.currentLine.atoms:
atom.offset.y = (atom.offset.y + shiftTop + offsety).round(cellHeight)
let minHeight = atom.offset.y - offsety + atom.size.h
ictx.currentLine.minHeight = max(ictx.currentLine.minHeight, minHeight)
# Y is always final, so it is safe to calculate Y overflow
root.state.overflow[dtVertical].expand(atom.overflow(dtVertical))
if noAlign:
# X is final, calculate X overflow
root.state.overflow[dtHorizontal].expand(atom.overflow(dtHorizontal))
# Align atoms (inline boxes, text, etc.) vertically (i.e. along the block/y
# axis) inside the line.
proc verticalAlignLine(ictx: var InlineContext) =
# Start with line-height as the baseline and line height.
let lineHeight = ictx.currentLine.lineHeight
ictx.currentLine.size.h = lineHeight
let ch = ictx.cellHeight
# Baseline is what we computed in addAtom, or lineHeight if that's greater.
ictx.currentLine.baseline = max(ictx.currentLine.baseline, lineHeight)
.round(ch)
# Resize according to the baseline and atom sizes.
ictx.currentLine.size.h = ictx.currentLine.resizeLine(ictx.lctx)
# Now we can calculate the actual position of atoms inside the line.
let marginTop = ictx.currentLine.positionAtoms(ictx.lctx)
# Finally, offset all atoms' y position by the largest top margin and the
# line box's top padding.
ictx.shiftAtoms(marginTop, ch)
#TODO this does not really work with rounding :/
ictx.currentLine.baseline += ictx.currentLine.paddingTop
# Ensure that the line is exactly as high as its highest atom demands,
# rounded up to the next line.
# (This is almost the same as completely ignoring line height. However, there
# *is* a difference, because line height is still taken into account when
# positioning the atoms.)
ictx.currentLine.size.h = ictx.currentLine.minHeight.ceilTo(ch)
# Now, if we got a height that is lower than cell height *and* line height,
# then set it back to the cell height. (This is to avoid the situation where
# we would swallow hard line breaks with <br>.)
if lineHeight >= ch and ictx.currentLine.size.h < ch:
ictx.currentLine.size.h = ch
# Set the line height to size.h.
ictx.currentLine.line.height = ictx.currentLine.size.h
proc putAtom(state: var LineBoxState; atom: InlineAtom;
iastate: InlineAtomState; fragment: InlineFragment) =
state.atomstates.add(iastate)
state.atoms.add(atom)
fragment.state.atoms.add(atom)
proc addSpacing(ictx: var InlineContext; width, height: LayoutUnit;
state: InlineState; hang = false) =
let spacing = InlineAtom(
t: iatSpacing,
size: size(w = width, h = height),
offset: offset(x = ictx.currentLine.size.w, y = height)
)
let iastate = InlineAtomState(baseline: height)
if not hang:
# In some cases, whitespace may "hang" at the end of the line. This means
# it is written, but is not actually counted in the box's width.
ictx.currentLine.size.w += width
ictx.currentLine.putAtom(spacing, iastate, ictx.whitespaceFragment)
proc flushWhitespace(ictx: var InlineContext; state: InlineState;
hang = false) =
let shift = ictx.computeShift(state)
ictx.currentLine.charwidth += ictx.whitespacenum
ictx.whitespacenum = 0
if shift > 0:
ictx.addSpacing(shift, ictx.cellHeight, state, hang)
# Prepare the next line's initial width and available width.
# (If space on the left is excluded by floats, set the initial width to
# the end of that space. If space on the right is excluded, set the available
# width to that space.)
proc initLine(ictx: var InlineContext) =
ictx.currentLine.availableWidth = ictx.space.w.u
let bctx = ictx.bctx
#TODO what if maxContent/minContent?
if bctx.exclusions.len != 0:
let bfcOffset = ictx.bfcOffset
let y = ictx.currentLine.offsety + bfcOffset.y
var left = bfcOffset.x
var right = bfcOffset.x + ictx.currentLine.availableWidth
for ex in bctx.exclusions:
if ex.offset.y <= y and y < ex.offset.y + ex.size.h:
ictx.currentLine.hasExclusion = true
if ex.t == FloatLeft:
left = ex.offset.x + ex.size.w
else:
right = ex.offset.x
ictx.currentLine.line.size.w = left - bfcOffset.x
ictx.currentLine.availableWidth = right - bfcOffset.x
proc finishLine(ictx: var InlineContext; state: var InlineState; wrap: bool;
force = false) =
if ictx.currentLine.atoms.len != 0 or force:
let whitespace = state.fragment.computed{"white-space"}
if whitespace == WhitespacePre:
ictx.flushWhitespace(state)
elif whitespace == WhitespacePreWrap:
ictx.flushWhitespace(state, hang = true)
else:
ictx.whitespacenum = 0
ictx.verticalAlignLine()
# add line to ictx
let y = ictx.currentLine.offsety
# * set first baseline if this is the first line box
# * always set last baseline (so the baseline of the last line box remains)
if ictx.lines.len == 0:
ictx.root.state.firstBaseline = y + ictx.currentLine.baseline
ictx.root.state.baseline = y + ictx.currentLine.baseline
ictx.size.h += ictx.currentLine.size.h
let lineWidth = if wrap:
ictx.currentLine.availableWidth
else:
ictx.currentLine.size.w
if state.firstLine:
#TODO padding top
state.fragment.state.startOffset = offset(
x = state.startOffsetTop.x,
y = y + ictx.currentLine.size.h
)
state.firstLine = false
ictx.size.w = max(ictx.size.w, lineWidth)
ictx.lines.add(ictx.currentLine.line)
ictx.currentLine = LineBoxState(
line: LineBox(offsety: y + ictx.currentLine.size.h)
)
ictx.initLine()
proc addBackgroundAreas(ictx: var InlineContext; rootFragment: InlineFragment) =
var traverseStack: seq[InlineFragment] = @[rootFragment]
var currentStack: seq[InlineFragment] = @[]
template top: InlineFragment = currentStack[^1]
var atomIdx = 0
var lineSkipped = false
for line in ictx.lines:
if line.atoms.len == 0:
# no atoms here; set lineSkipped to true so that we don't accidentally
# extend background areas over this
lineSkipped = true
continue
var prevEnd: LayoutUnit = 0
for atom in line.atoms:
if currentStack.len == 0 or atomIdx >= top.state.atoms.len:
atomIdx = 0
while true:
let thisNode = traverseStack.pop()
if thisNode == nil: # sentinel found
let oldTop = currentStack.pop()
# finish oldTop area
if oldTop.state.areas[^1].offset.y == line.offsety:
# if offset.y is this offsety, then it means that we added it on
# this line, so we just have to set its width
if prevEnd > 0:
oldTop.state.areas[^1].size.w = prevEnd -
oldTop.state.areas[^1].offset.x
else:
# fragment got dropped without prevEnd moving anywhere; delete
# area
oldTop.state.areas.setLen(oldTop.state.areas.high)
elif prevEnd > 0:
# offset.y is presumably from a previous line
# (if prevEnd is 0, then the area doesn't extend to this line,
# so we do not have to do anything.)
let x = line.atoms[0].offset.x
let w = prevEnd - x
if oldTop.state.areas[^1].offset.x == x and
oldTop.state.areas[^1].size.w == w:
# same vertical dimensions; just extend.
oldTop.state.areas[^1].size.h = line.offsety + line.height -
oldTop.state.areas[^1].offset.y
else:
# vertical dimensions differ; add new area.
oldTop.state.areas.add(Area(
offset: offset(x = x, y = line.offsety),
size: size(w = w, h = line.height)
))
continue
traverseStack.add(nil) # sentinel
if thisNode.t == iftParent:
for i in countdown(thisNode.children.high, 0):
traverseStack.add(thisNode.children[i])
thisNode.state.areas.add(Area(
offset: offset(x = atom.offset.x, y = line.offsety),
size: size(w = atom.size.w, h = line.height)
))
currentStack.add(thisNode)
if thisNode.state.atoms.len > 0:
break
prevEnd = atom.offset.x + atom.size.w
assert top.state.atoms[atomIdx] == atom
inc atomIdx
# extend current areas
for node in currentStack:
if node.state.areas[^1].offset.y == line.offsety:
# added in this iteration. no need to extend vertically, but make sure
# that it reaches prevEnd.
node.state.areas[^1].size.w = prevEnd - node.state.areas[^1].offset.x
continue
let x1 = node.state.areas[^1].offset.x
let x2 = node.state.areas[^1].offset.x + node.state.areas[^1].size.w
if x1 == line.atoms[0].offset.x and x2 == prevEnd and not lineSkipped:
# horizontal dimensions are the same as for the last area. just move its
# vertical end to the current line's end.
node.state.areas[^1].size.h = line.offsety + line.height -
node.state.areas[^1].offset.y
else:
# horizontal dimensions differ; add a new area
node.state.areas.add(Area(
offset: offset(x = line.atoms[0].offset.x, y = line.offsety),
size: size(w = prevEnd - line.atoms[0].offset.x, h = line.height)
))
lineSkipped = false
func xminwidth(atom: InlineAtom): LayoutUnit =
if atom.t == iatInlineBlock:
return atom.innerbox.state.xminwidth
return atom.size.w
func shouldWrap(ictx: InlineContext; w: LayoutUnit;
pcomputed: CSSComputedValues): bool =
if pcomputed != nil and pcomputed.nowrap:
return false
if ictx.space.w.t == scMaxContent:
return false # no wrap with max-content
if ictx.space.w.t == scMinContent:
return true # always wrap with min-content
return ictx.currentLine.size.w + w > ictx.currentLine.availableWidth
func shouldWrap2(ictx: InlineContext; w: LayoutUnit): bool =
if not ictx.currentLine.hasExclusion:
return false
return ictx.currentLine.size.w + w > ictx.currentLine.availableWidth
# Start a new line, even if the previous one is empty
proc flushLine(ictx: var InlineContext; state: var InlineState) =
ictx.applyLineHeight(ictx.currentLine, state.fragment.computed)
ictx.finishLine(state, wrap = false, force = true)
# Add an inline atom atom, with state iastate.
# Returns true on newline.
proc addAtom(ictx: var InlineContext; state: var InlineState;
iastate: InlineAtomState; atom: InlineAtom): bool =
result = false
var shift = ictx.computeShift(state)
ictx.currentLine.charwidth += ictx.whitespacenum
ictx.whitespacenum = 0
# Line wrapping
if ictx.shouldWrap(atom.size.w + shift, state.fragment.computed):
ictx.finishLine(state, wrap = true, force = false)
result = true
# Recompute on newline
shift = ictx.computeShift(state)
# For floats: flush lines until we can place the atom.
#TODO this is inefficient
while ictx.shouldWrap2(atom.size.w + shift):
ictx.applyLineHeight(ictx.currentLine, state.fragment.computed)
ictx.currentLine.lineHeight = max(ictx.currentLine.lineHeight,
ictx.cellHeight)
ictx.finishLine(state, wrap = false, force = true)
# Recompute on newline
shift = ictx.computeShift(state)
if atom.size.w > 0 and atom.size.h > 0:
if shift > 0:
ictx.addSpacing(shift, ictx.cellHeight, state)
ictx.root.state.xminwidth = max(ictx.root.state.xminwidth, atom.xminwidth)
ictx.applyLineHeight(ictx.currentLine, state.fragment.computed)
if atom.t != iatWord:
ictx.currentLine.charwidth = 0
ictx.currentLine.putAtom(atom, iastate, state.fragment)
atom.offset.x += ictx.currentLine.size.w
ictx.currentLine.size.w += atom.size.w
let baseline = case iastate.vertalign.keyword
of VerticalAlignBaseline:
let len = iastate.vertalign.length.px(ictx.lctx, state.lineHeight)
iastate.baseline + len
of VerticalAlignTop, VerticalAlignBottom:
atom.size.h
of VerticalAlignMiddle:
atom.size.h div 2
else:
iastate.baseline
# store for later use in resizeLine/shiftAtoms
atom.offset.y = baseline
ictx.currentLine.baseline = max(ictx.currentLine.baseline, baseline)
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.
result = ictx.addAtom(state, ictx.wordstate, ictx.word)
ictx.newWord(state)
proc addWordEOL(ictx: var InlineContext; state: var InlineState): bool =
result = false
if ictx.word.str != "":
if ictx.wrappos != -1:
let leftstr = ictx.word.str.substr(ictx.wrappos)
ictx.word.str.setLen(ictx.wrappos)
if ictx.hasshy:
const shy = $Rune(0xAD) # soft hyphen
ictx.word.str &= shy
ictx.hasshy = false
result = ictx.addWord(state)
ictx.word.str = leftstr
ictx.word.size.w = leftstr.width() * ictx.cellWidth
else:
result = ictx.addWord(state)
proc checkWrap(ictx: var InlineContext; state: var InlineState; r: Rune) =
if state.fragment.computed.nowrap:
return
let shift = ictx.computeShift(state)
let rw = r.width()
state.prevrw = rw
if ictx.word.str.len == 0:
state.firstrw = rw
if rw >= 2:
# remove wrap opportunity, so we wrap properly on the last CJK char (instead
# of any dash inside CJK sentences)
ictx.wrappos = -1
case state.fragment.computed{"word-break"}
of WordBreakNormal:
if rw == 2 or ictx.wrappos != -1: # break on cjk and wrap opportunities
let plusWidth = ictx.word.size.w + shift + rw * ictx.cellWidth
if ictx.shouldWrap(plusWidth, nil):
if not ictx.addWordEOL(state): # no line wrapping occured in addAtom
ictx.finishLine(state, wrap = true)
ictx.whitespacenum = 0
of WordBreakBreakAll:
let plusWidth = ictx.word.size.w + shift + rw * ictx.cellWidth
if ictx.shouldWrap(plusWidth, nil):
if not ictx.addWordEOL(state): # no line wrapping occured in addAtom
ictx.finishLine(state, wrap = true)
ictx.whitespacenum = 0
of WordBreakKeepAll:
let plusWidth = ictx.word.size.w + shift + rw * ictx.cellWidth
if ictx.shouldWrap(plusWidth, nil):
ictx.finishLine(state, wrap = true)
ictx.whitespacenum = 0
proc processWhitespace(ictx: var InlineContext; state: var InlineState;
c: char) =
discard ictx.addWord(state)
case state.fragment.computed{"white-space"}
of WhitespaceNormal, WhitespaceNowrap:
if ictx.whitespacenum < 1:
ictx.whitespacenum = 1
ictx.whitespaceFragment = state.fragment
ictx.whitespaceIsLF = c == '\n'
if c != '\n':
ictx.whitespaceIsLF = false
of WhitespacePreLine:
if c == '\n':
ictx.flushLine(state)
elif ictx.whitespacenum < 1:
ictx.whitespaceIsLF = false
ictx.whitespacenum = 1
ictx.whitespaceFragment = state.fragment
of WhitespacePre, WhitespacePreWrap:
#TODO whitespace type should be preserved here. (it isn't, because
# it would break tabs in the current buffer model.)
ictx.whitespaceIsLF = false
if c == '\n':
ictx.flushLine(state)
elif c == '\t':
let realWidth = ictx.currentLine.charwidth + ictx.whitespacenum
let targetTabStops = realWidth div 8 + 1
let targetWidth = targetTabStops * 8
ictx.whitespacenum += targetWidth - realWidth
ictx.whitespaceFragment = state.fragment
else:
inc ictx.whitespacenum
ictx.whitespaceFragment = state.fragment
# set the "last word's last rune width" to the previous rune width
state.lastrw = state.prevrw
func initInlineContext(bctx: var BlockContext; space: AvailableSpace;
bfcOffset: Offset; root: RootInlineFragment;
computed: CSSComputedValues): InlineContext =
var ictx = InlineContext(
currentLine: LineBoxState(
line: LineBox()
),
bctx: addr bctx,
lctx: bctx.lctx,
bfcOffset: bfcOffset,
space: space,
root: root,
computed: computed
)
ictx.initLine()
return ictx
proc layoutTextLoop(ictx: var InlineContext; state: var InlineState;
str: string) =
var i = 0
while i < str.len:
let c = str[i]
if c in Ascii:
if c in AsciiWhitespace:
ictx.processWhitespace(state, c)
else:
let r = Rune(c)
ictx.checkWrap(state, r)
ictx.word.str &= c
let w = r.width()
ictx.word.size.w += w * ictx.cellWidth
ictx.currentLine.charwidth += w
if c == '-': # ascii dash
ictx.wrappos = ictx.word.str.len
ictx.hasshy = false
inc i
else:
var r: Rune
fastRuneAt(str, i, r)
ictx.checkWrap(state, r)
if r == Rune(0xAD): # soft hyphen
ictx.wrappos = ictx.word.str.len
ictx.hasshy = true
else:
ictx.word.str &= r
let w = r.width()
ictx.word.size.w += w * ictx.cellWidth
ictx.currentLine.charwidth += w
discard ictx.addWord(state)
let shift = ictx.computeShift(state)
ictx.currentLine.widthAfterWhitespace = ictx.currentLine.size.w + shift
proc layoutText(ictx: var InlineContext; state: var InlineState; s: string) =
ictx.flushWhitespace(state)
ictx.newWord(state)
let transform = state.fragment.computed{"text-transform"}
if transform == TextTransformNone:
ictx.layoutTextLoop(state, s)
else:
let s = case transform
of TextTransformCapitalize: s.capitalizeLU()
of TextTransformUppercase: s.toUpperLU()
of TextTransformLowercase: s.toLowerLU()
of TextTransformFullWidth: s.fullwidth()
of TextTransformFullSizeKana: s.fullsize()
of TextTransformChaHalfWidth: s.halfwidth()
else: ""
ictx.layoutTextLoop(state, s)
func spx(l: CSSLength; lctx: LayoutContext; p: SizeConstraint;
computed: CSSComputedValues; padding: LayoutUnit): LayoutUnit =
let u = l.px(lctx, p)
if computed{"box-sizing"} == BoxSizingBorderBox:
return max(u - padding, 0)
return max(u, 0)
proc resolveContentWidth(sizes: var ResolvedSizes; widthpx: LayoutUnit;
parentWidth: SizeConstraint; computed: CSSComputedValues;
isauto = false) =
if not sizes.space.w.isDefinite() or not parentWidth.isDefinite():
# 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 or sizes.space.w.t == scFitContent:
if underflow >= 0:
sizes.space.w = SizeConstraint(t: sizes.space.w.t, u: underflow)
else:
sizes.margin[dtHorizontal].send += underflow
elif underflow > 0:
if not computed{"margin-left"}.auto and not computed{"margin-right"}.auto:
sizes.margin[dtHorizontal].send += underflow
elif not computed{"margin-left"}.auto and computed{"margin-right"}.auto:
sizes.margin[dtHorizontal].send = underflow
elif computed{"margin-left"}.auto and not computed{"margin-right"}.auto:
sizes.margin[dtHorizontal].start = underflow
else:
sizes.margin[dtHorizontal].start = underflow div 2
sizes.margin[dtHorizontal].send = underflow div 2
proc resolveMargins(availableWidth: SizeConstraint; lctx: LayoutContext;
computed: CSSComputedValues): RelativeRect =
# Note: we use availableWidth for percentage resolution intentionally.
return [
dtHorizontal: Span(
start: computed{"margin-left"}.px(lctx, availableWidth),
send: computed{"margin-right"}.px(lctx, availableWidth),
),
dtVertical: Span(
start: computed{"margin-top"}.px(lctx, availableWidth),
send: computed{"margin-bottom"}.px(lctx, availableWidth),
)
]
proc resolvePadding(availableWidth: SizeConstraint; lctx: LayoutContext;
computed: CSSComputedValues): RelativeRect =
# Note: we use availableWidth for percentage resolution intentionally.
return [
dtHorizontal: Span(
start: computed{"padding-left"}.px(lctx, availableWidth),
send: computed{"padding-right"}.px(lctx, availableWidth)
),
dtVertical: Span(
start: computed{"padding-top"}.px(lctx, availableWidth),
send: computed{"padding-bottom"}.px(lctx, availableWidth),
)
]
func resolvePositioned(space: AvailableSpace; lctx: LayoutContext;
computed: CSSComputedValues): RelativeRect =
# As per standard, vertical percentages refer to the *height*, not the width
# (unlike with margin/padding)
return [
dtHorizontal: Span(
start: computed{"left"}.px(lctx, space.w),
send: computed{"right"}.px(lctx, space.w)
),
dtVertical: Span(
start: computed{"top"}.px(lctx, space.h),
send: computed{"bottom"}.px(lctx, space.h),
)
]
func resolveMinMaxSize(length: CSSLength; sc: SizeConstraint;
fallback, padding: LayoutUnit; computed: CSSComputedValues;
lctx: LayoutContext): LayoutUnit =
if length.canpx(sc):
return length.spx(lctx, sc, computed, padding)
return fallback
func resolveMinMaxSizes(lctx: LayoutContext; space: AvailableSpace;
inlinePadding, blockPadding: LayoutUnit; computed: CSSComputedValues):
array[DimensionType, Span] =
return [
dtHorizontal: Span(
start: computed{"min-width"}.resolveMinMaxSize(space.w, 0, inlinePadding,
computed, lctx),
send: computed{"max-width"}.resolveMinMaxSize(space.w, LayoutUnit.high,
inlinePadding, computed, lctx)
),
dtVertical: Span(
start: computed{"min-height"}.resolveMinMaxSize(space.h, 0, blockPadding,
computed, lctx),
send: computed{"max-height"}.resolveMinMaxSize(space.h, LayoutUnit.high,
blockPadding, computed, lctx)
)
]
proc resolveBlockWidth(sizes: var ResolvedSizes; parentWidth: SizeConstraint;
inlinePadding: LayoutUnit; computed: CSSComputedValues;
lctx: LayoutContext) =
let width = computed{"width"}
var widthpx: LayoutUnit = 0
if width.canpx(parentWidth):
widthpx = width.spx(lctx, parentWidth, computed, inlinePadding)
sizes.space.w = stretch(widthpx)
sizes.resolveContentWidth(widthpx, parentWidth, computed, width.auto)
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:
# available width would stretch over max-width
sizes.space.w = stretch(sizes.maxWidth)
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)
if sizes.space.w.isDefinite() and sizes.minWidth > sizes.space.w.u or
sizes.minWidth > 0 and sizes.space.w.t == scMinContent:
# two cases:
# * available width is stretched under min-width. in this case,
# stretch to min-width instead.
# * 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)
proc resolveBlockHeight(sizes: var ResolvedSizes; parentHeight: SizeConstraint;
blockPadding: LayoutUnit; computed: CSSComputedValues;
lctx: LayoutContext) =
let height = computed{"height"}
if height.canpx(parentHeight):
let heightpx = height.spx(lctx, parentHeight, computed, blockPadding)
sizes.space.h = stretch(heightpx)
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.
if sizes.space.h.t == scStretch:
sizes.space.h = stretch(sizes.maxHeight)
else: # scFitContent
sizes.space.h = fitContent(sizes.maxHeight)
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)
proc resolveAbsoluteSize(sizes: var ResolvedSizes; space: AvailableSpace;
dim: DimensionType; cvalSize, cvalLeft, cvalRight: CSSLength;
computed: CSSComputedValues; lctx: LayoutContext) =
# Note: cvalLeft, cvalRight are top/bottom when called with vertical dim
if cvalSize.auto:
if space[dim].isDefinite:
let u = max(space[dim].u - sizes.positioned[dim].sum() -
sizes.margin[dim].sum() - sizes.padding[dim].sum(), 0)
if not cvalLeft.auto and not cvalRight.auto:
# width is auto and left & right are not auto.
# Solve for width.
sizes.space[dim] = stretch(u)
else:
# Return shrink to fit and solve for left/right.
sizes.space[dim] = fitContent(u)
else:
sizes.space[dim] = space[dim]
else:
let padding = sizes.padding[dim].sum()
let sizepx = cvalSize.spx(lctx, space[dim], computed, padding)
# We could solve for left/right here, as available width is known.
# Nevertheless, it is only needed for positioning, so we do not solve
# them yet.
sizes.space[dim] = stretch(sizepx)
proc resolveBlockSizes(lctx: LayoutContext; space: AvailableSpace;
computed: CSSComputedValues): ResolvedSizes =
let padding = resolvePadding(space.w, lctx, computed)
let inlinePadding = padding[dtHorizontal].sum()
let blockPadding = padding[dtVertical].sum()
var sizes = ResolvedSizes(
margin: resolveMargins(space.w, lctx, computed),
padding: padding,
# Take defined sizes if our width/height resolves to auto.
# For block boxes, this is:
# (width: stretch(parentWidth), height: max-content)
space: space,
minMaxSizes: lctx.resolveMinMaxSizes(space, inlinePadding, blockPadding,
computed)
)
if computed{"display"} == DisplayTableWrapper:
sizes.space.w = fitContent(sizes.space.w)
# height is max-content normally, but fit-content for clip.
sizes.space.h = if computed{"overflow"} != OverflowClip:
maxContent()
else:
fitContent(sizes.space.h)
if computed{"position"} == PositionRelative:
# only compute this when needed
sizes.positioned = resolvePositioned(space, lctx, computed)
# Finally, calculate available width and height.
sizes.resolveBlockWidth(space.w, inlinePadding, computed, lctx)
#TODO parent height should be lctx height in quirks mode for percentage
# resolution.
sizes.resolveBlockHeight(space.h, blockPadding, computed, lctx)
return sizes
# Calculate and resolve available width & height for absolutely positioned
# boxes.
proc resolveAbsoluteSizes(lctx: LayoutContext; computed: CSSComputedValues):
ResolvedSizes =
let space = lctx.positioned[^1]
var sizes = ResolvedSizes(
margin: resolveMargins(space.w, lctx, computed),
padding: resolvePadding(space.w, lctx, computed),
positioned: resolvePositioned(space, lctx, computed),
minMaxSizes: [dtHorizontal: DefaultSpan, dtVertical: DefaultSpan]
)
sizes.resolveAbsoluteSize(space, dtHorizontal, computed{"width"},
computed{"left"}, computed{"right"}, computed, lctx)
sizes.resolveAbsoluteSize(space, dtVertical, computed{"height"},
computed{"top"}, computed{"bottom"}, computed, lctx)
return sizes
# Calculate and resolve available width & height for floating boxes.
proc resolveFloatSizes(lctx: LayoutContext; space: AvailableSpace;
preserveHeight: bool; computed: CSSComputedValues):
ResolvedSizes =
let padding = resolvePadding(space.w, lctx, computed)
let inlinePadding = padding[dtHorizontal].sum()
let blockPadding = padding[dtVertical].sum()
var sizes = ResolvedSizes(
margin: resolveMargins(space.w, lctx, computed),
padding: padding,
space: availableSpace(w = fitContent(space.w), h = space.h),
# note: we use parent's space here intentionally.
minMaxSizes: lctx.resolveMinMaxSizes(space, inlinePadding, blockPadding,
computed)
)
if not preserveHeight: # Note: preserveHeight is only true for flex.
sizes.space.h = maxContent()
if computed{"width"}.canpx(sizes.space.w):
let widthpx = computed{"width"}.spx(lctx, sizes.space.w, computed,
inlinePadding)
sizes.space.w = stretch(clamp(widthpx, sizes.minWidth, sizes.maxWidth))
elif sizes.space.w.isDefinite():
sizes.space.w = fitContent(clamp(sizes.space.w.u, sizes.minWidth,
sizes.maxWidth))
if computed{"height"}.canpx(sizes.space.h):
let heightpx = computed{"height"}.spx(lctx, sizes.space.h, computed,
blockPadding)
sizes.space.h = stretch(clamp(heightpx, sizes.minHeight, sizes.maxHeight))
elif sizes.space.h.isDefinite():
sizes.space.h = fitContent(clamp(sizes.space.h.u, sizes.minHeight,
sizes.maxHeight))
return sizes
# Calculate and resolve available width, height, padding, margins, etc.
# space is the width/height of the containing box.
proc resolveSizes(lctx: LayoutContext; space: AvailableSpace;
computed: CSSComputedValues): ResolvedSizes =
if computed{"position"} == PositionAbsolute:
return lctx.resolveAbsoluteSizes(computed)
elif computed{"float"} != FloatNone:
return lctx.resolveFloatSizes(space, preserveHeight = false, computed)
else:
return lctx.resolveBlockSizes(space, computed)
proc append(a: var Strut; b: LayoutUnit) =
if b < 0:
a.neg = min(b, a.neg)
else:
a.pos = max(b, a.pos)
func sum(a: Strut): LayoutUnit =
return a.pos + a.neg
# forward declarations
proc layoutRootInline(bctx: var BlockContext; root: RootInlineFragment;
space: AvailableSpace; computed: CSSComputedValues; offset, bfcOffset: Offset)
proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes)
proc layoutTableWrapper(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes)
proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes)
proc layoutInline(ictx: var InlineContext; fragment: InlineFragment)
proc layoutRootBlock(lctx: LayoutContext; box: BlockBox;
space: AvailableSpace; offset: Offset; marginBottomOut: var LayoutUnit)
# Note: padding must still be applied after this.
proc applySize(box: BlockBox; sizes: ResolvedSizes;
maxChildSize: LayoutUnit; space: AvailableSpace; dim: DimensionType) =
# Make the box as small/large as the content's width or specified width.
box.state.size[dim] = maxChildSize.applySizeConstraint(space[dim])
# Then, clamp it to minWidth and maxWidth (if applicable).
let span = sizes.minMaxSizes[dim]
box.state.size[dim] = clamp(box.state.size[dim], span.start, span.send)
proc applyWidth(box: BlockBox; sizes: ResolvedSizes;
maxChildWidth: LayoutUnit; space: AvailableSpace) =
box.applySize(sizes, maxChildWidth, space, dtHorizontal)
proc applyWidth(box: BlockBox; sizes: ResolvedSizes;
maxChildWidth: LayoutUnit) =
box.applyWidth(sizes, maxChildWidth, sizes.space)
proc applyHeight(box: BlockBox; sizes: ResolvedSizes;
maxChildHeight: LayoutUnit) =
box.applySize(sizes, maxChildHeight, sizes.space, dtVertical)
proc applyPadding(box: BlockBox; padding: RelativeRect) =
box.state.size.w += padding[dtHorizontal].sum()
box.state.size.h += padding[dtVertical].sum()
func bfcOffset(bctx: BlockContext): Offset =
if bctx.parentBps != nil:
return bctx.parentBps.offset
return offset(x = 0, y = 0)
# expand to (0, size[dim].u)
func finalize(overflow: var Overflow; size: Size) =
overflow[dtHorizontal].expand(Span(start: 0, send: size[dtHorizontal]))
overflow[dtVertical].expand(Span(start: 0, send: size[dtVertical]))
proc layoutInline(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
var bfcOffset = bctx.bfcOffset
let offset = offset(x = sizes.padding.left, y = sizes.padding.top)
bfcOffset.x += box.state.offset.x + offset.x
bfcOffset.y += box.state.offset.y + offset.y
bctx.layoutRootInline(box.inline, sizes.space, box.computed, offset,
bfcOffset)
box.state.xminwidth = max(box.state.xminwidth, box.inline.state.xminwidth)
box.state.size.w = box.inline.state.size.w + sizes.padding[dtHorizontal].sum()
box.applyWidth(sizes, box.inline.state.size.w)
box.applyHeight(sizes, box.inline.state.size.h)
box.applyPadding(sizes.padding)
box.state.baseline = offset.y + box.inline.state.baseline
box.state.firstBaseline = offset.y + box.inline.state.firstBaseline
box.state.overflow = box.inline.state.overflow
# shift overflow
for dim in DimensionType:
box.state.overflow[dim] += offset[dim]
box.state.overflow.finalize(box.state.size)
const DisplayBlockLike = {DisplayBlock, DisplayListItem, DisplayInlineBlock}
# Return true if no more margin collapsing can occur for the current strut.
func canFlushMargins(box: BlockBox; sizes: ResolvedSizes): bool =
if box.computed{"position"} == PositionAbsolute:
return false
return sizes.padding.top != 0 or sizes.padding.bottom != 0 or
box.inline != nil or box.computed{"display"} notin DisplayBlockLike or
box.computed{"clear"} != ClearNone
proc flushMargins(bctx: var BlockContext; box: BlockBox) =
# Apply uncommitted margins.
let margin = bctx.marginTodo.sum()
if bctx.marginTarget == nil:
box.state.offset.y += margin
else:
if bctx.marginTarget.box != nil:
bctx.marginTarget.box.state.offset.y += margin
var p = bctx.marginTarget
while true:
p.offset.y += margin
p.resolved = true
p = p.next
if p == nil: break
bctx.marginTarget = nil
bctx.marginTodo = Strut()
proc clearFloats(offset: var Offset; bctx: var BlockContext; clear: CSSClear) =
var y = bctx.bfcOffset.y + offset.y
case clear
of ClearLeft, ClearInlineStart:
for ex in bctx.exclusions:
if ex.t == FloatLeft:
y = max(ex.offset.y + ex.size.h, y)
of ClearRight, ClearInlineEnd:
for ex in bctx.exclusions:
if ex.t == FloatRight:
y = max(ex.offset.y + ex.size.h, y)
of ClearBoth:
for ex in bctx.exclusions:
y = max(ex.offset.y + ex.size.h, y)
of ClearNone: assert false
bctx.clearOffset = y
offset.y = y - bctx.bfcOffset.y
type
BlockState = object
offset: Offset
maxChildWidth: LayoutUnit
totalFloatWidth: LayoutUnit # used for re-layouts
maxChildOverflowWidth: LayoutUnit
space: AvailableSpace
xminwidth: LayoutUnit
prevParentBps: BlockPositionState
needsReLayout: bool
# State kept for when a re-layout is necessary:
oldMarginTodo: Strut
oldExclusionsLen: int
initialMarginTarget: BlockPositionState
initialTargetOffset: Offset
initialParentOffset: Offset
func findNextFloatOffset(bctx: BlockContext; offset: Offset; size: Size;
space: AvailableSpace; float: CSSFloat; outw: var LayoutUnit): Offset =
# Algorithm originally from QEmacs.
var y = offset.y
let leftStart = offset.x
let rightStart = offset.x + max(size.w, space.w.u)
while true:
var left = leftStart
var right = rightStart
var miny = high(LayoutUnit)
let cy2 = y + size.h
for ex in bctx.exclusions:
let ey2 = ex.offset.y + ex.size.h
if cy2 >= ex.offset.y and y < ey2:
let ex2 = ex.offset.x + ex.size.w
if ex.t == FloatLeft and left < ex2:
left = ex2
if ex.t == FloatRight and right > ex.offset.x:
right = ex.offset.x
miny = min(ey2, miny)
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
if float == FloatLeft:
return offset(x = left, y = y)
else: # FloatRight
return offset(x = right - size.w, y = y)
# Move y to the bottom exclusion edge at the lowest y (where the exclusion
# still intersects with the previous y).
y = miny
assert false
func findNextFloatOffset(bctx: BlockContext; offset: Offset; size: Size;
space: AvailableSpace; float: CSSFloat): Offset =
var dummy: LayoutUnit
return bctx.findNextFloatOffset(offset, size, space, float, dummy)
func findNextBlockOffset(bctx: BlockContext; offset: Offset; size: Size;
space: AvailableSpace; outw: var LayoutUnit): Offset =
return bctx.findNextFloatOffset(offset, size, space, FloatLeft, outw)
proc positionFloat(bctx: var BlockContext; child: BlockBox;
space: AvailableSpace; bfcOffset: Offset) =
let clear = child.computed{"clear"}
if clear != ClearNone:
child.state.offset.clearFloats(bctx, clear)
let size = size(
w = child.outerSize(dtHorizontal),
h = child.outerSize(dtVertical)
)
let childBfcOffset = offset(
x = bfcOffset.x + child.state.offset.x - child.state.margin.left,
y = max(bfcOffset.y + child.state.offset.y - child.state.margin.top,
bctx.clearOffset)
)
assert space.w.t != scFitContent
let ft = child.computed{"float"}
assert ft != FloatNone
let offset = bctx.findNextFloatOffset(childBfcOffset, size, space, ft)
child.state.offset = offset(
x = offset.x - bfcOffset.x + child.state.margin.left,
y = offset.y - bfcOffset.y + child.state.margin.top
)
let ex = Exclusion(offset: offset, size: size, t: ft)
bctx.exclusions.add(ex)
bctx.maxFloatHeight = max(bctx.maxFloatHeight, ex.offset.y + ex.size.h)
proc applyOverflowDimensions(box, child: BlockBox) =
var childOverflow = child.state.overflow
for dim in DimensionType:
childOverflow[dim] += child.state.offset[dim]
box.state.overflow[dim].expand(childOverflow[dim])
proc positionFloats(bctx: var BlockContext) =
for f in bctx.unpositionedFloats:
bctx.positionFloat(f.box, f.space, f.parentBps.offset)
# Propagate overflow dimensions to the float's parent box.
f.parentBox.applyOverflowDimensions(f.box)
bctx.unpositionedFloats.setLen(0)
proc layoutFlow(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
if box.canFlushMargins(sizes):
bctx.flushMargins(box)
bctx.positionFloats()
if box.computed{"clear"} != ClearNone:
box.state.offset.clearFloats(bctx, box.computed{"clear"})
if box.inline != nil:
# Builder only contains inline boxes.
bctx.layoutInline(box, sizes)
else:
# Builder only contains block boxes.
bctx.layoutBlock(box, sizes)
proc layoutListItem(bctx: var BlockContext; box: BlockBox;
sizes: ResolvedSizes) =
case box.computed{"list-style-position"}
of ListStylePositionOutside:
let marker = box.nested[0]
let content = box.nested[1]
marker.state = BlockBoxLayoutState()
content.state = BlockBoxLayoutState(
offset: box.state.offset,
positioned: sizes.positioned
)
bctx.layoutFlow(content, sizes)
#TODO we should put markers right before the first atom of the parent
# list item or something...
var bctx = BlockContext(lctx: bctx.lctx)
let markerSizes = ResolvedSizes(
space: availableSpace(w = fitContent(sizes.space.w), h = sizes.space.h),
minMaxSizes: [dtHorizontal: DefaultSpan, dtVertical: DefaultSpan]
)
bctx.layoutFlow(marker, markerSizes)
marker.state.offset.x = -marker.state.size.w
# take inner box min width etc.
box.state = content.state
content.state.offset = offset(x = 0, y = 0)
content.state.margin = [Span(), Span()]
content.state.positioned = [Span(), Span()]
of ListStylePositionInside:
bctx.layoutFlow(box, sizes)
proc addInlineBlock(ictx: var InlineContext; state: var InlineState;
box: BlockBox) =
let lctx = ictx.lctx
let sizes = lctx.resolveFloatSizes(ictx.space, preserveHeight = false,
box.computed)
box.state = BlockBoxLayoutState(
margin: sizes.margin,
positioned: sizes.positioned
)
var bctx = BlockContext(lctx: lctx)
bctx.marginTodo.append(sizes.margin.top)
case box.computed{"display"}
of DisplayInlineBlock: bctx.layoutFlow(box, sizes)
of DisplayInlineTableWrapper: bctx.layoutTableWrapper(box, sizes)
of DisplayInlineFlex: bctx.layoutFlex(box, sizes)
else: assert false
assert bctx.unpositionedFloats.len == 0
bctx.marginTodo.append(sizes.margin.bottom)
let marginTop = box.state.offset.y
let marginBottom = bctx.marginTodo.sum()
# If the highest float edge is higher than the box itself, set that as
# the box height.
if bctx.maxFloatHeight > box.state.size.h + marginBottom:
box.state.size.h = bctx.maxFloatHeight - marginBottom
box.state.offset.y = 0
# Apply the block box's properties to the atom itself.
let iblock = InlineAtom(
t: iatInlineBlock,
innerbox: box,
offset: offset(x = sizes.margin.left, y = 0),
size: size(
w = box.outerSize(dtHorizontal),
h = box.state.size.h
)
)
let iastate = InlineAtomState(
baseline: box.state.baseline,
vertalign: box.computed{"vertical-align"},
marginTop: marginTop,
marginBottom: bctx.marginTodo.sum()
)
discard ictx.addAtom(state, iastate, iblock)
ictx.whitespacenum = 0
proc addInlineImage(ictx: var InlineContext; state: var InlineState;
bmp: Bitmap) =
let h = int(bmp.height).toLayoutUnit().ceilTo(ictx.cellHeight)
let iastate = InlineAtomState(
vertalign: state.fragment.computed{"vertical-align"},
baseline: h
)
let atom = InlineAtom(
t: iatImage,
bmp: bmp,
size: size(w = int(bmp.width), h = h), #TODO overflow
)
discard ictx.addAtom(state, iastate, atom)
func calcLineHeight(computed: CSSComputedValues; lctx: LayoutContext):
LayoutUnit =
if computed{"line-height"}.auto: # ergo normal
return lctx.cellHeight.toLayoutUnit()
# Percentage: refers to the font size of the element itself.
return computed{"line-height"}.px(lctx, lctx.cellHeight)
proc layoutInline(ictx: var InlineContext; fragment: InlineFragment) =
let lctx = ictx.lctx
let computed = fragment.computed
fragment.state = InlineFragmentState()
if stSplitStart in fragment.splitType:
ictx.currentLine.size.w += computed{"margin-left"}.px(lctx, ictx.space.w)
ictx.currentLine.size.w += computed{"padding-left"}.px(lctx, ictx.space.w)
var state = InlineState(
fragment: fragment,
firstLine: true,
startOffsetTop: offset(
x = ictx.currentLine.widthAfterWhitespace,
y = ictx.currentLine.offsety
),
lineHeight: computed.calcLineHeight(lctx)
)
ictx.applyLineHeight(ictx.currentLine, computed)
case fragment.t
of iftNewline:
ictx.flushLine(state)
of iftBox:
ictx.addInlineBlock(state, fragment.box)
of iftBitmap:
ictx.addInlineImage(state, fragment.bmp)
of iftText:
ictx.layoutText(state, fragment.text)
of iftParent:
for child in fragment.children:
ictx.layoutInline(child)
if stSplitEnd in fragment.splitType:
ictx.currentLine.size.w += computed{"padding-right"}.px(lctx, ictx.space.w)
ictx.currentLine.size.w += computed{"margin-right"}.px(lctx, ictx.space.w)
if state.firstLine:
fragment.state.startOffset = offset(
x = state.startOffsetTop.x,
y = ictx.currentLine.offsety
)
else:
fragment.state.startOffset = offset(x = 0, y = ictx.currentLine.offsety)
if fragment.t != iftParent:
if not ictx.textFragmentSeen:
ictx.textFragmentSeen = true
ictx.root.fragment.state.startOffset = fragment.state.startOffset
ictx.lastTextFragment = fragment
proc layoutRootInline(bctx: var BlockContext; root: RootInlineFragment;
space: AvailableSpace; computed: CSSComputedValues;
offset, bfcOffset: Offset) =
root.state = RootInlineFragmentState(offset: offset)
var ictx = bctx.initInlineContext(space, bfcOffset, root, computed)
ictx.layoutInline(root.fragment)
if ictx.lastTextFragment != nil:
let fragment = ictx.lastTextFragment
var state = InlineState(
fragment: fragment,
lineHeight: fragment.computed.calcLineHeight(ictx.lctx)
)
ictx.finishLine(state, wrap = false)
ictx.horizontalAlignLines()
ictx.addBackgroundAreas(root.fragment)
ictx.root.state.overflow.finalize(ictx.root.state.size)
proc positionAbsolute(lctx: LayoutContext; box: BlockBox;
margin: RelativeRect) =
let last = lctx.positioned[^1]
let parentWidth = applySizeConstraint(lctx.attrs.width_px, last.w)
let parentHeight = applySizeConstraint(lctx.attrs.height_px, last.h)
if not box.computed{"left"}.auto:
box.state.offset.x = box.state.positioned.left + margin.left
elif not box.computed{"right"}.auto:
box.state.offset.x = parentWidth - box.state.positioned.right -
box.state.size.w - margin.right
if not box.computed{"top"}.auto:
box.state.offset.y = box.state.positioned.top + margin.top
elif not box.computed{"bottom"}.auto:
box.state.offset.y = parentHeight - box.state.positioned.bottom -
box.state.size.h - margin.bottom
proc positionRelative(parent, box: BlockBox) =
if not box.computed{"left"}.auto:
box.state.offset.x += box.state.positioned.left
elif not box.computed{"right"}.auto:
box.state.offset.x += parent.state.size.w - box.state.positioned.right -
box.state.size.w
if not box.computed{"top"}.auto:
box.state.offset.y += box.state.positioned.top
elif not box.computed{"bottom"}.auto:
box.state.offset.y += parent.state.size.h - box.state.positioned.bottom -
box.state.size.h
# Note: caption is not included here
const RowGroupBox = {
DisplayTableRowGroup, DisplayTableHeaderGroup, DisplayTableFooterGroup
}
const ProperTableChild = RowGroupBox + {
DisplayTableRow, DisplayTableColumn, DisplayTableColumnGroup
}
const ProperTableRowParent = RowGroupBox + {
DisplayTable, DisplayInlineTable
}
type
CellWrapper = ref object
box: BlockBox
coli: int
colspan: int
rowspan: int
reflow: bool
grown: int # number of remaining rows
real: CellWrapper # for filler wrappers
last: bool # is this the last filler?
height: LayoutUnit
baseline: LayoutUnit
RowContext = object
cells: seq[CellWrapper]
reflow: seq[bool]
width: LayoutUnit
height: LayoutUnit
box: BlockBox
ncols: int
ColumnContext = object
minwidth: LayoutUnit
width: LayoutUnit
wspecified: bool
reflow: bool
weight: float64
TableContext = object
lctx: LayoutContext
rows: seq[RowContext]
cols: seq[ColumnContext]
growing: seq[CellWrapper]
maxwidth: LayoutUnit
blockSpacing: LayoutUnit
inlineSpacing: LayoutUnit
space: AvailableSpace # space we got from parent
proc layoutTableCell(lctx: LayoutContext; box: BlockBox;
space: AvailableSpace) =
var sizes = ResolvedSizes(
padding: resolvePadding(space.w, lctx, box.computed),
space: space,
minMaxSizes: [dtHorizontal: DefaultSpan, dtVertical: DefaultSpan]
)
if sizes.space.w.isDefinite():
sizes.space.w.u -= sizes.padding.left
sizes.space.w.u -= sizes.padding.right
if sizes.space.h.isDefinite():
sizes.space.h.u -= sizes.padding.top
sizes.space.h.u -= sizes.padding.bottom
box.state = BlockBoxLayoutState(positioned: sizes.positioned)
var bctx = BlockContext(lctx: lctx)
bctx.layoutFlow(box, sizes)
assert bctx.unpositionedFloats.len == 0
# Table cells ignore margins.
box.state.offset.y = 0
# 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)
# Sort growing cells, and filter out cells that have grown to their intended
# rowspan.
proc sortGrowing(pctx: var TableContext) =
var i = 0
for j, cellw in pctx.growing:
if pctx.growing[i].grown == 0:
continue
if j != i:
pctx.growing[i] = cellw
inc i
pctx.growing.setLen(i)
pctx.growing.sort(proc(a, b: CellWrapper): int = cmp(a.coli, b.coli))
# Grow cells with a rowspan > 1 (to occupy their place in a new row).
proc growRowspan(pctx: var TableContext; ctx: var RowContext;
growi, i, n: var int; growlen: int) =
while growi < growlen:
let cellw = pctx.growing[growi]
if cellw.coli > n:
break
dec cellw.grown
let rowspanFiller = CellWrapper(
colspan: cellw.colspan,
rowspan: cellw.rowspan,
coli: n,
real: cellw,
last: cellw.grown == 0
)
ctx.cells.add(nil)
ctx.cells[i] = rowspanFiller
for i in n ..< n + cellw.colspan:
ctx.width += pctx.cols[i].width
ctx.width += pctx.inlineSpacing * 2
n += cellw.colspan
inc i
inc growi
proc preBuildTableRow(pctx: var TableContext; row, parent: BlockBox;
rowi, numrows: int): RowContext =
var ctx = RowContext(box: row, cells: newSeq[CellWrapper](row.nested.len))
var n = 0
var i = 0
var growi = 0
# this increases in the loop, but we only want to check growing cells that
# were added by previous rows.
let growlen = pctx.growing.len
for box in row.nested:
assert box.computed{"display"} == DisplayTableCell
pctx.growRowspan(ctx, growi, i, n, growlen)
let colspan = box.computed{"-cha-colspan"}
let rowspan = min(box.computed{"-cha-rowspan"}, numrows - rowi)
let cw = box.computed{"width"}
let ch = box.computed{"height"}
let space = availableSpace(
w = cw.stretchOrMaxContent(pctx.lctx, pctx.space.w),
h = ch.stretchOrMaxContent(pctx.lctx, pctx.space.h)
)
#TODO specified table height should be distributed among rows.
# Allow the table cell to use its specified width.
pctx.lctx.layoutTableCell(box, space)
let wrapper = CellWrapper(
box: box,
colspan: colspan,
rowspan: rowspan,
coli: n
)
ctx.cells[i] = wrapper
if rowspan > 1:
pctx.growing.add(wrapper)
wrapper.grown = rowspan - 1
if pctx.cols.len < n + colspan:
pctx.cols.setLen(n + colspan)
if ctx.reflow.len < n + colspan:
ctx.reflow.setLen(n + colspan)
let minw = box.state.xminwidth div colspan
let w = box.state.size.w div colspan
for i in n ..< n + colspan:
# Add spacing.
ctx.width += pctx.inlineSpacing
# Figure out this cell's effect on the column's width.
# Four cases exits:
# 1. colwidth already fixed, cell width is fixed: take maximum
# 2. colwidth already fixed, cell width is auto: take colwidth
# 3. colwidth is not fixed, cell width is fixed: take cell width
# 4. neither of colwidth or cell width are fixed: take maximum
if ctx.reflow.len <= i: ctx.reflow.setLen(i + 1)
if pctx.cols[i].wspecified:
if space.w.isDefinite():
# A specified column already exists; we take the larger width.
if w > pctx.cols[i].width:
pctx.cols[i].width = w
ctx.reflow[i] = true
if pctx.cols[i].width != w:
wrapper.reflow = true
else:
if space.w.isDefinite():
# This is the first specified column. Replace colwidth with whatever
# we have.
ctx.reflow[i] = true
pctx.cols[i].wspecified = true
pctx.cols[i].width = w
else:
if w > pctx.cols[i].width:
pctx.cols[i].width = w
ctx.reflow[i] = true
else:
wrapper.reflow = true
if pctx.cols[i].minwidth < minw:
pctx.cols[i].minwidth = minw
if pctx.cols[i].width < minw:
pctx.cols[i].width = minw
ctx.reflow[i] = true
ctx.width += pctx.cols[i].width
# Add spacing to the right side.
ctx.width += pctx.inlineSpacing
n += colspan
inc i
pctx.growRowspan(ctx, growi, i, n, growlen)
pctx.sortGrowing()
when defined(debug):
for cell in ctx.cells:
assert cell != nil
ctx.ncols = n
return ctx
proc alignTableCell(cell: BlockBox; availableHeight, baseline: LayoutUnit) =
case cell.computed{"vertical-align"}.keyword
of VerticalAlignTop:
cell.state.offset.y = 0
of VerticalAlignMiddle:
cell.state.offset.y = availableHeight div 2 - cell.state.size.h div 2
of VerticalAlignBottom:
cell.state.offset.y = availableHeight - cell.state.size.h
else:
cell.state.offset.y = baseline - cell.state.firstBaseline
proc layoutTableRow(tctx: TableContext; ctx: RowContext;
parent, row: BlockBox) =
row.state = BlockBoxLayoutState()
var x: LayoutUnit = 0
var n = 0
var baseline: LayoutUnit = 0
# real cellwrappers of fillers
var toAlign: seq[CellWrapper] = @[]
# cells with rowspan > 1 that must store baseline
var toBaseline: seq[CellWrapper] = @[]
# cells that we must update row height of
var toHeight: seq[CellWrapper] = @[]
for cellw in ctx.cells:
var w: LayoutUnit = 0
for i in n ..< n + cellw.colspan:
w += tctx.cols[i].width
# Add inline spacing for merged columns.
w += tctx.inlineSpacing * (cellw.colspan - 1) * 2
if cellw.reflow and cellw.box != nil:
# Do not allow the table cell to make use of its specified width.
# e.g. in the following table
# <TABLE>
# <TR>
# <TD style="width: 5ch" bgcolor=blue>5ch</TD>
# </TR>
# <TR>
# <TD style="width: 9ch" bgcolor=red>9ch</TD>
# </TR>
# </TABLE>
# the TD with a width of 5ch should be 9ch wide as well.
let space = availableSpace(w = stretch(w), h = maxContent())
tctx.lctx.layoutTableCell(cellw.box, space)
w = max(w, cellw.box.state.size.w)
let cell = cellw.box
x += tctx.inlineSpacing
if cell != nil:
cell.state.offset.x += x
x += tctx.inlineSpacing
x += w
n += cellw.colspan
const HasNoBaseline = {
VerticalAlignTop, VerticalAlignMiddle, VerticalAlignBottom
}
if cell != nil:
if cell.computed{"vertical-align"}.keyword notin HasNoBaseline: # baseline
baseline = max(cell.state.firstBaseline, baseline)
if cellw.rowspan > 1:
toBaseline.add(cellw)
if cellw.rowspan > 1:
toHeight.add(cellw)
row.state.size.h = max(row.state.size.h,
cell.state.size.h div cellw.rowspan)
else:
row.state.size.h = max(row.state.size.h,
cellw.real.box.state.size.h div cellw.rowspan)
toHeight.add(cellw.real)
if cellw.last:
toAlign.add(cellw.real)
for cellw in toHeight:
cellw.height += row.state.size.h
for cellw in toBaseline:
cellw.baseline = baseline
for cellw in toAlign:
alignTableCell(cellw.box, cellw.height, cellw.baseline)
for cell in row.nested:
alignTableCell(cell, row.state.size.h, baseline)
# cell position is final here; apply overflow dimensions
row.applyOverflowDimensions(cell)
row.state.size.w = x
proc preLayoutTableRows(tctx: var TableContext; rows: seq[BlockBox];
table: BlockBox) =
for i, row in rows:
let rctx = tctx.preBuildTableRow(row, table, i, rows.len)
tctx.rows.add(rctx)
tctx.maxwidth = max(rctx.width, tctx.maxwidth)
proc preLayoutTableRows(tctx: var TableContext; table: BlockBox) =
# Use separate seqs for different row groups, so that e.g. this HTML:
# echo '<TABLE><TBODY><TR><TD>world<THEAD><TR><TD>hello'|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.nested:
assert child.computed{"display"} in ProperTableChild
case child.computed{"display"}
of DisplayTableRow: tbody.add(child)
of DisplayTableHeaderGroup: thead.add(child.nested)
of DisplayTableRowGroup: tbody.add(child.nested)
of DisplayTableFooterGroup: tfoot.add(child.nested)
else: assert false
tctx.preLayoutTableRows(thead, table)
tctx.preLayoutTableRows(tbody, table)
tctx.preLayoutTableRows(tfoot, table)
func calcSpecifiedRatio(tctx: TableContext; W: LayoutUnit): LayoutUnit =
var totalSpecified: LayoutUnit = 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 / totalSpecified
proc calcUnspecifiedColIndices(tctx: var TableContext; W: var LayoutUnit;
weight: var float64): seq[int] =
let specifiedRatio = tctx.calcSpecifiedRatio(W)
# Spacing for each column:
var avail = newSeqUninitialized[int](tctx.cols.len)
var j = 0
for i, col in tctx.cols.mpairs:
if not col.wspecified:
avail[j] = i
let w = if col.width < W:
toFloat64(col.width)
else:
toFloat64(W) * (ln(toFloat64(col.width) / toFloat64(W)) + 1)
col.weight = w
weight += w
inc j
else:
if specifiedRatio != 1:
col.width *= specifiedRatio
col.reflow = true
W -= col.width
avail.del(j)
return avail
func needsRedistribution(tctx: TableContext; computed: CSSComputedValues):
bool =
case tctx.space.w.t
of scMinContent, scMaxContent:
return false
of scStretch:
return tctx.space.w.u != tctx.maxwidth
of scFitContent:
let u = tctx.space.w.u
return u > tctx.maxwidth and not computed{"width"}.auto or u < tctx.maxwidth
proc redistributeWidth(tctx: var TableContext) =
# Remove inline spacing from distributable width.
var W = tctx.space.w.u - tctx.cols.len * tctx.inlineSpacing * 2
var weight = 0f64
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 = toFloat64(W) / weight
weight = 0
for i in countdown(avail.high, 0):
let j = avail[i]
let x = (unit * tctx.cols[j].weight).toLayoutUnit()
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: LayoutUnit = 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()
# row size does not change from here on.
row.state.overflow.finalize(row.state.size)
y += tctx.blockSpacing
y += row.state.size.h
table.state.size.w = max(row.state.size.w, table.state.size.w)
table.state.size.h = applySizeConstraint(y, sizes.space.h)
proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
let space = availableSpace(w = stretch(parent.state.size.w), h = maxContent())
var marginBottomOut: LayoutUnit
tctx.lctx.layoutRootBlock(box, space, offset(x = 0, y = 0), marginBottomOut)
box.state.offset.x += box.state.margin.left
box.state.offset.y += box.state.margin.top
let outerHeight = box.outerSize(dtVertical) + marginBottomOut
let outerWidth = box.outerSize(dtHorizontal)
let table = parent.nested[0]
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.h += outerHeight
parent.state.size.w = max(parent.state.size.w, outerWidth)
# Table layout. We try to emulate w3m's behavior here:
# 1. Calculate minimum and preferred width of each column
# 2. If column width is not auto, set width to max(min_col_width, specified)
# 3. Calculate the maximum preferred row width. If this is
# a) less than the specified table width, or
# b) greater than the table's content width:
# Distribute the table's content width among cells with an unspecified
# width. If this would give any cell a width < min_width, set that
# cell's width to min_width, then re-do the distribution.
proc layoutTable(tctx: var TableContext; table: BlockBox;
sizes: ResolvedSizes) =
let lctx = tctx.lctx
if table.computed{"border-collapse"} != BorderCollapseCollapse:
tctx.inlineSpacing = table.computed{"border-spacing"}.a.px(lctx)
tctx.blockSpacing = table.computed{"border-spacing"}.b.px(lctx)
tctx.preLayoutTableRows(table) # first pass
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
# As per standard, we must put the caption outside the actual table, inside a
# block-level wrapper box.
proc layoutTableWrapper(bctx: BlockContext; box: BlockBox;
sizes: ResolvedSizes) =
let table = box.nested[0]
table.state = BlockBoxLayoutState()
var tctx = TableContext(lctx: bctx.lctx, space: sizes.space)
tctx.layoutTable(table, sizes)
box.state.size = table.state.size
if box.nested.len > 1:
# do it here, so that caption's box can stretch to our width
let caption = box.nested[1]
#TODO also count caption width in table width
tctx.layoutCaption(box, caption)
proc postAlignChild(box, child: BlockBox; width: LayoutUnit) =
case box.computed{"text-align"}
of TextAlignChaCenter:
child.state.offset.x += max(width div 2 - child.state.size.w div 2, 0)
of TextAlignChaRight:
child.state.offset.x += max(width - child.state.size.w -
child.state.margin.right, 0)
else: # TextAlignChaLeft or not block-aligned
discard
proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
case box.computed{"display"}
of DisplayBlock, DisplayFlowRoot, DisplayTableCaption:
bctx.layoutFlow(box, sizes)
of DisplayListItem:
bctx.layoutListItem(box, sizes)
of DisplayTableWrapper:
bctx.layoutTableWrapper(box, sizes)
of DisplayFlex:
bctx.layoutFlex(box, sizes)
else:
assert false
proc layoutFlexChild(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.
box.state = BlockBoxLayoutState(
offset: offset(x = sizes.margin.left, y = 0),
margin: sizes.margin,
positioned: sizes.positioned
)
bctx.layout(box, sizes)
assert bctx.unpositionedFloats.len == 0
# If the highest float edge is higher than the box itself, set that as
# the box height.
if bctx.maxFloatHeight > box.state.offset.y + box.state.size.h:
box.state.size.h = bctx.maxFloatHeight - box.state.offset.y
type
FlexWeightType = enum
fwtGrow, fwtShrink
FlexPendingItem = object
child: BlockBox
weights: array[FlexWeightType, float64]
sizes: ResolvedSizes
FlexContext = object
mains: seq[FlexMainContext]
offset: Offset
lctx: LayoutContext
totalMaxSize: Size
box: BlockBox
FlexMainContext = object
totalSize: Size
maxSize: Size
maxMargin: RelativeRect
totalWeight: array[FlexWeightType, float64]
pending: seq[FlexPendingItem]
const FlexRow = {FlexDirectionRow, FlexDirectionRowReverse}
proc updateMaxSizes(mctx: var FlexMainContext; child: BlockBox) =
for dim in DimensionType:
mctx.maxSize[dim] = max(mctx.maxSize[dim], child.state.size[dim])
mctx.maxMargin[dim].start = max(mctx.maxMargin[dim].start,
child.state.margin[dim].start)
mctx.maxMargin[dim].send = max(mctx.maxMargin[dim].send,
child.state.margin[dim].send)
proc redistributeMainSize(mctx: var FlexMainContext; sizes: ResolvedSizes;
dim: DimensionType; lctx: LayoutContext) =
let odim = dim.opposite
if sizes.space[dim].isDefinite:
var diff = sizes.space[dim].u - mctx.totalSize[dim]
let wt = if diff > 0: fwtGrow else: fwtShrink
var totalWeight = mctx.totalWeight[wt]
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
let unit = diff.toFloat64() / totalWeight
# reset total weight & available diff for the next iteration (if there is
# one)
totalWeight = 0
diff = 0
for it in mctx.pending.mitems:
if it.weights[wt] == 0:
mctx.updateMaxSizes(it.child)
continue
var u = it.child.state.size[dim] +
(unit * it.weights[wt]).toLayoutUnit()
# check for min/max violation
var minu = it.sizes.minMaxSizes[dim].start
if dim == dtHorizontal:
minu = max(it.child.state.xminwidth, minu)
if minu > u:
# min violation
if wt == fwtShrink: # freeze
diff += u - minu
it.weights[wt] = 0
u = minu
let maxu = it.sizes.minMaxSizes[dim].send
if maxu < u:
# max violation
if wt == fwtGrow: # freeze
diff += u - maxu
it.weights[wt] = 0
u = maxu
it.sizes.space[dim] = stretch(u - it.sizes.padding[dim].sum())
totalWeight += it.weights[wt]
#TODO we should call this only on freeze, and then put another loop to
# the end for non-freezed items
lctx.layoutFlexChild(it.child, it.sizes)
mctx.updateMaxSizes(it.child)
proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext;
sizes: ResolvedSizes; dim: DimensionType) =
let odim = dim.opposite
let lctx = fctx.lctx
mctx.redistributeMainSize(sizes, dim, lctx)
let h = mctx.maxSize[odim] + mctx.maxMargin[odim].sum()
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())
lctx.layoutFlexChild(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.child.state.margin[odim].start
fctx.box.applyOverflowDimensions(it.child)
offset[dim] += it.child.state.size[dim]
fctx.totalMaxSize[dim] = max(fctx.totalMaxSize[dim], offset[dim])
fctx.mains.add(mctx)
mctx = FlexMainContext()
fctx.offset[odim] += h
proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
assert box.inline == nil
let lctx = bctx.lctx
var i = 0
var fctx = FlexContext(lctx: lctx, box: box)
var mctx = FlexMainContext()
let flexDir = box.computed{"flex-direction"}
let canWrap = box.computed{"flex-wrap"} != FlexWrapNowrap
let dim = if flexDir in FlexRow: dtHorizontal else: dtVertical
while i < box.nested.len:
let child = box.nested[i]
var childSizes = lctx.resolveFloatSizes(sizes.space, preserveHeight = true,
child.computed)
let flexBasis = child.computed{"flex-basis"}
if not flexBasis.auto:
childSizes.space[dim] = stretch(flexBasis.px(lctx, sizes.space[dim]))
lctx.layoutFlexChild(child, childSizes)
if not flexBasis.auto and childSizes.space.w.isDefinite and
child.state.xminwidth > childSizes.space.w.u:
# first pass gave us a box that is smaller than the minimum acceptable
# width whatever reason; this may have happened because the initial flex
# basis was e.g. 0. Try to resize it to something more usable.
# Note: this is a hack; we need it because we cheat with size resolution
# by using the algorithm that was in fact designed for floats, and without
# this hack layouts with a flex-base of 0 break down horribly.
# (And we need flex-base because using auto wherever the two-value `flex'
# shorthand is used breaks down even more horribly.)
#TODO implement the standard size resolution properly
childSizes.space.w = stretch(child.state.xminwidth)
lctx.layoutFlexChild(child, childSizes)
if 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, dim)
mctx.totalSize[dim] += child.outerSize(dim)
mctx.updateMaxSizes(child)
let grow = child.computed{"flex-grow"}
let shrink = child.computed{"flex-shrink"}
mctx.totalWeight[fwtGrow] += grow
mctx.totalWeight[fwtShrink] += shrink
mctx.pending.add(FlexPendingItem(
child: child,
weights: [grow, shrink],
sizes: childSizes
))
inc i # need to increment index here for needsGrow
if mctx.pending.len > 0:
fctx.flushMain(mctx, sizes, dim)
box.applySize(sizes, fctx.totalMaxSize[dim], sizes.space, dim)
box.applySize(sizes, fctx.offset[dim.opposite], sizes.space, dim.opposite)
box.state.overflow.finalize(box.state.size)
# Build an outer block box inside an existing block formatting context.
proc layoutBlockChild(bctx: var BlockContext; box: BlockBox;
space: AvailableSpace; offset: Offset; appendMargins: bool) =
let sizes = bctx.lctx.resolveSizes(space, box.computed)
if appendMargins:
# for nested blocks that do not establish their own BFC, and thus take part
# in margin collapsing.
bctx.marginTodo.append(sizes.margin.top)
box.state = BlockBoxLayoutState(
offset: offset(x = offset.x + sizes.margin.left, y = offset.y),
margin: sizes.margin,
positioned: sizes.positioned
)
bctx.layout(box, sizes)
if appendMargins:
bctx.marginTodo.append(sizes.margin.bottom)
# Inner layout for boxes that establish a new block formatting context.
proc layoutRootBlock(lctx: LayoutContext; box: BlockBox;
space: AvailableSpace; offset: Offset; marginBottomOut: var LayoutUnit) =
var bctx = BlockContext(lctx: lctx)
bctx.layoutBlockChild(box, space, offset, appendMargins = false)
assert bctx.unpositionedFloats.len == 0
marginBottomOut = bctx.marginTodo.sum()
# If the highest float edge is higher than the box itself, set that as
# the box height.
if bctx.maxFloatHeight > box.state.size.h + marginBottomOut:
box.state.size.h = bctx.maxFloatHeight - marginBottomOut
proc initBlockPositionStates(state: var BlockState; bctx: var BlockContext;
box: BlockBox) =
let prevBps = bctx.ancestorsHead
bctx.ancestorsHead = BlockPositionState(
box: box,
offset: state.offset,
resolved: bctx.parentBps == nil
)
if prevBps != nil:
prevBps.next = bctx.ancestorsHead
if bctx.parentBps != nil:
bctx.ancestorsHead.offset += bctx.parentBps.offset
# If parentBps is not nil, then our starting position is not in a new
# BFC -> we must add it to our BFC offset.
bctx.ancestorsHead.offset += box.state.offset
if bctx.marginTarget == nil:
bctx.marginTarget = bctx.ancestorsHead
state.initialMarginTarget = bctx.marginTarget
state.initialTargetOffset = bctx.marginTarget.offset
if bctx.parentBps == nil:
# We have just established a new BFC. Resolve the margins instantly.
bctx.marginTarget = nil
state.prevParentBps = bctx.parentBps
bctx.parentBps = bctx.ancestorsHead
state.initialParentOffset = bctx.parentBps.offset
func isParentResolved(state: BlockState; bctx: BlockContext): bool =
return bctx.marginTarget != state.initialMarginTarget or
state.prevParentBps != nil and state.prevParentBps.resolved
# Note: this does not include display types that cannot appear as block
# children.
func establishesBFC(computed: CSSComputedValues): bool =
return computed{"float"} != FloatNone or
computed{"position"} == PositionAbsolute or
computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayTableWrapper,
DisplayFlex} or
computed{"overflow"} notin {OverflowVisible, OverflowClip}
#TODO contain, grid, multicol, column-span
# Outer layout for block-level children that establish a BFC.
# Returns the vertical size used (incl. margins).
proc layoutBlockChildBFC(state: var BlockState; bctx: var BlockContext;
child: BlockBox): LayoutUnit =
var marginBottomOut: LayoutUnit
bctx.lctx.layoutRootBlock(child, state.space, state.offset,
marginBottomOut)
# Do not collapse margins of elements that do not participate in
# the flow.
if child.computed{"position"} != PositionAbsolute and
child.computed{"float"} == FloatNone:
bctx.marginTodo.append(child.state.margin.top)
bctx.flushMargins(child)
bctx.positionFloats()
bctx.marginTodo.append(child.state.margin.bottom)
if child.computed{"clear"} != ClearNone:
state.offset.clearFloats(bctx, child.computed{"clear"})
if bctx.exclusions.len > 0:
# Consulting the standard for an important edge case... (abridged)
#
# > The border box of an element that establishes a new BFC must not
# > overlap the margin box of any floats in the same BFC as the
# > element itself. If necessary, implementations should clear the
# > said element, but may place it adjacent to such floats if there
# > is sufficient space. CSS2 does not define when a UA may put said
# > element next to the float.
#
# ...as expected. Thanks for nothing.
#
# OK here's what we do:
# * run a normal pass
# * place the longest word (i.e. xminwidth) somewhere
# * run another pass with the placement we got
#
# I suspect this breaks horribly on some layouts, but I don't care
# enough to make this convoluted garbage even more complex.
#
# Note that we do this only for elements in the flow. FF yanks
# absolutely positioned elements on top of floats, and so do we.
let pbfcOffset = bctx.bfcOffset
let bfcOffset = offset(
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)
var outw: LayoutUnit
let offset = bctx.findNextBlockOffset(bfcOffset, minSize,
state.space, outw)
let space = availableSpace(w = stretch(outw), h = state.space.h)
bctx.lctx.layoutRootBlock(child, space, offset - pbfcOffset,
marginBottomOut)
else:
child.state.offset.y += child.state.margin.top
if state.isParentResolved(bctx):
# If parent offset has been resolved, use marginTodo in this
# float's initial offset.
child.state.offset.y += bctx.marginTodo.sum()
# delta y is difference between old and new offsets (margin-top), sum
# of margin todo in bctx2 (margin-bottom) + height.
return child.state.offset.y - state.offset.y + child.state.size.h +
marginBottomOut
# Layout and place all children in the block box.
# Box placement must occur during this pass, since child box layout in the
# same block formatting context depends on knowing where the box offset is
# (because of floats).
proc layoutBlockChildren(state: var BlockState; bctx: var BlockContext;
parent: BlockBox) =
for child in parent.nested:
var dy: LayoutUnit = 0 # delta
if child.computed.establishesBFC():
dy = state.layoutBlockChildBFC(bctx, child)
else:
bctx.layoutBlockChild(child, state.space, state.offset,
appendMargins = true)
# delta y is difference between old and new offsets (margin-top),
# plus height.
dy = child.state.offset.y - state.offset.y + child.state.size.h
let childWidth = child.outerSize(dtHorizontal)
state.xminwidth = max(state.xminwidth, child.state.xminwidth)
let isfloat = child.computed{"float"} != FloatNone
if child.computed{"position"} != PositionAbsolute and not isfloat:
# Not absolute, and not a float.
state.maxChildWidth = max(state.maxChildWidth, childWidth)
state.offset.y += dy
elif isfloat:
if state.space.w.t == scFitContent:
# Float position depends on the available width, but in this case
# the parent width is not known.
#
# Set the "re-layout" flag, and skip this box.
# (If child boxes with fit-content have floats, those will be
# re-layouted too first, so we do not have to consider those here.)
state.needsReLayout = true
# Since we emulate max-content here, the float will not contribute to
# maxChildWidth in this iteration; instead, its outer width will be
# summed up in totalFloatWidth and added to maxChildWidth in
# initReLayout.
state.totalFloatWidth += childWidth
continue
state.maxChildWidth = max(state.maxChildWidth, childWidth)
# Two cases exist:
# a) The float cannot be positioned, because `box' has not resolved
# its y offset yet. (e.g. if float comes before the first child,
# we do not know yet if said child will move our y offset with a
# margin-top value larger than ours.)
# In this case we put it in unpositionedFloats, and defer positioning
# until our y offset is resolved.
# b) `box' has resolved its y offset, so the float can already
# be positioned.
# We check whether our y offset has been positioned as follows:
# * save marginTarget in BlockState at layoutBlock's start
# * if our saved marginTarget and bctx's marginTarget no longer point
# to the same object, that means our (or an ancestor's) offset has
# been resolved, i.e. we can position floats already.
if bctx.marginTarget != state.initialMarginTarget:
# y offset resolved
bctx.positionFloat(child, state.space, bctx.parentBps.offset)
else:
bctx.unpositionedFloats.add(UnpositionedFloat(
space: state.space,
parentBps: bctx.parentBps,
box: child,
parentBox: parent
))
# Unlucky path, where we have floating blocks and a fit-content width.
# Reset marginTodo & the starting offset, and stretch the box to the
# max child width.
proc initReLayout(state: var BlockState; bctx: var BlockContext;
box: BlockBox; sizes: ResolvedSizes) =
bctx.marginTodo = state.oldMarginTodo
# Note: we do not reset our own BlockPositionState's offset; we assume it
# has already been resolved in the previous pass.
# (If not, it won't be resolved in this pass either, so the following code
# does not really change anything.)
bctx.parentBps.next = nil
if state.initialMarginTarget != bctx.marginTarget:
# Reset the initial margin target to its previous state, and then set
# it as the marginTarget again.
# Two solutions exist:
# a) Store the initial margin target offset, then restore it here. Seems
# clean, but it would require a linked list traversal to update all
# child margin positions.
# b) Re-use the previous margin target offsets; they are guaranteed
# to remain the same, because out-of-flow elements (like floats) do not
# participate in margin resolution. We do this by setting the margin
# target to a dummy object, which is a small price to pay compared
# to solution a).
bctx.marginTarget = BlockPositionState(
# Use initialTargetOffset to emulate the BFC positioning of the
# previous pass.
offset: state.initialTargetOffset
)
# Also set ancestorsHead as the dummy object, so next elements are
# chained to that.
bctx.ancestorsHead = bctx.marginTarget
bctx.exclusions.setLen(state.oldExclusionsLen)
state.offset = offset(x = sizes.padding.left, y = sizes.padding.top)
box.applyWidth(sizes, state.maxChildWidth + state.totalFloatWidth)
# Positioning of the children will differ now; reset the overflow offsets.
for dim in DimensionType:
box.state.overflow[dim] = Span()
state.space.w = stretch(box.state.size.w)
# Re-position the children.
# The x offset with a fit-content width depends on the parent box's width,
# so we cannot do this in the first pass.
proc repositionChildren(state: BlockState; box: BlockBox; lctx: LayoutContext) =
for child in box.nested:
if child.computed{"position"} != PositionAbsolute:
box.postAlignChild(child, box.state.size.w)
case child.computed{"position"}
of PositionRelative:
box.positionRelative(child)
of PositionAbsolute:
lctx.positionAbsolute(child, child.state.margin)
else: discard #TODO
# Set overflow here, after the child has been positioned.
box.applyOverflowDimensions(child)
proc layoutBlock(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) =
let lctx = bctx.lctx
let positioned = box.computed{"position"} notin {
PositionStatic, PositionFixed, PositionSticky
}
if positioned:
lctx.positioned.add(sizes.space)
var state = BlockState(
offset: offset(x = sizes.padding.left, y = sizes.padding.top),
space: sizes.space,
oldMarginTodo: bctx.marginTodo,
oldExclusionsLen: bctx.exclusions.len
)
state.initBlockPositionStates(bctx, box)
state.layoutBlockChildren(bctx, box)
if state.needsReLayout:
state.initReLayout(bctx, box, sizes)
state.layoutBlockChildren(bctx, box)
if box.nested.len > 0:
let lastNested = box.nested[^1]
box.state.baseline = lastNested.state.offset.y + lastNested.state.baseline
# Apply width, then move the inline offset of children that still need
# further relative positioning.
box.applyWidth(sizes, state.maxChildWidth, state.space)
state.repositionChildren(box, lctx)
# Set the inner height to the last y offset minus the starting offset
# (that is, top padding).
let innerHeight = state.offset.y - sizes.padding.top
box.applyHeight(sizes, innerHeight)
# Add padding; we cannot do this further up without influencing positioning.
box.applyPadding(sizes.padding)
# Pass down relevant data from state.
box.state.xminwidth = state.xminwidth
if state.isParentResolved(bctx):
# Our offset has already been resolved, ergo any margins in marginTodo will
# be passed onto the next box. Set marginTarget to nil, so that if we (or
# one of our ancestors) were still set as a marginTarget, we no longer are.
bctx.positionFloats()
bctx.marginTarget = nil
# All children are positioned now; finalize our overflow dimensions.
box.state.overflow.finalize(box.state.size)
# Reset parentBps to the previous node.
bctx.parentBps = state.prevParentBps
if positioned:
lctx.positioned.setLen(lctx.positioned.len - 1)
# 1st pass: build tree
proc newMarkerBox(computed: CSSComputedValues; listItemCounter: int):
InlineFragment =
let computed = computed.inheritProperties()
computed{"display"} = DisplayInline
# Use pre, so the space at the end of the default markers isn't ignored.
computed{"white-space"} = WhitespacePre
return InlineFragment(
t: iftText,
computed: computed,
text: computed{"list-style-type"}.listMarker(listItemCounter)
)
type InnerBlockContext = object
styledNode: StyledNode
outer: BlockBox
lctx: LayoutContext
anonRow: BlockBox
anonTableWrapper: BlockBox
quoteLevel: int
listItemCounter: int
listItemReset: bool
parent: ptr InnerBlockContext
inlineStack: seq[StyledNode]
inlineStackFragments: seq[InlineFragment]
# if inline is not nil, then inline.children.len > 0
inline: RootInlineFragment
proc flushInlineGroup(ctx: var InnerBlockContext) =
if ctx.inline != nil:
let computed = ctx.outer.computed.inheritProperties()
computed{"display"} = DisplayBlock
let box = BlockBox(computed: computed, inline: ctx.inline)
ctx.outer.nested.add(box)
ctx.inline = nil
# Don't build empty anonymous inline blocks between block boxes
func canBuildAnonymousInline(ctx: InnerBlockContext;
computed: CSSComputedValues; str: string): bool =
return ctx.inline != nil and ctx.inline.fragment.children.len > 0 or
computed.whitespacepre or not str.onlyWhitespace()
proc buildBlock(ctx: var InnerBlockContext)
proc buildTable(ctx: var InnerBlockContext)
proc buildFlex(ctx: var InnerBlockContext)
proc buildInlineBoxes(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues)
proc buildTableRowGroup(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox
proc buildTableRow(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox
proc buildTableCell(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox
proc buildTableCaption(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox
proc newInnerBlockContext(styledNode: StyledNode; box: BlockBox;
lctx: LayoutContext; parent: ptr InnerBlockContext): InnerBlockContext
func toTableWrapper(display: CSSDisplay): CSSDisplay =
if display == DisplayTable:
return DisplayTableWrapper
assert display == DisplayInlineTable
return DisplayInlineTableWrapper
proc createAnonTable(ctx: var InnerBlockContext; computed: CSSComputedValues) =
if ctx.anonTableWrapper == nil:
let inherited = computed.inheritProperties()
let (outerComputed, innerComputed) = inherited.splitTable()
#TODO this should be DisplayInlineTableWrapper inside inline contexts
outerComputed{"display"} = DisplayTableWrapper
let innerTable = BlockBox(computed: innerComputed)
ctx.anonTableWrapper = BlockBox(
computed: outerComputed,
nested: @[innerTable]
)
proc flushTableRow(ctx: var InnerBlockContext) =
if ctx.anonRow != nil:
if ctx.outer.computed{"display"} in ProperTableRowParent:
ctx.outer.nested.add(ctx.anonRow)
else:
ctx.createAnonTable(ctx.outer.computed)
ctx.anonTableWrapper.nested[0].nested.add(ctx.anonRow)
ctx.anonRow = nil
proc flushTable(ctx: var InnerBlockContext) =
ctx.flushTableRow()
if ctx.anonTableWrapper != nil:
ctx.outer.nested.add(ctx.anonTableWrapper)
proc iflush(ctx: var InnerBlockContext) =
ctx.inlineStackFragments.setLen(0)
proc flushInherit(ctx: var InnerBlockContext) =
if ctx.parent != nil:
if not ctx.listItemReset:
ctx.parent.listItemCounter = ctx.listItemCounter
ctx.parent.quoteLevel = ctx.quoteLevel
proc flush(ctx: var InnerBlockContext) =
ctx.flushInlineGroup()
ctx.flushTableRow()
ctx.flushTable()
ctx.flushInherit()
proc addInlineRoot(ctx: var InnerBlockContext; box: InlineFragment) =
if ctx.inline == nil:
let fragment = InlineFragment(
t: iftParent,
computed: ctx.lctx.myRootProperties,
children: @[box]
)
ctx.inline = RootInlineFragment(fragment: fragment)
else:
ctx.inline.fragment.children.add(box)
proc reconstructInlineParents(ctx: var InnerBlockContext) =
if ctx.inlineStackFragments.len == 0:
var parent = InlineFragment(
t: iftParent,
computed: ctx.inlineStack[0].computed,
node: ctx.inlineStack[0]
)
ctx.inlineStackFragments.add(parent)
ctx.addInlineRoot(parent)
for i in 1 ..< ctx.inlineStack.len:
let node = ctx.inlineStack[i]
let child = InlineFragment(
t: iftParent,
computed: node.computed,
node: node
)
parent.children.add(child)
ctx.inlineStackFragments.add(child)
parent = child
proc buildSomeBlock(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox =
let box = BlockBox(computed: computed, node: styledNode)
var childCtx = newInnerBlockContext(styledNode, box, ctx.lctx, addr ctx)
case computed{"display"}
of DisplayBlock, DisplayFlowRoot, DisplayInlineBlock: childCtx.buildBlock()
of DisplayFlex, DisplayInlineFlex: childCtx.buildFlex()
of DisplayTable, DisplayInlineTable: childCtx.buildTable()
else: discard
return box
# Note: these also pop
proc pushBlock(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flush()
let box = ctx.buildSomeBlock(styledNode, computed)
ctx.outer.nested.add(box)
proc pushInline(ctx: var InnerBlockContext; fragment: InlineFragment) =
if ctx.inlineStack.len == 0:
ctx.addInlineRoot(fragment)
else:
ctx.reconstructInlineParents()
ctx.inlineStackFragments[^1].children.add(fragment)
proc pushInlineText(ctx: var InnerBlockContext; computed: CSSComputedValues;
styledNode: StyledNode; text: string) =
let box = InlineFragment(
t: iftText,
computed: computed,
node: styledNode,
text: text
)
ctx.pushInline(box)
proc pushInlineBlock(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
let wrapper = InlineFragment(
t: iftBox,
computed: computed.inheritProperties(),
node: styledNode,
box: ctx.buildSomeBlock(styledNode, computed)
)
ctx.pushInline(wrapper)
proc pushListItem(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flush()
inc ctx.listItemCounter
let marker = newMarkerBox(computed, ctx.listItemCounter)
let position = computed{"list-style-position"}
let content = BlockBox(computed: computed, node: styledNode)
var contentCtx = newInnerBlockContext(styledNode, content, ctx.lctx, addr ctx)
case position
of ListStylePositionOutside:
contentCtx.buildBlock()
content.computed = content.computed.copyProperties()
content.computed{"display"} = DisplayBlock
let markerComputed = marker.computed.copyProperties()
markerComputed{"display"} = DisplayBlock
let marker = BlockBox(
computed: marker.computed,
inline: RootInlineFragment(fragment: marker)
)
let wrapper = BlockBox(computed: computed, nested: @[marker, content])
ctx.outer.nested.add(wrapper)
of ListStylePositionInside:
contentCtx.pushInline(marker)
contentCtx.buildBlock()
ctx.outer.nested.add(content)
proc pushTableRow(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flushInlineGroup()
ctx.flushTableRow()
let child = ctx.buildTableRow(styledNode, computed)
if ctx.outer.computed{"display"} in ProperTableRowParent:
ctx.outer.nested.add(child)
else:
ctx.createAnonTable(ctx.outer.computed)
ctx.anonTableWrapper.nested[0].nested.add(child)
proc pushTableRowGroup(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flushInlineGroup()
ctx.flushTableRow()
let child = ctx.buildTableRowGroup(styledNode, computed)
if ctx.outer.computed{"display"} in {DisplayTable, DisplayInlineTable}:
ctx.outer.nested.add(child)
else:
ctx.createAnonTable(ctx.outer.computed)
ctx.anonTableWrapper.nested[0].nested.add(child)
proc pushTableCell(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flushInlineGroup()
let child = ctx.buildTableCell(styledNode, computed)
if ctx.outer.computed{"display"} == DisplayTableRow:
ctx.outer.nested.add(child)
else:
if ctx.anonRow == nil:
let wrapperVals = ctx.outer.computed.inheritProperties()
wrapperVals{"display"} = DisplayTableRow
ctx.anonRow = BlockBox(computed: wrapperVals)
ctx.anonRow.nested.add(child)
proc pushTableCaption(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
ctx.iflush()
ctx.flushInlineGroup()
ctx.flushTableRow()
let child = ctx.buildTableCaption(styledNode, computed)
if ctx.outer.computed{"display"} in {DisplayTable, DisplayInlineTable}:
ctx.outer.nested.add(child)
else:
ctx.createAnonTable(ctx.outer.computed)
# only add first caption
if ctx.anonTableWrapper.nested.len == 1:
ctx.anonTableWrapper.nested.add(child)
proc buildFromElem(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
case computed{"display"}
of DisplayBlock, DisplayFlowRoot, DisplayFlex, DisplayTable:
ctx.pushBlock(styledNode, computed)
of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex:
ctx.pushInlineBlock(styledNode, computed)
of DisplayListItem:
ctx.pushListItem(styledNode, computed)
of DisplayInline:
ctx.buildInlineBoxes(styledNode, computed)
of DisplayTableRow:
ctx.pushTableRow(styledNode, computed)
of DisplayTableRowGroup, DisplayTableHeaderGroup, DisplayTableFooterGroup:
ctx.pushTableRowGroup(styledNode, computed)
of DisplayTableCell:
ctx.pushTableCell(styledNode, computed)
of DisplayTableCaption:
ctx.pushTableCaption(styledNode, computed)
of DisplayTableColumn: discard #TODO
of DisplayTableColumnGroup: discard #TODO
of DisplayNone: discard
of DisplayTableWrapper, DisplayInlineTableWrapper: assert false
proc buildReplacement(ctx: var InnerBlockContext; child, parent: StyledNode;
computed: CSSComputedValues) =
case child.content.t
of ContentOpenQuote:
let quotes = parent.computed{"quotes"}
var text: string = ""
if quotes.qs.len > 0:
text = quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].s
elif quotes.auto:
text = quoteStart(ctx.quoteLevel)
else: return
ctx.pushInlineText(computed, parent, text)
inc ctx.quoteLevel
of ContentCloseQuote:
if ctx.quoteLevel > 0: dec ctx.quoteLevel
let quotes = parent.computed{"quotes"}
var text: string = ""
if quotes.qs.len > 0:
text = quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].e
elif quotes.auto:
text = quoteEnd(ctx.quoteLevel)
else: return
ctx.pushInlineText(computed, parent, text)
of ContentNoOpenQuote:
inc ctx.quoteLevel
of ContentNoCloseQuote:
if ctx.quoteLevel > 0: dec ctx.quoteLevel
of ContentString:
ctx.pushInlineText(computed, parent, child.content.s)
of ContentImage:
if child.content.bmp != nil:
let wrapper = InlineFragment(
t: iftBitmap,
computed: computed,
node: parent,
bmp: child.content.bmp
)
ctx.pushInline(wrapper)
else:
ctx.pushInlineText(computed, parent, "[img]")
of ContentVideo:
ctx.pushInlineText(computed, parent, "[video]")
of ContentAudio:
ctx.pushInlineText(computed, parent, "[audio]")
of ContentNewline:
let fragment = InlineFragment(
t: iftNewline,
computed: computed,
node: child
)
ctx.pushInline(fragment)
proc buildInlineBoxes(ctx: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues) =
let parent = InlineFragment(
t: iftParent,
computed: computed,
splitType: {stSplitStart}
)
if ctx.inlineStack.len == 0:
ctx.addInlineRoot(parent)
else:
ctx.reconstructInlineParents()
ctx.inlineStackFragments[^1].children.add(parent)
ctx.inlineStack.add(styledNode)
ctx.inlineStackFragments.add(parent)
for child in styledNode.children:
case child.t
of stElement:
ctx.buildFromElem(child, child.computed)
of stText:
ctx.pushInlineText(computed, styledNode, child.textData)
of stReplacement:
ctx.buildReplacement(child, styledNode, computed)
ctx.reconstructInlineParents()
let fragment = ctx.inlineStackFragments.pop()
fragment.splitType.incl(stSplitEnd)
ctx.inlineStack.setLen(ctx.inlineStack.high)
proc newInnerBlockContext(styledNode: StyledNode; box: BlockBox;
lctx: LayoutContext; parent: ptr InnerBlockContext): InnerBlockContext =
assert box.computed{"display"} != DisplayInline
var ctx = InnerBlockContext(
styledNode: styledNode,
outer: box,
lctx: lctx,
parent: parent
)
if parent != nil:
ctx.listItemCounter = parent[].listItemCounter
ctx.quoteLevel = parent[].quoteLevel
for reset in styledNode.computed{"counter-reset"}:
if reset.name == "list-item":
ctx.listItemCounter = reset.num
ctx.listItemReset = true
return ctx
proc buildInnerBlock(ctx: var InnerBlockContext) =
let inlineComputed = ctx.outer.computed.inheritProperties()
for child in ctx.styledNode.children:
case child.t
of stElement:
ctx.buildFromElem(child, child.computed)
of stText:
let text = child.textData
if ctx.canBuildAnonymousInline(ctx.outer.computed, text):
ctx.pushInlineText(inlineComputed, ctx.styledNode, text)
of stReplacement:
ctx.buildReplacement(child, ctx.styledNode, inlineComputed)
ctx.iflush()
proc buildBlock(ctx: var InnerBlockContext) =
ctx.buildInnerBlock()
# Flush anonymous tables here, to avoid setting inline layout with tables.
ctx.flushTableRow()
ctx.flushTable()
ctx.flushInherit() # (flush here, because why not)
# Avoid unnecessary anonymous block boxes. This also helps set our layout to
# inline even if no inner anonymous block was built.
if ctx.outer.nested.len == 0:
ctx.outer.inline = if ctx.inline != nil:
ctx.inline
else:
RootInlineFragment(fragment: InlineFragment(
t: iftParent,
computed: ctx.lctx.myRootProperties
))
ctx.inline = nil
ctx.flushInlineGroup()
proc buildInnerFlex(ctx: var InnerBlockContext) =
let inlineComputed = ctx.outer.computed.inheritProperties()
for child in ctx.styledNode.children:
case child.t
of stElement:
let display = child.computed{"display"}.blockify()
let computed = if display != child.computed{"display"}:
let computed = child.computed.copyProperties()
computed{"display"} = display
computed
else:
child.computed
ctx.buildFromElem(child, computed)
of stText:
let text = child.textData
if ctx.canBuildAnonymousInline(ctx.outer.computed, text):
ctx.pushInlineText(inlineComputed, ctx.styledNode, text)
of stReplacement:
ctx.buildReplacement(child, ctx.styledNode, inlineComputed)
ctx.iflush()
proc buildFlex(ctx: var InnerBlockContext) =
ctx.buildInnerFlex()
# Flush anonymous tables here, to avoid setting inline layout with tables.
ctx.flushTableRow()
ctx.flushTable()
# (flush here, because why not)
ctx.flushInherit()
ctx.flushInlineGroup()
assert ctx.outer.inline == nil
const FlexReverse = {FlexDirectionRowReverse, FlexDirectionColumnReverse}
if ctx.outer.computed{"flex-direction"} in FlexReverse:
ctx.outer.nested.reverse()
proc buildTableCell(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox =
let box = BlockBox(node: styledNode, computed: computed)
var ctx = newInnerBlockContext(styledNode, box, parent.lctx, addr parent)
ctx.buildInnerBlock()
ctx.flush()
return box
proc buildTableRowChildWrappers(box: BlockBox) =
var wrapperVals: CSSComputedValues = nil
for child in box.nested.mitems:
if child.computed{"display"} != DisplayTableCell:
if wrapperVals == nil:
wrapperVals = box.computed.inheritProperties()
wrapperVals{"display"} = DisplayTableCell
child = BlockBox(computed: wrapperVals, nested: @[child])
proc buildTableRow(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox =
let box = BlockBox(node: styledNode, computed: computed)
var ctx = newInnerBlockContext(styledNode, box, parent.lctx, addr parent)
ctx.buildInnerBlock()
ctx.flush()
box.buildTableRowChildWrappers()
return box
proc buildTableRowGroupChildWrappers(box: BlockBox) =
let wrapperVals = box.computed.inheritProperties()
wrapperVals{"display"} = DisplayTableRow
for child in box.nested.mitems:
if child.computed{"display"} != DisplayTableRow:
let wrapper = BlockBox(computed: wrapperVals, nested: @[child])
wrapper.buildTableRowChildWrappers()
child = wrapper
proc buildTableRowGroup(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox =
let box = BlockBox(node: styledNode, computed: computed)
var ctx = newInnerBlockContext(styledNode, box, parent.lctx, addr parent)
ctx.buildInnerBlock()
ctx.flush()
box.buildTableRowGroupChildWrappers()
return box
proc buildTableCaption(parent: var InnerBlockContext; styledNode: StyledNode;
computed: CSSComputedValues): BlockBox =
let box = BlockBox(node: styledNode, computed: computed)
var ctx = newInnerBlockContext(styledNode, box, parent.lctx, addr parent)
ctx.buildInnerBlock()
ctx.flush()
return box
proc buildTableChildWrappers(box: BlockBox; computed: CSSComputedValues) =
let innerTable = BlockBox(computed: computed, node: box.node)
let wrapperVals = box.computed.inheritProperties()
wrapperVals{"display"} = DisplayTableRow
var caption: BlockBox = nil
for child in box.nested:
if child.computed{"display"} in ProperTableChild:
innerTable.nested.add(child)
elif child.computed{"display"} == DisplayTableCaption:
if caption == nil:
caption = child
else:
let wrapper = BlockBox(computed: wrapperVals, nested: @[child])
wrapper.buildTableRowChildWrappers()
innerTable.nested.add(wrapper)
box.nested = @[innerTable]
if caption != nil:
box.nested.add(caption)
proc buildTable(ctx: var InnerBlockContext) =
ctx.buildInnerBlock()
ctx.flush()
let (outerComputed, innerComputed) = ctx.outer.computed.splitTable()
ctx.outer.computed = outerComputed
outerComputed{"display"} = outerComputed{"display"}.toTableWrapper()
ctx.outer.buildTableChildWrappers(innerComputed)
proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
let space = availableSpace(
w = stretch(attrsp[].width_px),
h = stretch(attrsp[].height_px)
)
let lctx = LayoutContext(
attrsp: attrsp,
positioned: @[space],
myRootProperties: rootProperties()
)
let box = BlockBox(computed: root.computed, node: root)
var ctx = newInnerBlockContext(root, box, lctx, nil)
ctx.buildBlock()
var marginBottomOut: LayoutUnit
lctx.layoutRootBlock(box, space, offset(x = 0, y = 0), marginBottomOut)
return box