import std/algorithm import std/math import std/options import std/unicode import css/stylednode import css/values import layout/box import layout/layoutunit import types/winattrs import utils/luwrap import utils/strwidth import utils/twtstr import utils/widthconv type LayoutState = ref object attrsp: ptr WindowAttributes positioned: seq[AvailableSpace] # 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 STRETCH, FIT_CONTENT, MIN_CONTENT, MAX_CONTENT SizeConstraint = object t: SizeConstraintType u: LayoutUnit AvailableSpace = object w: SizeConstraint h: SizeConstraint ResolvedSizes = object margin: RelativeRect padding: RelativeRect space: AvailableSpace minWidth: LayoutUnit maxWidth: LayoutUnit minHeight: LayoutUnit maxHeight: LayoutUnit template attrs(state: LayoutState): WindowAttributes = state.attrsp[] func maxContent(): SizeConstraint = return SizeConstraint(t: MAX_CONTENT) func stretch(u: LayoutUnit): SizeConstraint = return SizeConstraint(t: STRETCH, u: u) func fitContent(u: LayoutUnit): SizeConstraint = return SizeConstraint(t: FIT_CONTENT, u: u) type BoxBuilder = ref object of RootObj children: seq[BoxBuilder] computed: CSSComputedValues node: StyledNode InlineBoxBuilder = ref object of BoxBuilder text: seq[string] newline: bool splitType: set[SplitType] BlockBoxBuilder = ref object of BoxBuilder inlinelayout: bool MarkerBoxBuilder = ref object of InlineBoxBuilder ListItemBoxBuilder = ref object of BoxBuilder marker: MarkerBoxBuilder content: BlockBoxBuilder TableRowGroupBoxBuilder = ref object of BlockBoxBuilder TableRowBoxBuilder = ref object of BlockBoxBuilder TableCellBoxBuilder = ref object of BlockBoxBuilder TableBoxBuilder = ref object of BlockBoxBuilder rowgroups: seq[TableRowGroupBoxBuilder] TableCaptionBoxBuilder = ref object of BlockBoxBuilder #TODO ? func stretch(sc: SizeConstraint): SizeConstraint = case sc.t of MIN_CONTENT, MAX_CONTENT: return sc of STRETCH, FIT_CONTENT: return SizeConstraint(t: STRETCH, u: sc.u) func fitContent(sc: SizeConstraint): SizeConstraint = case sc.t of MIN_CONTENT, MAX_CONTENT: return SizeConstraint(t: sc.t) of STRETCH, FIT_CONTENT: return SizeConstraint(t: FIT_CONTENT, u: sc.u) func isDefinite(sc: SizeConstraint): bool = return sc.t in {STRETCH, FIT_CONTENT} # Layout (2nd pass) func px(l: CSSLength, lctx: LayoutState, p: LayoutUnit = 0): LayoutUnit {.inline.} = return px(l, lctx.attrs, p) func px(l: CSSLength, lctx: LayoutState, p: Option[LayoutUnit]): Option[LayoutUnit] {.inline.} = if l.unit == UNIT_PERC and p.isNone: return none(LayoutUnit) return some(px(l, lctx.attrs, p.get(0))) func canpx(l: CSSLength, sc: SizeConstraint): bool = return l.unit != UNIT_PERC or sc.isDefinite() func canpx(l: CSSLength, p: Option[LayoutUnit]): bool = return l.unit != UNIT_PERC or p.isSome # Note: for margins only # For percentages, use 0 for indefinite, and containing box's size for # definite. func px(l: CSSLength, lctx: LayoutState, p: SizeConstraint): LayoutUnit = if l.unit == UNIT_PERC: case p.t of MIN_CONTENT, MAX_CONTENT: return 0 of STRETCH, FIT_CONTENT: return l.px(lctx, p.u) return px(l, lctx.attrs, 0) func applySizeConstraint(u: LayoutUnit, availableSize: SizeConstraint): LayoutUnit = case availableSize.t of STRETCH: return availableSize.u of MIN_CONTENT, MAX_CONTENT: # must be calculated elsewhere... return u of FIT_CONTENT: return min(u, availableSize.u) type BlockContext = object lctx: LayoutState 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 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 # offset of line in root fragment offsety: LayoutUnit # minimum height to fit all inline atoms minHeight: LayoutUnit LineBox = ref object atoms: seq[InlineAtom] size: Size InlineAtomState = object vertalign: CSSVerticalAlign baseline: LayoutUnit marginTop: LayoutUnit marginBottom: LayoutUnit InlineContext = object root: RootInlineFragment bctx: ptr BlockContext bfcOffset: Offset currentLine: LineBoxState hasshy: bool lctx: LayoutState lines: seq[LineBox] minwidth: LayoutUnit space: AvailableSpace whitespacenum: int whitespaceIsLF: bool whitespaceFragment: InlineFragment word: InlineAtom wordstate: InlineAtomState wrappos: int # position of last wrapping opportunity, or -1 firstTextFragment: InlineFragment lastTextFragment: InlineFragment InlineState = object computed: CSSComputedValues node: StyledNode fragment: InlineFragment firstLine: bool startOffsetTop: Offset # 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 {WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP} func nowrap(computed: CSSComputedValues): bool = computed{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE} func cellwidth(lctx: LayoutState): int = lctx.attrs.ppc func cellwidth(ictx: InlineContext): int = ictx.lctx.cellwidth func cellheight(lctx: LayoutState): 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 func size(ictx: var InlineContext): var Size = ictx.root.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.computed.whitespacepre: if ictx.currentLine.atoms.len == 0 or ictx.currentLine.atoms[^1].t == INLINE_SPACING: return 0 return ictx.cellwidth * ictx.whitespacenum proc applyLineHeight(ictx: InlineContext, state: var LineBoxState, computed: CSSComputedValues) = let lctx = ictx.lctx #TODO this should be computed during cascading. 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: INLINE_WORD, size: Size(h: ictx.cellheight) ) ictx.wordstate = InlineAtomState( vertalign: state.computed{"vertical-align"}, baseline: ictx.cellheight ) ictx.wrappos = -1 ictx.hasshy = false proc horizontalAlignLine(ictx: var InlineContext, state: InlineState, line: LineBox) = let width = case ictx.space.w.t of MIN_CONTENT, MAX_CONTENT: ictx.size.w of FIT_CONTENT: min(ictx.size.w, ictx.space.w.u) of STRETCH: max(ictx.size.w, ictx.space.w.u) # we don't support directions for now so left = start and right = end case state.computed{"text-align"} of TEXT_ALIGN_START, TEXT_ALIGN_LEFT, TEXT_ALIGN_CHA_LEFT, TEXT_ALIGN_JUSTIFY: discard of TEXT_ALIGN_END, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CHA_RIGHT: # move everything let x = max(width, line.size.w) - line.size.w for atom in line.atoms.mitems: atom.offset.x += x ictx.size.w = max(atom.offset.x + atom.size.w, ictx.size.w) of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER: # NOTE if we need line x offsets, use: #let width = width - line.offset.x let x = max((max(width, line.size.w)) div 2 - line.size.w div 2, 0) for atom in line.atoms.mitems: atom.offset.x += x ictx.size.w = max(atom.offset.x + atom.size.w, ictx.size.w) # If necessary, update ictx's width. ictx.size.w = max(line.size.w, ictx.size.w) # Align atoms (inline boxes, text, etc.) vertically (i.e. along the inline # 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 var baseline = lineheight # Calculate the line's baseline based on atoms' baseline. # Also, collect the maximum vertical margins of inline blocks. var marginTop: LayoutUnit = 0 var bottomEdge = baseline for i, atom in ictx.currentLine.atoms: let atom = ictx.currentLine.atoms[i] let iastate = ictx.currentLine.atomstates[i] case iastate.vertalign.keyword of VERTICAL_ALIGN_BASELINE: let len = iastate.vertalign.length.px(ictx.lctx, lineheight) baseline = max(baseline, iastate.baseline + len) of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM: baseline = max(baseline, atom.size.h) of VERTICAL_ALIGN_MIDDLE: baseline = max(baseline, atom.size.h div 2) else: baseline = max(baseline, iastate.baseline) let ch = ictx.cellheight baseline = baseline.round(ch) # 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. for i, atom in ictx.currentLine.atoms: let atom = ictx.currentLine.atoms[i] let iastate = ictx.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.) ictx.currentLine.size.h = max(ictx.currentLine.size.h, atom.size.h) case iastate.vertalign.keyword of VERTICAL_ALIGN_BASELINE: # Line height must be at least as high as # (line baseline) - (atom baseline) + (atom height) + (extra height). let len = iastate.vertalign.length.px(ictx.lctx, lineheight) ictx.currentLine.size.h = max(baseline - iastate.baseline + atom.size.h + len, ictx.currentLine.size.h) of VERTICAL_ALIGN_MIDDLE: # Line height must be at least # (line baseline) + (atom height / 2). ictx.currentLine.size.h = max(baseline + atom.size.h div 2, ictx.currentLine.size.h) of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM: # Line height must be at least atom height (already ensured above.) discard else: # See baseline (with len = 0). ictx.currentLine.size.h = max(baseline - iastate.baseline + atom.size.h, ictx.currentLine.size.h) # Now we can calculate the actual position of atoms inside the line. for i, atom in ictx.currentLine.atoms: let iastate = ictx.currentLine.atomstates[i] case iastate.vertalign.keyword of VERTICAL_ALIGN_BASELINE: # Atom is placed at (line baseline) - (atom baseline) - len let len = iastate.vertalign.length.px(ictx.lctx, lineheight) atom.offset.y = baseline - iastate.baseline - len of VERTICAL_ALIGN_MIDDLE: # Atom is placed at (line baseline) - ((atom height) / 2) atom.offset.y = baseline - atom.size.h div 2 of VERTICAL_ALIGN_TOP: # Atom is placed at the top of the line. atom.offset.y = 0 of VERTICAL_ALIGN_BOTTOM: # Atom is placed at the bottom of the line. atom.offset.y = ictx.currentLine.size.h - atom.size.h else: # See baseline (with len = 0). atom.offset.y = baseline - iastate.baseline # Find the best top margin and bottom edge of all atoms. # In fact, we are looking for the lowest top edge and the highest bottom # edge of the line, so we have to do this after we know where the atoms # will be placed. marginTop = max(iastate.marginTop - atom.offset.y, marginTop) bottomEdge = max(atom.offset.y + atom.size.h + iastate.marginBottom, bottomEdge) # Finally, offset all atoms' y position by the largest top margin and the # line box's top padding. let paddingTop = ictx.currentLine.paddingTop let offsety = ictx.currentLine.offsety for atom in ictx.currentLine.atoms: atom.offset.y = (atom.offset.y + marginTop + paddingTop + offsety).round(ch) ictx.currentLine.minHeight = max(ictx.currentLine.minHeight, atom.offset.y - offsety + atom.size.h) # Set the line height to new top edge + old bottom edge, and set the # baseline. ictx.currentLine.size.h = max(bottomEdge + marginTop, lineheight) ictx.currentLine.baseline = baseline # Add padding. ictx.currentLine.size.h += ictx.currentLine.paddingTop ictx.currentLine.size.h += ictx.currentLine.paddingBottom #TODO this does not really work with rounding :/ ictx.currentLine.baseline += ictx.currentLine.paddingTop # Round line ictx.currentLine.size.h = max(ictx.currentLine.size.h.round(ch), ictx.currentLine.minHeight) proc addSpacing(ictx: var InlineContext, width, height: LayoutUnit, hang = false) = let spacing = InlineAtom( t: INLINE_SPACING, size: Size( w: width, h: height ), offset: Offset(x: ictx.currentLine.size.w) ) let iastate = InlineAtomState( baseline: height #TODO vertalign? ) 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.atomstates.add(iastate) ictx.currentLine.atoms.add(spacing) # whitespaceFragment's endOffset may already be set, in this case we must # shift it by width. # (If it is not set, then it will simply be overridden when endOffset is set.) ictx.whitespaceFragment.endOffset.x += width ictx.whitespaceFragment.atoms.add(spacing) 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, 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 == FLOAT_LEFT: 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.computed{"white-space"} if whitespace == WHITESPACE_PRE: ictx.flushWhitespace(state) elif whitespace == WHITESPACE_PRE_WRAP: 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.firstBaseline = y + ictx.currentLine.baseline ictx.root.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.startOffset = Offset( x: state.startOffsetTop.x, y: y + ictx.currentLine.size.h ) state.fragment.size.w = lineWidth - state.startOffsetTop.x state.firstLine = false else: state.fragment.size.w = max(lineWidth, state.fragment.size.w) ictx.size.w = max(ictx.size.w, lineWidth) ictx.lines.add(ictx.currentLine.line) ictx.currentLine = LineBoxState( offsety: y + ictx.currentLine.size.h, line: LineBox() ) ictx.initLine() proc finish(ictx: var InlineContext, state: var InlineState) = ictx.finishLine(state, wrap = false) for line in ictx.lines: ictx.horizontalAlignLine(state, line) func minwidth(atom: InlineAtom): LayoutUnit = if atom.t == INLINE_BLOCK: return atom.innerbox.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 == MAX_CONTENT: return false # no wrap with max-content if ictx.space.w.t == MIN_CONTENT: 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.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.whitespacenum = 0 # Line wrapping if ictx.shouldWrap(atom.size.w + shift, state.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.flushLine(state) # 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) ictx.minwidth = max(ictx.minwidth, atom.minwidth) ictx.applyLineHeight(ictx.currentLine, state.computed) if atom.t != INLINE_WORD: ictx.currentLine.charwidth = 0 ictx.currentLine.atoms.add(atom) state.fragment.atoms.add(atom) ictx.currentLine.atomstates.add(iastate) ictx.currentLine.atoms[^1].offset.x += ictx.currentLine.size.w ictx.currentLine.size.w += atom.size.w 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.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.computed{"word-break"} of WORD_BREAK_NORMAL: 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 WORD_BREAK_BREAK_ALL: 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 WORD_BREAK_KEEP_ALL: 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.computed{"white-space"} of WHITESPACE_NORMAL, WHITESPACE_NOWRAP: if ictx.whitespacenum < 1: ictx.whitespacenum = 1 ictx.whitespaceFragment = state.fragment ictx.whitespaceIsLF = c == '\n' if c != '\n': ictx.whitespaceIsLF = false of WHITESPACE_PRE_LINE: if c == '\n': ictx.flushLine(state) elif ictx.whitespacenum < 1: ictx.whitespaceIsLF = false ictx.whitespacenum = 1 ictx.whitespaceFragment = state.fragment of WHITESPACE_PRE, WHITESPACE_PRE_WRAP: #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): InlineContext = var ictx = InlineContext( currentLine: LineBoxState( line: LineBox() ), bctx: addr bctx, lctx: bctx.lctx, bfcOffset: bfcOffset, space: space, root: root ) 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, str: string) = ictx.flushWhitespace(state) ictx.newWord(state) case state.computed{"text-transform"} of TEXT_TRANSFORM_NONE: ictx.layoutTextLoop(state, str) {.linearScanEnd.} of TEXT_TRANSFORM_CAPITALIZE: ictx.layoutTextLoop(state, str.capitalize()) of TEXT_TRANSFORM_UPPERCASE: ictx.layoutTextLoop(state, str.toUpper()) of TEXT_TRANSFORM_LOWERCASE: ictx.layoutTextLoop(state, str.toLower()) of TEXT_TRANSFORM_FULL_WIDTH: ictx.layoutTextLoop(state, str.fullwidth()) of TEXT_TRANSFORM_FULL_SIZE_KANA: ictx.layoutTextLoop(state, str.fullsize()) of TEXT_TRANSFORM_CHA_HALF_WIDTH: ictx.layoutTextLoop(state, str.halfwidth()) func spx(l: CSSLength, lctx: LayoutState, p: SizeConstraint, computed: CSSComputedValues, padding: LayoutUnit): LayoutUnit = let u = l.px(lctx, p) if computed{"box-sizing"} == BOX_SIZING_BORDER_BOX: return max(u - padding, 0) return max(u, 0) func spx(l: CSSLength, lctx: LayoutState, p: Option[LayoutUnit], computed: CSSComputedValues, padding: LayoutUnit): Option[LayoutUnit] = let u = l.px(lctx, p) if u.isSome: let u = u.get if computed{"box-sizing"} == BOX_SIZING_BORDER_BOX: return some(max(u - padding, 0)) return some(max(u, 0)) return u proc resolveContentWidth(sizes: var ResolvedSizes, widthpx: LayoutUnit, containingWidth: SizeConstraint, computed: CSSComputedValues, isauto = false) = if not sizes.space.w.isDefinite(): # width is indefinite, so no conflicts can be resolved here. return let total = widthpx + sizes.margin.left + sizes.margin.right + sizes.padding.left + sizes.padding.right let underflow = containingWidth.u - total if isauto or sizes.space.w.t == FIT_CONTENT: if underflow >= 0: sizes.space.w = SizeConstraint(t: sizes.space.w.t, u: underflow) else: sizes.margin.right += underflow elif underflow > 0: if not computed{"margin-left"}.auto and not computed{"margin-right"}.auto: sizes.margin.right += underflow elif not computed{"margin-left"}.auto and computed{"margin-right"}.auto: sizes.margin.right = underflow elif computed{"margin-left"}.auto and not computed{"margin-right"}.auto: sizes.margin.left = underflow else: sizes.margin.left = underflow div 2 sizes.margin.right = underflow div 2 proc resolveMargins(availableWidth: SizeConstraint, lctx: LayoutState, computed: CSSComputedValues): RelativeRect = # Note: we use availableWidth for percentage resolution intentionally. return RelativeRect( top: computed{"margin-top"}.px(lctx, availableWidth), bottom: computed{"margin-bottom"}.px(lctx, availableWidth), left: computed{"margin-left"}.px(lctx, availableWidth), right: computed{"margin-right"}.px(lctx, availableWidth) ) proc resolvePadding(availableWidth: SizeConstraint, lctx: LayoutState, computed: CSSComputedValues): RelativeRect = # Note: we use availableWidth for percentage resolution intentionally. return RelativeRect( top: computed{"padding-top"}.px(lctx, availableWidth), bottom: computed{"padding-bottom"}.px(lctx, availableWidth), left: computed{"padding-left"}.px(lctx, availableWidth), right: computed{"padding-right"}.px(lctx, availableWidth) ) proc resolveBlockWidth(sizes: var ResolvedSizes, containingWidth: SizeConstraint, computed: CSSComputedValues, lctx: LayoutState) = let width = computed{"width"} let padding = sizes.padding.left + sizes.padding.right var widthpx: LayoutUnit = 0 if not width.auto and width.canpx(containingWidth): widthpx = width.spx(lctx, containingWidth, computed, padding) sizes.space.w = stretch(widthpx) sizes.resolveContentWidth(widthpx, containingWidth, computed, width.auto) if not computed{"max-width"}.auto: let maxWidth = computed{"max-width"}.spx(lctx, containingWidth, computed, padding) sizes.maxWidth = maxWidth if sizes.space.w.t in {STRETCH, FIT_CONTENT} and maxWidth < sizes.space.w.u or sizes.space.w.t == MAX_CONTENT: sizes.space.w = stretch(maxWidth) #TODO is stretch ok here? if sizes.space.w.t == STRETCH: # available width would stretch over max-width sizes.space.w = stretch(maxWidth) else: # FIT_CONTENT # available width could be higher than max-width (but not necessarily) sizes.space.w = fitContent(maxWidth) sizes.resolveContentWidth(maxWidth, containingWidth, computed) if not computed{"min-width"}.auto: let minWidth = computed{"min-width"}.spx(lctx, containingWidth, computed, padding) sizes.minWidth = minWidth if sizes.space.w.t in {STRETCH, FIT_CONTENT} and minWidth > sizes.space.w.u or sizes.space.w.t == MIN_CONTENT: # 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(minWidth) sizes.resolveContentWidth(minWidth, containingWidth, computed) proc resolveBlockHeight(sizes: var ResolvedSizes, containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], computed: CSSComputedValues, lctx: LayoutState) = let height = computed{"height"} let padding = sizes.padding.top + sizes.padding.bottom var heightpx: LayoutUnit = 0 if not height.auto and height.canpx(percHeight): heightpx = height.spx(lctx, percHeight, computed, padding).get sizes.space.h = stretch(heightpx) if not computed{"max-height"}.auto: let maxHeight = computed{"max-height"}.spx(lctx, percHeight, computed, padding) sizes.maxHeight = maxHeight.get(high(LayoutUnit)) if maxHeight.isSome: let maxHeight = maxHeight.get if sizes.space.h.t in {STRETCH, FIT_CONTENT} and maxHeight < sizes.space.h.u or sizes.space.h.t == MAX_CONTENT: # same reasoning as for width. if sizes.space.h.t == STRETCH: sizes.space.h = stretch(maxHeight) else: # FIT_CONTENT sizes.space.h = fitContent(maxHeight) if not computed{"min-height"}.auto: let minHeight = computed{"min-height"}.spx(lctx, percHeight, computed, padding) sizes.minHeight = minHeight.get(0) if minHeight.isSome: let minHeight = minHeight.get if sizes.space.h.t in {STRETCH, FIT_CONTENT} and minHeight > sizes.space.h.u or sizes.space.h.t == MIN_CONTENT: # same reasoning as for width. sizes.space.h = stretch(minHeight) proc resolveAbsoluteWidth(sizes: var ResolvedSizes, containingWidth: SizeConstraint, computed: CSSComputedValues, lctx: LayoutState) = let left = computed{"left"} let right = computed{"right"} let width = computed{"width"} if width.auto: if not left.auto and not right.auto: # width is auto and left & right are not auto. # Solve for width. if containingWidth.isDefinite: let leftpx = left.px(lctx, containingWidth) let rightpx = right.px(lctx, containingWidth) let u = containingWidth.u - leftpx - rightpx - sizes.margin.left - sizes.margin.right - sizes.padding.left - sizes.padding.right sizes.space.w = stretch(max(u, 0)) else: sizes.space.w = containingWidth else: # Return shrink to fit and solve for left/right. # Note that we do not know content width yet, so it is impossible to # solve left/right yet. sizes.space.w = fitContent(containingWidth) else: let padding = sizes.padding.left + sizes.padding.right let widthpx = width.spx(lctx, containingWidth, 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.w = stretch(widthpx) proc resolveAbsoluteHeight(sizes: var ResolvedSizes, containingHeight: SizeConstraint, computed: CSSComputedValues, lctx: LayoutState) = #TODO this might be incorrect because of percHeight? let top = computed{"top"} let bottom = computed{"bottom"} let height = computed{"height"} if height.auto: if not top.auto and not bottom.auto: # height is auto and top & bottom are not auto. # Solve for height. if containingHeight.isDefinite: let toppx = top.px(lctx, containingHeight) let bottompx = bottom.px(lctx, containingHeight) #TODO I assume border collapsing does not matter here? let u = containingHeight.u - toppx - bottompx - sizes.margin.top - sizes.margin.bottom - sizes.padding.top - sizes.padding.bottom sizes.space.h = stretch(max(u, 0)) else: sizes.space.h = containingHeight else: sizes.space.h = fitContent(containingHeight) else: let padding = sizes.padding.top + sizes.padding.bottom let heightpx = height.spx(lctx, containingHeight, computed, padding) sizes.space.h = stretch(heightpx) proc resolveBlockSizes(lctx: LayoutState, containingWidth, containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], computed: CSSComputedValues): ResolvedSizes = var sizes = ResolvedSizes( margin: resolveMargins(containingWidth, lctx, computed), padding: resolvePadding(containingWidth, lctx, computed), # Take defined sizes if our width/height resolves to auto. # For block boxes, this is: # (width: stretch(parentWidth), height: max-content) space: AvailableSpace(w: containingWidth, h: containingHeight), minWidth: 0, maxWidth: high(LayoutUnit), minHeight: 0, maxHeight: high(LayoutUnit) ) # Finally, calculate available width and height. sizes.resolveBlockWidth(containingWidth, computed, lctx) sizes.resolveBlockHeight(containingHeight, percHeight, computed, lctx) return sizes # Calculate and resolve available width & height for absolutely positioned # boxes. proc resolveAbsoluteSizes(lctx: LayoutState, computed: CSSComputedValues): ResolvedSizes = let containingWidth = lctx.positioned[^1].w let containingHeight = lctx.positioned[^1].h var sizes = ResolvedSizes( margin: resolveMargins(containingWidth, lctx, computed), padding: resolvePadding(containingWidth, lctx, computed), minWidth: 0, maxWidth: high(LayoutUnit) ) sizes.resolveAbsoluteWidth(containingWidth, computed, lctx) sizes.resolveAbsoluteHeight(containingHeight, computed, lctx) return sizes # Calculate and resolve available width & height for floating boxes. proc resolveFloatSizes(lctx: LayoutState, containingWidth, containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], computed: CSSComputedValues): ResolvedSizes = var space = AvailableSpace( w: fitContent(containingWidth), h: containingHeight ) let padding = resolvePadding(containingWidth, lctx, computed) let inlinePadding = padding.left + padding.right let blockPadding = padding.top + padding.bottom let minWidth: LayoutUnit = if not computed{"min-width"}.auto: computed{"min-width"}.spx(lctx, containingWidth, computed, inlinePadding) else: 0 let maxWidth = if not computed{"max-width"}.auto: computed{"max-width"}.spx(lctx, containingWidth, computed, inlinePadding) else: high(LayoutUnit) let width = computed{"width"} if not width.auto and width.canpx(containingWidth): let widthpx = width.spx(lctx, containingWidth, computed, inlinePadding) space.w = stretch(clamp(widthpx, minWidth, maxWidth)) elif containingWidth.isDefinite(): space.w = fitContent(clamp(containingWidth.u, minWidth, maxWidth)) let minHeight: LayoutUnit = if not computed{"min-height"}.auto: computed{"min-height"}.spx(lctx, percHeight, computed, blockPadding).get(0) else: 0 let maxHeight = if not computed{"max-height"}.auto: computed{"max-height"}.spx(lctx, percHeight, computed, blockPadding) .get(high(LayoutUnit)) else: high(LayoutUnit) let height = computed{"height"} if not height.auto and height.canpx(containingHeight): let heightpx = height.px(lctx, containingHeight) space.h = stretch(clamp(heightpx, minHeight, maxHeight)) elif containingHeight.isDefinite(): space.h = fitContent(clamp(containingHeight.u, minHeight, maxHeight)) return ResolvedSizes( margin: resolveMargins(containingWidth, lctx, computed), padding: padding, space: space, minWidth: minWidth, maxWidth: maxWidth, minHeight: minHeight, maxHeight: maxHeight ) # Calculate and resolve available width & height for box children. # containingWidth: width of the containing box # containingHeight: ditto, but with height. # Note that this is not the final content size, just the amount of space # available for content. # The percentage width/height is generally # availableSize.isDefinite() ? availableSize.u : 0, but for some reason it # differs for the root height (TODO: and all heights in quirks mode) in that # it uses the lctx height. Therefore we pass percHeight as a separate # parameter. (TODO surely there is a better solution to this?) proc resolveSizes(lctx: LayoutState, containingWidth, containingHeight: SizeConstraint, percHeight: Option[LayoutUnit], computed: CSSComputedValues): ResolvedSizes = if computed{"position"} == POSITION_ABSOLUTE: return lctx.resolveAbsoluteSizes(computed) elif computed{"float"} != FLOAT_NONE: return lctx.resolveFloatSizes(containingWidth, containingHeight, percHeight, computed) else: return lctx.resolveBlockSizes(containingWidth, containingHeight, percHeight, computed) proc resolveTableCellSizes(lctx: LayoutState, containingWidth, containingHeight: SizeConstraint, override: bool, computed: CSSComputedValues): ResolvedSizes = var sizes = ResolvedSizes( padding: resolvePadding(containingWidth, lctx, computed), space: AvailableSpace(w: containingWidth, h: containingHeight), minWidth: 0, maxWidth: high(LayoutUnit), minHeight: 0, maxHeight: high(LayoutUnit) ) if not override: let width = computed{"width"} if not width.auto and width.unit != UNIT_PERC: sizes.space.w = stretch(width.px(lctx)) sizes.space.w.u -= sizes.padding.left sizes.space.w.u -= sizes.padding.right if not override: let height = computed{"height"} if not height.auto and height.unit != UNIT_PERC: sizes.space.h = stretch(height.px(lctx)) return sizes func toPercSize(sc: SizeConstraint): Option[LayoutUnit] = if sc.isDefinite(): return some(sc.u) return none(LayoutUnit) 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 proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder], space: AvailableSpace, computed: CSSComputedValues, offset, bfcOffset: Offset): RootInlineFragment proc layoutBlock(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder, sizes: ResolvedSizes) proc layoutTable(lctx: LayoutState, table: BlockBox, builder: TableBoxBuilder, sizes: ResolvedSizes) # Note: padding must still be applied after this. proc applyWidth(box: BlockBox, sizes: ResolvedSizes, maxChildWidth: LayoutUnit) = # Make the box as small/large as the content's width or specified width. box.size.w = maxChildWidth.applySizeConstraint(sizes.space.w) # Then, clamp it to minWidth and maxWidth (if applicable). box.size.w = clamp(box.size.w, sizes.minWidth, sizes.maxWidth) proc applyHeight(box: BlockBox, sizes: ResolvedSizes, maxChildHeight: LayoutUnit) = # Make the box as small/large as the content's width or specified width. box.size.h = maxChildHeight.applySizeConstraint(sizes.space.h) # Then, clamp it to minWidth and maxWidth (if applicable). box.size.h = clamp(box.size.h, sizes.minHeight, sizes.maxHeight) proc applyPadding(box: BlockBox, padding: RelativeRect) = box.size.w += padding.left box.size.w += padding.right box.size.h += padding.top box.size.h += padding.bottom func bfcOffset(bctx: BlockContext): Offset = if bctx.parentBps != nil: return bctx.parentBps.offset return Offset() proc layoutInline(bctx: var BlockContext, box: BlockBox, children: seq[BoxBuilder], sizes: ResolvedSizes) = var bfcOffset = bctx.bfcOffset let offset = Offset(x: sizes.padding.left, y: sizes.padding.top) bfcOffset.x += box.offset.x + offset.x bfcOffset.y += box.offset.y + offset.y box.inline = bctx.layoutRootInline(children, sizes.space, box.computed, offset, bfcOffset) box.xminwidth = max(box.xminwidth, box.inline.xminwidth) box.size.w = box.inline.size.w + sizes.padding.left + sizes.padding.right box.applyWidth(sizes, box.inline.size.w) box.applyHeight(sizes, box.inline.size.h) box.applyPadding(sizes.padding) box.baseline = offset.y + box.inline.baseline box.firstBaseline = offset.y + box.inline.firstBaseline const DisplayBlockLike = {DISPLAY_BLOCK, DISPLAY_LIST_ITEM, DISPLAY_INLINE_BLOCK} # Return true if no more margin collapsing can occur for the current strut. func canFlushMargins(builder: BlockBoxBuilder, sizes: ResolvedSizes): bool = if builder.computed{"position"} == POSITION_ABSOLUTE: return false return sizes.padding.top != 0 or sizes.padding.bottom != 0 or builder.inlinelayout or builder.computed{"display"} notin DisplayBlockLike proc flushMargins(bctx: var BlockContext, box: BlockBox) = # Apply uncommitted margins. let margin = bctx.marginTodo.sum() if bctx.marginTarget == nil: box.offset.y += margin else: if bctx.marginTarget.box != nil: bctx.marginTarget.box.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 CLEAR_LEFT, CLEAR_INLINE_START: for ex in bctx.exclusions: if ex.t == FLOAT_LEFT: y = max(ex.offset.y + ex.size.h, y) of CLEAR_RIGHT, CLEAR_INLINE_END: for ex in bctx.exclusions: if ex.t == FLOAT_RIGHT: y = max(ex.offset.y + ex.size.h, y) of CLEAR_BOTH: for ex in bctx.exclusions: y = max(ex.offset.y + ex.size.h, y) of CLEAR_NONE: assert false bctx.clearOffset = y offset.y = y - bctx.bfcOffset.y type BlockState = object offset: Offset maxChildWidth: LayoutUnit nested: seq[BlockBox] 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, clear: CSSClear): Offset = # Algorithm originally from QEmacs. var y = offset.y let leftStart = offset.x let rightStart = max(offset.x + 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 == FLOAT_LEFT and left < ex2: left = ex2 if ex.t == FLOAT_RIGHT and right > ex.offset.x: right = ex.offset.x miny = min(ey2, miny) if right - left >= size.w or miny == high(LayoutUnit): # Enough space, or no other exclusions found at this y offset. if float == FLOAT_LEFT: return Offset(x: left, y: y) else: # FLOAT_RIGHT 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 proc positionFloat(bctx: var BlockContext, child: BlockBox, space: AvailableSpace, bfcOffset: Offset) = let clear = child.computed{"clear"} if clear != CLEAR_NONE: child.offset.clearFloats(bctx, clear) let size = Size( w: child.margin.left + child.margin.right + child.size.w, h: child.margin.top + child.margin.bottom + child.size.h ) let childBfcOffset = Offset( x: bfcOffset.x + child.offset.x - child.margin.left, y: max(bfcOffset.y + child.offset.y - child.margin.top, bctx.clearOffset) ) assert space.w.t != FIT_CONTENT let ft = child.computed{"float"} assert ft != FLOAT_NONE let offset = bctx.findNextFloatOffset(childBfcOffset, size, space, ft, clear) child.offset = Offset( x: offset.x - bfcOffset.x + child.margin.left, y: offset.y - bfcOffset.y + child.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 positionFloats(bctx: var BlockContext) = for f in bctx.unpositionedFloats: bctx.positionFloat(f.box, f.space, f.parentBps.offset) bctx.unpositionedFloats.setLen(0) func establishesBFC(computed: CSSComputedValues): bool = return computed{"float"} != FLOAT_NONE or computed{"position"} == POSITION_ABSOLUTE or computed{"display"} in {DISPLAY_INLINE_BLOCK, DISPLAY_FLOW_ROOT} + InternalTableBox #TODO overflow, contain, flex, grid, multicol, column-span proc layoutFlow(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder, sizes: ResolvedSizes) = let isBfc = builder.computed.establishesBFC() if not isBfc: bctx.marginTodo.append(sizes.margin.top) if builder.canFlushMargins(sizes): bctx.flushMargins(box) bctx.positionFloats() if builder.computed{"clear"} != CLEAR_NONE: box.offset.clearFloats(bctx, builder.computed{"clear"}) if builder.inlinelayout: # Builder only contains inline boxes. bctx.layoutInline(box, builder.children, sizes) else: # Builder only contains block boxes. bctx.layoutBlock(box, builder, sizes) if not isBfc: bctx.marginTodo.append(sizes.margin.bottom) func toperc100(sc: SizeConstraint): Option[LayoutUnit] = if sc.isDefinite(): return some(sc.u) return none(LayoutUnit) # parentWidth, parentHeight: width/height of the containing block. proc addInlineBlock(ictx: var InlineContext, state: var InlineState, builder: BlockBoxBuilder, parentWidth, parentHeight: SizeConstraint) = let lctx = ictx.lctx let percHeight = parentHeight.toperc100() let sizes = lctx.resolveFloatSizes(parentWidth, maxContent(), percHeight, builder.computed) let box = BlockBox( computed: builder.computed, node: builder.node, margin: sizes.margin ) var bctx = BlockContext(lctx: lctx) bctx.marginTodo.append(sizes.margin.top) case builder.computed{"display"} of DISPLAY_INLINE_BLOCK: bctx.layoutFlow(box, builder, sizes) of DISPLAY_INLINE_TABLE: lctx.layoutTable(box, TableBoxBuilder(builder), sizes) else: assert false, $builder.computed{"display"} bctx.positionFloats() bctx.marginTodo.append(sizes.margin.bottom) let marginTop = box.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.offset.y + box.size.h + marginBottom: box.size.h = bctx.maxFloatHeight - box.offset.y - marginBottom box.offset.y = 0 # Apply the block box's properties to the atom itself. let iblock = InlineAtom( t: INLINE_BLOCK, innerbox: box, offset: Offset(x: sizes.margin.left), size: Size( w: box.size.w + sizes.margin.left + sizes.margin.right, h: box.size.h ) ) let iastate = InlineAtomState( baseline: box.baseline, vertalign: builder.computed{"vertical-align"}, marginTop: marginTop, marginBottom: bctx.marginTodo.sum() ) discard ictx.addAtom(state, iastate, iblock) ictx.whitespacenum = 0 proc layoutInline(ictx: var InlineContext, box: InlineBoxBuilder): InlineFragment = let lctx = ictx.lctx let fragment = InlineFragment( computed: box.computed, node: box.node, splitType: box.splitType ) if stSplitStart in box.splitType: let marginLeft = box.computed{"margin-left"}.px(lctx, ictx.space.w) ictx.currentLine.size.w += marginLeft var state = InlineState( computed: box.computed, node: box.node, fragment: fragment, firstLine: true, startOffsetTop: Offset( x: ictx.currentLine.widthAfterWhitespace, y: ictx.currentLine.offsety ) ) if box.newline: ictx.flushLine(state) if stSplitStart in box.splitType: let paddingLeft = box.computed{"padding-left"}.px(lctx, ictx.space.w) ictx.currentLine.size.w += paddingLeft assert box.children.len == 0 or box.text.len == 0 ictx.applyLineHeight(ictx.currentLine, state.computed) if ictx.firstTextFragment == nil: ictx.firstTextFragment = fragment ictx.lastTextFragment = fragment for text in box.text: ictx.layoutText(state, text) for child in box.children: case child.computed{"display"} of DISPLAY_INLINE: let child = ictx.layoutInline(InlineBoxBuilder(child)) state.fragment.children.add(child) of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE: let child = BlockBoxBuilder(child) let w = fitContent(ictx.space.w) let h = ictx.space.h ictx.addInlineBlock(state, child, w, h) else: assert false, "child.t is " & $child.computed{"display"} if stSplitEnd in box.splitType: let paddingRight = box.computed{"padding-right"}.px(lctx, ictx.space.w) ictx.currentLine.size.w += paddingRight let marginRight = box.computed{"margin-right"}.px(lctx, ictx.space.w) ictx.currentLine.size.w += marginRight #TODO we verticalAlignLine here to know line height, but this is incredibly # ugly. Maybe figure out some incremental line alignment scheme instead? ictx.verticalAlignLine() if state.firstLine: fragment.startOffset = Offset( x: state.startOffsetTop.x, y: ictx.currentLine.offsety + ictx.currentLine.size.h ) fragment.endOffset = Offset( x: ictx.currentLine.size.w, y: ictx.currentLine.offsety ) fragment.size.h = ictx.currentLine.offsety + ictx.currentLine.size.h - state.startOffsetTop.y return fragment proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder], space: AvailableSpace, computed: CSSComputedValues, offset, bfcOffset: Offset): RootInlineFragment = let root = RootInlineFragment( offset: offset, fragment: InlineFragment(computed: computed) ) var ictx = bctx.initInlineContext(space, bfcOffset, root) for child in inlines: case child.computed{"display"} of DISPLAY_INLINE: let childFragment = ictx.layoutInline(InlineBoxBuilder(child)) root.fragment.children.add(childFragment) of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE: #TODO ??? var state = InlineState( computed: computed, fragment: InlineFragment(computed: root.fragment.computed), firstLine: true ) let w = fitContent(ictx.space.w) let h = ictx.space.h ictx.addInlineBlock(state, BlockBoxBuilder(child), w, h) root.fragment.children.add(state.fragment) else: assert false, "child.t is " & $child.computed{"display"} if ictx.firstTextFragment != nil: root.fragment.startOffset = ictx.firstTextFragment.startOffset if ictx.lastTextFragment != nil: root.fragment.endOffset = ictx.lastTextFragment.endOffset root.fragment.size = ictx.size let lastFragment = if ictx.lastTextFragment != nil: ictx.lastTextFragment else: InlineFragment(computed: computed) var state = InlineState(computed: computed, fragment: lastFragment) ictx.finish(state) root.xminwidth = ictx.minwidth return root proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace, lctx: LayoutState): RootInlineFragment = let space = AvailableSpace( w: fitContent(space.w), h: space.h ) #TODO we should put markers right before the first atom of the parent # list item or something... var bctx = BlockContext(lctx: lctx) let children = @[BoxBuilder(builder)] return bctx.layoutRootInline(children, space, builder.computed, Offset(), Offset()) # Build a block box without establishing a new block formatting context. proc buildBlock(bctx: var BlockContext, builder: BlockBoxBuilder, space: AvailableSpace, offset: Offset): BlockBox = let lctx = bctx.lctx let availableWidth = space.w let availableHeight = maxContent() #TODO fit-content when clip let percHeight = space.h.toPercSize() let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, builder.computed) let box = BlockBox( computed: builder.computed, node: builder.node, offset: Offset(x: offset.x + sizes.margin.left, y: offset.y), margin: sizes.margin ) bctx.layoutFlow(box, builder, sizes) return box proc buildListItem(bctx: var BlockContext, builder: ListItemBoxBuilder, space: AvailableSpace, offset: Offset): ListItemBox = let availableWidth = stretch(space.w) let availableHeight = maxContent() #TODO fit-content when clip let percHeight = space.h.toPercSize() let lctx = bctx.lctx let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, builder.computed) let box = ListItemBox( computed: builder.computed, node: builder.node, offset: Offset(x: offset.x + sizes.margin.left, y: offset.y), margin: sizes.margin ) if builder.marker != nil: box.marker = buildMarker(builder.marker, sizes.space, lctx) bctx.layoutFlow(box, builder.content, sizes) return box proc buildTable(bctx: var BlockContext, builder: TableBoxBuilder, space: AvailableSpace, offset: Offset): BlockBox = let availableWidth = fitContent(space.w) let availableHeight = maxContent() #TODO fit-content when clip let percHeight = space.h.toPercSize() let lctx = bctx.lctx let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, builder.computed) let box = BlockBox( computed: builder.computed, node: builder.node, offset: Offset(x: offset.x + sizes.margin.left, y: offset.y), margin: sizes.margin ) let isBfc = builder.computed.establishesBFC() if not isBfc: bctx.marginTodo.append(sizes.margin.top) bctx.flushMargins(box) lctx.layoutTable(box, builder, sizes) if not isBfc: bctx.marginTodo.append(sizes.margin.bottom) return box proc positionAbsolute(lctx: LayoutState, box: BlockBox, margin: RelativeRect) = let last = lctx.positioned[^1] let left = box.computed{"left"} let right = box.computed{"right"} let top = box.computed{"top"} let bottom = box.computed{"bottom"} let parentWidth = applySizeConstraint(lctx.attrs.width_px, last.w) let parentHeight = applySizeConstraint(lctx.attrs.height_px, last.h) if not left.auto: box.offset.x = left.px(lctx, parentWidth) box.offset.x += margin.left elif not right.auto: box.offset.x = parentWidth - right.px(lctx, parentWidth) - box.size.w box.offset.x -= margin.right if not top.auto: box.offset.y = top.px(lctx, parentHeight) box.offset.y += margin.top elif not bottom.auto: box.offset.y = parentHeight - bottom.px(lctx, parentHeight) - box.size.h box.offset.y -= margin.bottom proc positionRelative(parent, box: BlockBox, lctx: LayoutState) = let left = box.computed{"left"} let right = box.computed{"right"} let top = box.computed{"top"} let bottom = box.computed{"bottom"} if not left.auto: box.offset.x += right.px(lctx) elif not right.auto: box.offset.x += parent.size.w - right.px(lctx) - box.size.w if not top.auto: box.offset.y += top.px(lctx) elif not top.auto: box.offset.y -= parent.size.h - bottom.px(lctx) - box.size.h type CellWrapper = ref object builder: TableCellBoxBuilder 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 builder: TableRowBoxBuilder ncols: int ColumnContext = object minwidth: LayoutUnit width: LayoutUnit wspecified: bool weight: float64 TableContext = object lctx: LayoutState caption: TableCaptionBoxBuilder rows: seq[RowContext] cols: seq[ColumnContext] growing: seq[CellWrapper] maxwidth: LayoutUnit blockspacing: LayoutUnit inlinespacing: LayoutUnit collapse: bool reflow: seq[bool] space: AvailableSpace # space we got from parent proc buildTableCaption(lctx: LayoutState, builder: TableCaptionBoxBuilder, availableWidth, availableHeight: SizeConstraint): BlockBox = let percHeight = availableHeight.toperc100() let sizes = lctx.resolveSizes(availableWidth, availableHeight, percHeight, builder.computed) let box = BlockBox( computed: builder.computed, node: builder.node, margin: sizes.margin ) var bctx = BlockContext(lctx: lctx) bctx.layoutFlow(box, builder, sizes) # Include marginTodo in our own height. #TODO this is not quite correct, as height should be the padding height. box.size.h += box.offset.y box.size.h += bctx.marginTodo.sum() return box proc buildTableCell(lctx: LayoutState, builder: TableCellBoxBuilder, availableWidth, availableHeight: SizeConstraint, override: bool): BlockBox = let sizes = lctx.resolveTableCellSizes(availableWidth, availableHeight, override, builder.computed) let box = BlockBox( computed: builder.computed, node: builder.node, margin: sizes.margin ) var ctx = BlockContext(lctx: lctx) ctx.layoutFlow(box, builder, sizes) # Table cells ignore margins. box.offset.y = 0 return box # Sort growing cells, and filter out cells that have grown to their intended # rowspan. proc sortGrowing(pctx: var TableContext) = var i = 0 for j in 0 ..< pctx.growing.len: if pctx.growing[i].grown == 0: continue if j != i: pctx.growing[i] = pctx.growing[j] 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, box: TableRowBoxBuilder, parent: BlockBox, rowi, numrows: int): RowContext = var ctx = RowContext(builder: box, cells: newSeq[CellWrapper](box.children.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 child in box.children: pctx.growRowspan(ctx, growi, i, n, growlen) assert child.computed{"display"} == DISPLAY_TABLE_CELL let cellbuilder = TableCellBoxBuilder(child) let colspan = cellbuilder.computed{"-cha-colspan"} let rowspan = min(cellbuilder.computed{"-cha-rowspan"}, numrows - rowi) let computedWidth = cellbuilder.computed{"width"} let cw = if not computedWidth.auto: stretch(computedWidth.px(pctx.lctx, pctx.space.w)) else: maxContent() #TODO specified table height should be distributed among rows. # Allow the table cell to use its specified width. let box = pctx.lctx.buildTableCell(cellbuilder, cw, maxContent(), override = false) let wrapper = CellWrapper( box: box, builder: cellbuilder, 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.xminwidth div colspan let w = box.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 not computedWidth.auto: let ww = computedWidth.px(pctx.lctx, pctx.space.w) # A specified column already exists; we take the larger width. if ww > pctx.cols[i].width: pctx.cols[i].width = ww ctx.reflow[i] = true else: if pctx.cols[i].width < w: wrapper.reflow = true else: if not computedWidth.auto: let ww = computedWidth.px(pctx.lctx, pctx.space.w) # 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 = ww else: if pctx.cols[i].width < w: pctx.cols[i].width = w ctx.reflow[i] = 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() for i in 0 ..< ctx.cells.len: doAssert ctx.cells[i] != nil, $i ctx.ncols = n return ctx proc alignTableCell(cell: BlockBox, availableHeight, baseline: LayoutUnit) = case cell.computed{"vertical-align"}.keyword of VERTICAL_ALIGN_TOP: cell.offset.y = 0 of VERTICAL_ALIGN_MIDDLE: cell.offset.y = availableHeight div 2 - cell.size.h div 2 of VERTICAL_ALIGN_BOTTOM: cell.offset.y = availableHeight - cell.size.h else: cell.offset.y = baseline - cell.firstBaseline proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, builder: TableRowBoxBuilder): BlockBox = var x: LayoutUnit = 0 var n = 0 let row = BlockBox( computed: builder.computed, node: builder.node ) var baseline: LayoutUnit = 0 # real cellwrappers of fillers var to_align: seq[CellWrapper] # cells with rowspan > 1 that must store baseline var to_baseline: seq[CellWrapper] # cells that we must update row height of var to_height: seq[CellWrapper] for cellw in ctx.cells: var w: LayoutUnit = 0 for i in n ..< n + cellw.colspan: w += pctx.cols[i].width # Add inline spacing for merged columns. w += pctx.inlinespacing * (cellw.colspan - 1) * 2 if cellw.reflow and cellw.builder != nil: # Do not allow the table cell to make use of its specified width. # e.g. in the following table # # # # # # # #
5ch
9ch
# the TD with a width of 5ch should be 9ch wide as well. cellw.box = pctx.lctx.buildTableCell(cellw.builder, stretch(w), maxContent(), override = true) w = max(w, cellw.box.size.w) let cell = cellw.box x += pctx.inlinespacing if cell != nil: cell.offset.x += x x += pctx.inlinespacing x += w n += cellw.colspan const HasNoBaseline = { VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM } if cell != nil: if cell.computed{"vertical-align"}.keyword notin HasNoBaseline: # baseline baseline = max(cell.firstBaseline, baseline) if cellw.rowspan > 1: to_baseline.add(cellw) row.nested.add(cell) if cellw.rowspan > 1: to_height.add(cellw) row.size.h = max(row.size.h, cell.size.h div cellw.rowspan) else: let real = cellw.real row.size.h = max(row.size.h, real.box.size.h div cellw.rowspan) to_height.add(real) if cellw.last: to_align.add(real) for cellw in to_height: cellw.height += row.size.h for cellw in to_baseline: cellw.baseline = baseline for cellw in to_align: alignTableCell(cellw.box, cellw.height, cellw.baseline) for cell in row.nested: alignTableCell(cell, row.size.h, baseline) row.size.w = x return row proc preBuildTableRows(ctx: var TableContext, rows: seq[TableRowBoxBuilder], table: BlockBox) = for i in 0 ..< rows.len: let row = rows[i] let rctx = ctx.preBuildTableRow(row, table, i, rows.len) ctx.rows.add(rctx) ctx.maxwidth = max(rctx.width, ctx.maxwidth) proc preBuildTableRows(ctx: var TableContext, builder: TableBoxBuilder, table: BlockBox) = # Use separate seqs for different row groups, so that e.g. this HTML: # echo '
world
hello'|cha -T text/html # is rendered as: # hello # world var thead: seq[TableRowBoxBuilder] var tbody: seq[TableRowBoxBuilder] var tfoot: seq[TableRowBoxBuilder] var caption: TableCaptionBoxBuilder for child in builder.children: assert child.computed{"display"} in ProperTableChild case child.computed{"display"} of DISPLAY_TABLE_ROW: tbody.add(TableRowBoxBuilder(child)) of DISPLAY_TABLE_HEADER_GROUP: for child in child.children: assert child.computed{"display"} == DISPLAY_TABLE_ROW thead.add(TableRowBoxBuilder(child)) of DISPLAY_TABLE_ROW_GROUP: for child in child.children: assert child.computed{"display"} == DISPLAY_TABLE_ROW tbody.add(TableRowBoxBuilder(child)) of DISPLAY_TABLE_FOOTER_GROUP: for child in child.children: assert child.computed{"display"} == DISPLAY_TABLE_ROW tfoot.add(TableRowBoxBuilder(child)) of DISPLAY_TABLE_CAPTION: if caption == nil: caption = TableCaptionBoxBuilder(child) else: discard if caption != nil: ctx.caption = caption ctx.preBuildTableRows(thead, table) ctx.preBuildTableRows(tbody, table) ctx.preBuildTableRows(tfoot, table) proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit, weight: var float64): seq[int] = # Spacing for each column: var avail = newSeqUninitialized[int](ctx.cols.len) var i = 0 var j = 0 while i < ctx.cols.len: if not ctx.cols[i].wspecified: avail[j] = i let colw = ctx.cols[i].width let w = if colw < W: toFloat64(colw) else: toFloat64(W) * (ln(toFloat64(colw) / toFloat64(W)) + 1) ctx.cols[i].weight = w weight += w inc j else: W -= ctx.cols[i].width avail.del(j) inc i return avail func needsRedistribution(ctx: TableContext, computed: CSSComputedValues): bool = case ctx.space.w.t of MIN_CONTENT, MAX_CONTENT: # bleh return false of STRETCH: let u = ctx.space.w.u return u > ctx.maxwidth or u < ctx.maxwidth of FIT_CONTENT: let u = ctx.space.w.u return u > ctx.maxwidth and not computed{"width"}.auto or u < ctx.maxwidth proc redistributeWidth(ctx: var TableContext) = var W = ctx.space.w.u # Remove inline spacing from distributable width. W -= ctx.cols.len * ctx.inlinespacing * 2 var weight: float64 var avail = ctx.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 sqrt(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 * ctx.cols[j].weight let mw = ctx.cols[j].minwidth ctx.cols[j].width = x if mw > x: W -= mw ctx.cols[j].width = mw avail.del(i) redo = true else: weight += ctx.cols[j].weight ctx.reflow[j] = true proc reflowTableCells(ctx: var TableContext) = for i in countdown(ctx.rows.high, 0): var row = addr ctx.rows[i] var n = ctx.cols.len - 1 for j in countdown(row.cells.high, 0): let m = n - row.cells[j].colspan while n > m: if ctx.reflow[n]: row.cells[j].reflow = true if n < row.reflow.len and row.reflow[n]: ctx.reflow[n] = true dec n proc buildTableRows(ctx: TableContext, table: BlockBox, sizes: ResolvedSizes) = var y: LayoutUnit = 0 for roww in ctx.rows: if roww.builder.computed{"visibility"} == VISIBILITY_COLLAPSE: continue y += ctx.blockspacing let row = ctx.buildTableRow(roww, table, roww.builder) row.offset.y += y row.offset.x += sizes.padding.left row.size.w += sizes.padding.left row.size.w += sizes.padding.right y += ctx.blockspacing y += row.size.h table.nested.add(row) table.size.w = max(row.size.w, table.size.w) table.size.h = applySizeConstraint(y, sizes.space.h) proc addTableCaption(ctx: TableContext, table: BlockBox) = let lctx = ctx.lctx case ctx.caption.computed{"caption-side"} of CAPTION_SIDE_TOP, CAPTION_SIDE_BLOCK_START: let caption = lctx.buildTableCaption(ctx.caption, stretch(table.size.w), maxContent()) for r in table.nested: r.offset.y += caption.size.h table.nested.insert(caption, 0) table.size.h += caption.size.h table.size.w = max(table.size.w, caption.size.w) of CAPTION_SIDE_BOTTOM, CAPTION_SIDE_BLOCK_END: let caption = lctx.buildTableCaption(ctx.caption, stretch(table.size.w), maxContent()) caption.offset.y += table.size.h table.nested.add(caption) table.size.h += caption.size.h table.size.w = max(table.size.w, caption.size.w) # 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(lctx: LayoutState, table: BlockBox, builder: TableBoxBuilder, sizes: ResolvedSizes) = let collapse = table.computed{"border-collapse"} == BORDER_COLLAPSE_COLLAPSE var ctx = TableContext(lctx: lctx, collapse: collapse, space: sizes.space) if not ctx.collapse: ctx.inlinespacing = table.computed{"border-spacing"}.a.px(lctx) ctx.blockspacing = table.computed{"border-spacing"}.b.px(lctx) ctx.preBuildTableRows(builder, table) ctx.reflow = newSeq[bool](ctx.cols.len) if ctx.needsRedistribution(table.computed): ctx.redistributeWidth() for col in ctx.cols: table.size.w += col.width ctx.reflowTableCells() ctx.buildTableRows(table, sizes) if ctx.caption != nil: ctx.addTableCaption(table) proc postAlignChild(box, child: BlockBox, width: LayoutUnit) = case box.computed{"text-align"} of TEXT_ALIGN_CHA_CENTER: child.offset.x += max(width div 2 - child.size.w div 2, 0) of TEXT_ALIGN_CHA_RIGHT: child.offset.x += max(width - child.size.w - child.margin.right, 0) of TEXT_ALIGN_CHA_LEFT: discard # default else: discard # Build an outer block box inside an existing block formatting context. proc layoutBlockChild(bctx: var BlockContext, builder: BoxBuilder, space: AvailableSpace, offset: Offset): BlockBox = let child = case builder.computed{"display"} of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT: bctx.buildBlock(BlockBoxBuilder(builder), space, offset) of DISPLAY_LIST_ITEM: bctx.buildListItem(ListItemBoxBuilder(builder), space, offset) of DISPLAY_TABLE: bctx.buildTable(TableBoxBuilder(builder), space, offset) else: assert false, "builder.t is " & $builder.computed{"display"} BlockBox(nil) return child # Establish a new block formatting context and build a block box. proc layoutRootBlock(lctx: LayoutState, builder: BoxBuilder, space: AvailableSpace, offset: Offset, marginBottomOut: var LayoutUnit): BlockBox = var bctx = BlockContext(lctx: lctx) let box = case builder.computed{"display"} of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT: bctx.buildBlock(BlockBoxBuilder(builder), space, offset) of DISPLAY_LIST_ITEM: bctx.buildListItem(ListItemBoxBuilder(builder), space, offset) of DISPLAY_TABLE: bctx.buildTable(TableBoxBuilder(builder), space, offset) else: assert false, "builder.t is " & $builder.computed{"display"} BlockBox(nil) bctx.positionFloats() 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.offset.y + box.size.h + marginBottomOut: box.size.h = bctx.maxFloatHeight - box.offset.y - marginBottomOut return box proc initBlockPositionStates(state: var BlockState, bctx: var BlockContext, box: BlockBox) = let prevBps = bctx.ancestorsHead bctx.ancestorsHead = BlockPositionState( box: box, offset: Offset( x: state.offset.x, y: state.offset.y ), resolved: bctx.parentBps == nil ) if prevBps != nil: prevBps.next = bctx.ancestorsHead if bctx.parentBps != nil: bctx.ancestorsHead.offset.x += bctx.parentBps.offset.x bctx.ancestorsHead.offset.y += bctx.parentBps.offset.y # 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.x += box.offset.x bctx.ancestorsHead.offset.y += box.offset.y 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 # 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, children: seq[BoxBuilder]) = for builder in children: var dy: LayoutUnit = 0 # delta var child: BlockBox let isfloat = builder.computed{"float"} != FLOAT_NONE let isinflow = builder.computed{"position"} != POSITION_ABSOLUTE and not isfloat if builder.computed.establishesBFC(): var marginBottomOut: LayoutUnit child = bctx.lctx.layoutRootBlock(builder, state.space, state.offset, marginBottomOut) # Do not collapse margins of elements that do not participate in # the flow. if isinflow: bctx.marginTodo.append(child.margin.top) bctx.flushMargins(child) bctx.positionFloats() bctx.marginTodo.append(child.margin.bottom) else: child.offset.y += child.margin.top if state.isParentResolved(bctx): # If parent offset has been resolved, use marginTodo in this # float's initial offset. child.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. dy = child.offset.y - state.offset.y + child.size.h + marginBottomOut else: child = bctx.layoutBlockChild(builder, state.space, state.offset) # delta y is difference between old and new offsets (margin-top), # plus height. dy = child.offset.y - state.offset.y + child.size.h let childWidth = child.margin.left + child.size.w + child.margin.right state.maxChildWidth = max(state.maxChildWidth, childWidth) state.xminwidth = max(state.xminwidth, child.xminwidth) if child.computed{"position"} != POSITION_ABSOLUTE and not isfloat: # Not absolute, and not a float. state.offset.y += dy elif isfloat: if state.space.w.t == FIT_CONTENT: # 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 continue # 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 )) state.nested.add(child) # 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 ) # Set ancestorsHead to a dummy object. Rationale: see below. # Also set ancestorsHead as the dummy object, so next elements are # chained to that. bctx.ancestorsHead = bctx.marginTarget state.nested.setLen(0) bctx.exclusions.setLen(state.oldExclusionsLen) state.offset = Offset(x: sizes.padding.left, y: sizes.padding.top) box.applyWidth(sizes, state.maxChildWidth) state.space.w = stretch(box.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: LayoutState) = for child in state.nested: if child.computed{"position"} != POSITION_ABSOLUTE: box.postAlignChild(child, box.size.w) case child.computed{"position"} of POSITION_RELATIVE: box.positionRelative(child, lctx) of POSITION_ABSOLUTE: lctx.positionAbsolute(child, child.margin) else: discard #TODO proc layoutBlock(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder, sizes: ResolvedSizes) = let lctx = bctx.lctx let positioned = box.computed{"position"} notin { POSITION_STATIC, POSITION_FIXED, POSITION_STICKY } 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, builder.children) if state.needsReLayout: state.initReLayout(bctx, box, sizes) state.layoutBlockChildren(bctx, builder.children) if state.nested.len > 0: let lastNested = state.nested[^1] box.baseline = lastNested.offset.y + lastNested.baseline # Apply width then move the inline offset of children that still need # further relative positioning. box.applyWidth(sizes, state.maxChildWidth) 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.nested = state.nested box.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) was still set as a marginTarget, it no # longer is. bctx.positionFloats() bctx.marginTarget = nil # Reset parentBps to the previous node. bctx.parentBps = state.prevParentBps if positioned: lctx.positioned.setLen(lctx.positioned.len - 1) # Tree generation (1st pass) proc newMarkerBox(computed: CSSComputedValues, listItemCounter: int): MarkerBoxBuilder = let computed = computed.inheritProperties() computed{"display"} = DISPLAY_INLINE # Use pre, so the space at the end of the default markers isn't ignored. computed{"white-space"} = WHITESPACE_PRE return MarkerBoxBuilder( computed: computed, text: @[computed{"list-style-type"}.listMarker(listItemCounter)] ) type BlockGroup = object parent: BlockBoxBuilder boxes: seq[BoxBuilder] type InnerBlockContext = object styledNode: StyledNode blockgroup: BlockGroup lctx: LayoutState ibox: InlineBoxBuilder iroot: InlineBoxBuilder anonRow: TableRowBoxBuilder anonTable: TableBoxBuilder quoteLevel: int listItemCounter: int listItemReset: bool parent: ptr InnerBlockContext inlineStack: seq[StyledNode] proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} = assert box.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_BLOCK}, $box.computed{"display"} blockgroup.boxes.add(box) proc flush(blockgroup: var BlockGroup) {.inline.} = if blockgroup.boxes.len > 0: assert blockgroup.parent.computed{"display"} != DISPLAY_INLINE let computed = blockgroup.parent.computed.inheritProperties() computed{"display"} = DISPLAY_BLOCK let bbox = BlockBoxBuilder(computed: computed) bbox.inlinelayout = true bbox.children = blockgroup.boxes blockgroup.parent.children.add(bbox) blockgroup.boxes.setLen(0) # Don't generate empty anonymous inline blocks between block boxes func canGenerateAnonymousInline(blockgroup: BlockGroup, computed: CSSComputedValues, str: string): bool = return blockgroup.boxes.len > 0 and blockgroup.boxes[^1].computed{"display"} == DISPLAY_INLINE or computed.whitespacepre or not str.onlyWhitespace() proc newBlockGroup(parent: BlockBoxBuilder): BlockGroup = assert parent.computed{"display"} != DISPLAY_INLINE result.parent = parent proc generateTableBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableBoxBuilder proc generateTableRowGroupBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableRowGroupBoxBuilder proc generateTableRowBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableRowBoxBuilder proc generateTableCellBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableCellBoxBuilder proc generateTableCaptionBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableCaptionBoxBuilder proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState, marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil): BlockBoxBuilder proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) proc generateBlockBox(pctx: var InnerBlockContext, styledNode: StyledNode, marker = none(MarkerBoxBuilder)): BlockBoxBuilder = return generateBlockBox(styledNode, pctx.lctx, marker, addr pctx) proc flushTableRow(ctx: var InnerBlockContext) = if ctx.anonRow != nil: if ctx.blockgroup.parent.computed{"display"} == DISPLAY_TABLE_ROW: ctx.blockgroup.parent.children.add(ctx.anonRow) else: if ctx.anonTable == nil: var wrappervals = ctx.styledNode.computed.inheritProperties() wrappervals{"display"} = DISPLAY_TABLE ctx.anonTable = TableBoxBuilder(computed: wrappervals) ctx.anonTable.children.add(ctx.anonRow) ctx.anonRow = nil proc flushTable(ctx: var InnerBlockContext) = ctx.flushTableRow() if ctx.anonTable != nil: ctx.blockgroup.parent.children.add(ctx.anonTable) proc iflush(ctx: var InnerBlockContext) = if ctx.iroot != nil: assert ctx.iroot.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE} ctx.blockgroup.add(ctx.iroot) ctx.iroot = nil ctx.ibox = nil proc bflush(ctx: var InnerBlockContext) = ctx.iflush() ctx.blockgroup.flush() 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.blockgroup.flush() ctx.flushTableRow() ctx.flushTable() ctx.flushInherit() proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) = let box = ctx.blockgroup.parent case styledNode.computed{"display"} of DISPLAY_BLOCK, DISPLAY_FLOW_ROOT: ctx.iflush() ctx.flush() let childbox = ctx.generateBlockBox(styledNode) box.children.add(childbox) of DISPLAY_LIST_ITEM: ctx.flush() inc ctx.listItemCounter let childbox = ListItemBoxBuilder( computed: styledNode.computed, marker: newMarkerBox(styledNode.computed, ctx.listItemCounter) ) if childbox.computed{"list-style-position"} == LIST_STYLE_POSITION_INSIDE: childbox.content = ctx.generateBlockBox(styledNode, some(childbox.marker)) childbox.marker = nil else: childbox.content = ctx.generateBlockBox(styledNode) childbox.content.computed = childbox.content.computed.copyProperties() childbox.content.computed{"display"} = DISPLAY_BLOCK box.children.add(childbox) of DISPLAY_INLINE: ctx.generateInlineBoxes(styledNode) of DISPLAY_INLINE_BLOCK: ctx.iflush() let childbox = ctx.generateBlockBox(styledNode) ctx.blockgroup.add(childbox) of DISPLAY_TABLE: ctx.flush() let childbox = styledNode.generateTableBox(ctx.lctx, ctx) box.children.add(childbox) of DISPLAY_TABLE_ROW: ctx.bflush() ctx.flushTableRow() let childbox = styledNode.generateTableRowBox(ctx.lctx, ctx) if box.computed{"display"} in ProperTableRowParent: box.children.add(childbox) else: if ctx.anonTable == nil: var wrappervals = box.computed.inheritProperties() #TODO make this an inline-table if we're in an inline context wrappervals{"display"} = DISPLAY_TABLE ctx.anonTable = TableBoxBuilder(computed: wrappervals) ctx.anonTable.children.add(childbox) of DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP, DISPLAY_TABLE_FOOTER_GROUP: ctx.bflush() ctx.flushTableRow() let childbox = styledNode.generateTableRowGroupBox(ctx.lctx, ctx) if box.computed{"display"} in {DISPLAY_TABLE, DISPLAY_INLINE_TABLE}: box.children.add(childbox) else: if ctx.anonTable == nil: var wrappervals = box.computed.inheritProperties() #TODO make this an inline-table if we're in an inline context wrappervals{"display"} = DISPLAY_TABLE ctx.anonTable = TableBoxBuilder(computed: wrappervals) ctx.anonTable.children.add(childbox) of DISPLAY_TABLE_CELL: ctx.bflush() let childbox = styledNode.generateTableCellBox(ctx.lctx, ctx) if box.computed{"display"} == DISPLAY_TABLE_ROW: box.children.add(childbox) else: if ctx.anonRow == nil: var wrappervals = box.computed.inheritProperties() wrappervals{"display"} = DISPLAY_TABLE_ROW ctx.anonRow = TableRowBoxBuilder(computed: wrappervals) ctx.anonRow.children.add(childbox) of DISPLAY_INLINE_TABLE: ctx.iflush() let childbox = styledNode.generateTableBox(ctx.lctx, ctx) ctx.blockgroup.add(childbox) of DISPLAY_TABLE_CAPTION: ctx.bflush() ctx.flushTableRow() let childbox = styledNode.generateTableCaptionBox(ctx.lctx, ctx) if box.computed{"display"} in {DISPLAY_TABLE, DISPLAY_INLINE_TABLE}: box.children.add(childbox) else: if ctx.anonTable == nil: var wrappervals = box.computed.inheritProperties() #TODO make this an inline-table if we're in an inline context wrappervals{"display"} = DISPLAY_TABLE ctx.anonTable = TableBoxBuilder(computed: wrappervals) ctx.anonTable.children.add(childbox) of DISPLAY_TABLE_COLUMN: discard #TODO of DISPLAY_TABLE_COLUMN_GROUP: discard #TODO of DISPLAY_NONE: discard proc reconstructInlineParents(ctx: var InnerBlockContext): InlineBoxBuilder = let rootNode = ctx.inlineStack[0] var parent = InlineBoxBuilder( computed: rootNode.computed, node: rootNode ) ctx.iroot = parent for i in 1 ..< ctx.inlineStack.len: let node = ctx.inlineStack[i] let nbox = InlineBoxBuilder(computed: node.computed, node: node) parent.children.add(nbox) parent = nbox return parent proc generateAnonymousInlineText(ctx: var InnerBlockContext, text: string, styledNode: StyledNode) = if ctx.iroot == nil: let computed = styledNode.computed.inheritProperties() ctx.ibox = InlineBoxBuilder(computed: computed, node: styledNode) if ctx.inlineStack.len > 0: let iparent = ctx.reconstructInlineParents() iparent.children.add(ctx.ibox) ctx.iroot = iparent else: ctx.iroot = ctx.ibox ctx.ibox.text.add(text) proc generateReplacement(ctx: var InnerBlockContext, child, parent: StyledNode) = case child.content.t of CONTENT_OPEN_QUOTE: 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.generateAnonymousInlineText(text, parent) inc ctx.quoteLevel of CONTENT_CLOSE_QUOTE: 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.generateAnonymousInlineText(text, parent) of CONTENT_NO_OPEN_QUOTE: inc ctx.quoteLevel of CONTENT_NO_CLOSE_QUOTE: if ctx.quoteLevel > 0: dec ctx.quoteLevel of CONTENT_STRING: #TODO canGenerateAnonymousInline? ctx.generateAnonymousInlineText(child.content.s, parent) of CONTENT_IMAGE: #TODO idk ctx.generateAnonymousInlineText("[img]", parent) of CONTENT_VIDEO: ctx.generateAnonymousInlineText("[video]", parent) of CONTENT_AUDIO: ctx.generateAnonymousInlineText("[audio]", parent) of CONTENT_NEWLINE: ctx.iflush() #TODO ?? # this used to set ibox (before we had iroot), now I'm not sure if we # should reconstruct here first ctx.iroot = InlineBoxBuilder(computed: parent.computed.inheritProperties()) ctx.iroot.newline = true ctx.iflush() proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) = ctx.iflush() ctx.inlineStack.add(styledNode) var lbox = ctx.reconstructInlineParents() lbox.splitType.incl(stSplitStart) ctx.ibox = lbox for child in styledNode.children: case child.t of STYLED_ELEMENT: ctx.generateFromElem(child) of STYLED_TEXT: if ctx.ibox != lbox: ctx.iflush() lbox = ctx.reconstructInlineParents() ctx.ibox = lbox lbox.text.add(child.text) of STYLED_REPLACEMENT: ctx.generateReplacement(child, styledNode) if ctx.ibox != lbox: ctx.iflush() lbox = ctx.reconstructInlineParents() ctx.ibox = lbox lbox.splitType.incl(stSplitEnd) ctx.inlineStack.setLen(ctx.inlineStack.len - 1) ctx.iflush() proc newInnerBlockContext(styledNode: StyledNode, box: BlockBoxBuilder, lctx: LayoutState, parent: ptr InnerBlockContext): InnerBlockContext = result = InnerBlockContext( styledNode: styledNode, blockgroup: newBlockGroup(box), lctx: lctx, parent: parent ) if parent != nil: result.listItemCounter = parent[].listItemCounter result.quoteLevel = parent[].quoteLevel for reset in styledNode.computed{"counter-reset"}: if reset.name == "list-item": result.listItemCounter = reset.num result.listItemReset = true proc generateInnerBlockBox(ctx: var InnerBlockContext) = let box = ctx.blockgroup.parent assert box.computed{"display"} != DISPLAY_INLINE for child in ctx.styledNode.children: case child.t of STYLED_ELEMENT: ctx.iflush() ctx.generateFromElem(child) of STYLED_TEXT: if canGenerateAnonymousInline(ctx.blockgroup, box.computed, child.text): ctx.generateAnonymousInlineText(child.text, ctx.styledNode) of STYLED_REPLACEMENT: ctx.generateReplacement(child, ctx.styledNode) ctx.iflush() proc generateBlockBox(styledNode: StyledNode, lctx: LayoutState, marker = none(MarkerBoxBuilder), parent: ptr InnerBlockContext = nil): BlockBoxBuilder = let box = BlockBoxBuilder(computed: styledNode.computed) box.node = styledNode var ctx = newInnerBlockContext(styledNode, box, lctx, parent) if marker.isSome: ctx.ibox = marker.get ctx.iflush() ctx.generateInnerBlockBox() # Flush anonymous tables here, to avoid setting inline layout with tables. ctx.flushTableRow() ctx.flushTable() # (flush here, because why not) ctx.flushInherit() # Avoid unnecessary anonymous block boxes. This also helps set our layout to # inline even if no inner anonymous block was generated. if box.children.len == 0: box.children = ctx.blockgroup.boxes box.inlinelayout = true ctx.blockgroup.boxes.setLen(0) ctx.blockgroup.flush() return box proc generateTableCellBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableCellBoxBuilder = let box = TableCellBoxBuilder(computed: styledNode.computed) var ctx = newInnerBlockContext(styledNode, box, lctx, addr parent) ctx.generateInnerBlockBox() ctx.flush() return box proc generateTableRowChildWrappers(box: TableRowBoxBuilder) = var newchildren = newSeqOfCap[BoxBuilder](box.children.len) var wrappervals = box.computed.inheritProperties() wrappervals{"display"} = DISPLAY_TABLE_CELL for child in box.children: if child.computed{"display"} == DISPLAY_TABLE_CELL: newchildren.add(child) else: let wrapper = TableCellBoxBuilder(computed: wrappervals) wrapper.children.add(child) newchildren.add(wrapper) box.children = newchildren proc generateTableRowBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableRowBoxBuilder = let box = TableRowBoxBuilder(computed: styledNode.computed) var ctx = newInnerBlockContext(styledNode, box, lctx, addr parent) ctx.generateInnerBlockBox() ctx.flush() box.generateTableRowChildWrappers() return box proc generateTableRowGroupChildWrappers(box: TableRowGroupBoxBuilder) = var newchildren = newSeqOfCap[BoxBuilder](box.children.len) var wrappervals = box.computed.inheritProperties() wrappervals{"display"} = DISPLAY_TABLE_ROW for child in box.children: if child.computed{"display"} == DISPLAY_TABLE_ROW: newchildren.add(child) else: let wrapper = TableRowBoxBuilder(computed: wrappervals) wrapper.children.add(child) wrapper.generateTableRowChildWrappers() newchildren.add(wrapper) box.children = newchildren proc generateTableRowGroupBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableRowGroupBoxBuilder = let box = TableRowGroupBoxBuilder(computed: styledNode.computed) var ctx = newInnerBlockContext(styledNode, box, lctx, addr parent) ctx.generateInnerBlockBox() ctx.flush() box.generateTableRowGroupChildWrappers() return box proc generateTableCaptionBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableCaptionBoxBuilder = let box = TableCaptionBoxBuilder(computed: styledNode.computed) var ctx = newInnerBlockContext(styledNode, box, lctx, addr parent) ctx.generateInnerBlockBox() ctx.flush() return box proc generateTableChildWrappers(box: TableBoxBuilder) = var newchildren = newSeqOfCap[BoxBuilder](box.children.len) var wrappervals = box.computed.inheritProperties() wrappervals{"display"} = DISPLAY_TABLE_ROW for child in box.children: if child.computed{"display"} in ProperTableChild: newchildren.add(child) else: let wrapper = TableRowBoxBuilder(computed: wrappervals) wrapper.children.add(child) wrapper.generateTableRowChildWrappers() newchildren.add(wrapper) box.children = newchildren proc generateTableBox(styledNode: StyledNode, lctx: LayoutState, parent: var InnerBlockContext): TableBoxBuilder = let box = TableBoxBuilder(computed: styledNode.computed, node: styledNode) var ctx = newInnerBlockContext(styledNode, box, lctx, addr parent) ctx.generateInnerBlockBox() ctx.flush() box.generateTableChildWrappers() return box proc renderLayout*(root: StyledNode, attrsp: ptr WindowAttributes): BlockBox = let space = AvailableSpace( w: stretch(attrsp[].width_px), h: stretch(attrsp[].height_px) ) let lctx = LayoutState( attrsp: attrsp, positioned: @[space] ) let builder = root.generateBlockBox(lctx) var marginBottomOut: LayoutUnit return lctx.layoutRootBlock(builder, space, Offset(), marginBottomOut)