diff options
author | bptato <nincsnevem662@gmail.com> | 2023-11-27 16:40:13 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-11-27 17:49:20 +0100 |
commit | 9fdea97d573962fe010c5e4d25c29ece7fee497d (patch) | |
tree | 1beeebc987bf0e284e65e48f59bad7182c178136 /src/layout | |
parent | ae7dfb0ae9620c9398b2f62066d73be548ad1a02 (diff) | |
download | chawan-9fdea97d573962fe010c5e4d25c29ece7fee497d.tar.gz |
layout: rewrite inline box handling
We now have real inline boxes. * Fix nesting of inline boxes * Represent inline boxes with a hierarchical RootInlineFragment -> InlineFragment tree * Get rid of inline padding hack * Get rid of ComputedFormat * Paint inline box backgrounds properly
Diffstat (limited to 'src/layout')
-rw-r--r-- | src/layout/box.nim | 57 | ||||
-rw-r--r-- | src/layout/engine.nim | 813 |
2 files changed, 475 insertions, 395 deletions
diff --git a/src/layout/box.nim b/src/layout/box.nim index 56bfb47b..6763846a 100644 --- a/src/layout/box.nim +++ b/src/layout/box.nim @@ -1,7 +1,6 @@ import css/stylednode import css/values import layout/layoutunit -import types/color type Offset* = object @@ -13,48 +12,58 @@ type h*: LayoutUnit InlineAtomType* = enum - INLINE_SPACING, INLINE_PADDING, INLINE_WORD, INLINE_BLOCK + INLINE_SPACING, INLINE_WORD, INLINE_BLOCK InlineAtom* = ref object offset*: Offset size*: Size case t*: InlineAtomType - of INLINE_SPACING, INLINE_PADDING: - sformat*: ComputedFormat + of INLINE_SPACING: + discard of INLINE_WORD: - wformat*: ComputedFormat str*: string of INLINE_BLOCK: innerbox*: BlockBox - ComputedFormat* = ref object - fontstyle*: CSSFontStyle - fontweight*: int - textdecoration*: set[CSSTextDecoration] - color*: RGBAColor - node*: StyledNode - #TODO: background color should not be stored in inline words. Instead, - # inline box fragments should be passed on to the renderer, which could - # then properly blend them. - bgcolor*: RGBAColor - LineBox* = ref object atoms*: seq[InlineAtom] offsety*: LayoutUnit size*: Size - InlineContext* = ref object + RootInlineFragment* = ref object + # offset relative to parent offset*: Offset - size*: Size - lines*: seq[LineBox] - + # root fragment + fragment*: InlineFragment # baseline of the first line box firstBaseline*: LayoutUnit # baseline of the last line box baseline*: LayoutUnit + # minimum content width + xminwidth*: LayoutUnit + size*: Size - # this is actually xminwidth. - minwidth*: LayoutUnit + InlineFragment* = ref object + # Say we have the following inline box: + # abcd + # efghij + # klm + # Then startOffset is x: 2ch, y: 1em, endOffset is x: 3ch, y: 2em, + # and size is w: 6ch, h: 3em. + # So the algorithm for painting a fragment is: + # if startOffset.y == endOffset.y: + # paint(startOffset, endOffset) + # else: + # paint(startOffset.x, 0, size.w, startOffset.y) + # paint(0, startOffset.y, size.w, endOffset.y) + # paint(0, endOffset.y, endOffset.x, size.h) + startOffset*: Offset + endOffset*: Offset + size*: Size + children*: seq[InlineFragment] + atoms*: seq[InlineAtom] + computed*: CSSComputedValues + node*: StyledNode RelativeRect* = object top*: LayoutUnit @@ -63,7 +72,7 @@ type right*: LayoutUnit BlockBox* = ref object of RootObj - inline*: InlineContext + inline*: RootInlineFragment node*: StyledNode nested*: seq[BlockBox] computed*: CSSComputedValues @@ -83,4 +92,4 @@ type baseline*: LayoutUnit ListItemBox* = ref object of BlockBox - marker*: InlineContext + marker*: RootInlineFragment diff --git a/src/layout/engine.nim b/src/layout/engine.nim index 2d581a81..e0b60a16 100644 --- a/src/layout/engine.nim +++ b/src/layout/engine.nim @@ -15,10 +15,6 @@ type attrs: WindowAttributes positioned: seq[AvailableSpace] - AvailableSpace = object - w: SizeConstraint - h: SizeConstraint - # 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 @@ -33,6 +29,10 @@ type t: SizeConstraintType u: LayoutUnit + AvailableSpace = object + w: SizeConstraint + h: SizeConstraint + ResolvedSizes = object margin: RelativeRect padding: RelativeRect @@ -54,7 +54,6 @@ func fitContent(u: LayoutUnit): SizeConstraint = type BoxBuilder = ref object of RootObj children: seq[BoxBuilder] - inlinelayout: bool computed: CSSComputedValues node: StyledNode @@ -65,6 +64,7 @@ type splitend: bool BlockBoxBuilder = ref object of BoxBuilder + inlinelayout: bool MarkerBoxBuilder = ref object of InlineBoxBuilder @@ -185,10 +185,15 @@ type 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 InlineAtomState = object vertalign: CSSVerticalAlign @@ -196,22 +201,30 @@ type marginTop: LayoutUnit marginBottom: LayoutUnit - InlineState = object - ictx: InlineContext - skip: bool - node: StyledNode - wordstate: InlineAtomState - word: InlineAtom - wrappos: int # position of last wrapping opportunity, or -1 - hasshy: bool - computed: CSSComputedValues + InlineContext = object + root: RootInlineFragment + bctx: ptr BlockContext + bfcOffset: Offset currentLine: LineBoxState - whitespacenum: int - format: ComputedFormat + hasshy: bool lctx: LayoutState - bctx: ptr BlockContext + lines: seq[LineBox] + minwidth: LayoutUnit space: AvailableSpace - bfcOffset: Offset + whitespacenum: int + 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 func whitespacepre(computed: CSSComputedValues): bool = computed{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_LINE, WHITESPACE_PRE_WRAP} @@ -222,14 +235,14 @@ func nowrap(computed: CSSComputedValues): bool = func cellwidth(lctx: LayoutState): LayoutUnit = lctx.attrs.ppc -func cellwidth(state: InlineState): LayoutUnit = - state.lctx.cellwidth +func cellwidth(ictx: InlineContext): LayoutUnit = + ictx.lctx.cellwidth func cellheight(lctx: LayoutState): LayoutUnit = lctx.attrs.ppl -func cellheight(state: InlineState): LayoutUnit = - state.lctx.cellheight +func cellheight(ictx: InlineContext): LayoutUnit = + ictx.lctx.attrs.ppl template atoms(state: LineBoxState): untyped = state.line.atoms @@ -240,79 +253,60 @@ template size(state: LineBoxState): untyped = template offsety(state: var LineBoxState): untyped = state.line.offsety -# Check if the last atom on the current line is a spacing atom, not counting -# padding atoms. -func hasLastSpacing(currentLine: LineBoxState): bool = - for i in countdown(currentLine.atoms.high, 0): - case currentLine.atoms[i].t - of INLINE_SPACING: - return true - of INLINE_PADDING: - continue # skip padding - else: - break - return false +func size(ictx: var InlineContext): var Size = + ictx.root.size # Whitespace between words -func computeShift(state: InlineState, computed: CSSComputedValues): - LayoutUnit = - if state.whitespacenum > 0: - if state.currentLine.atoms.len > 0 and - not state.currentLine.hasLastSpacing() or - computed.whitespacepre: - let spacing = computed{"word-spacing"} - if spacing.auto: - return state.cellwidth * state.whitespacenum - return spacing.px(state.lctx) * state.whitespacenum - return 0 - -proc applyLineHeight(state: var LineBoxState, lctx: LayoutState, +func computeShift(ictx: InlineContext, state: InlineState): LayoutUnit = + if ictx.whitespacenum == 0: + return 0 + if not state.computed.whitespacepre: + if ictx.currentLine.atoms.len == 0 or + ictx.currentLine.atoms[^1].t == INLINE_SPACING: + return 0 + let spacing = state.computed{"word-spacing"} + if spacing.auto: + return ictx.cellwidth * ictx.whitespacenum + return spacing.px(ictx.lctx) * 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 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) -func getComputedFormat(computed: CSSComputedValues, node: StyledNode): - ComputedFormat = - return ComputedFormat( - color: computed{"color"}, - fontstyle: computed{"font-style"}, - fontweight: computed{"font-weight"}, - textdecoration: computed{"text-decoration"}, - bgcolor: computed{"background-color"}, - node: node - ) - -proc newWord(state: var InlineState) = - let wformat = getComputedFormat(state.computed, state.node) - state.format = wformat - state.word = InlineAtom( +proc newWord(ictx: var InlineContext, state: var InlineState) = + ictx.word = InlineAtom( t: INLINE_WORD, - wformat: wformat, - size: Size(h: state.cellheight) + size: Size(h: ictx.cellheight) ) - state.wordstate = InlineAtomState( + ictx.wordstate = InlineAtomState( vertalign: state.computed{"vertical-align"}, - baseline: state.cellheight + baseline: ictx.cellheight ) - state.wrappos = -1 - state.hasshy = false + ictx.wrappos = -1 + ictx.hasshy = false -proc horizontalAlignLine(state: var InlineState, line: var LineBox, - computed: CSSComputedValues, last = false) = - let width = case state.space.w.t +proc horizontalAlignLine(ictx: var InlineContext, state: InlineState, + line: var LineBox, last = false) = + let width = case ictx.space.w.t of MIN_CONTENT, MAX_CONTENT: - state.ictx.size.w + ictx.size.w of FIT_CONTENT: - min(state.ictx.size.w, state.space.w.u) + min(ictx.size.w, ictx.space.w.u) of STRETCH: - max(state.ictx.size.w, state.space.w.u) + max(ictx.size.w, ictx.space.w.u) # we don't support directions for now so left = start and right = end - case computed{"text-align"} + case state.computed{"text-align"} of TEXT_ALIGN_START, TEXT_ALIGN_LEFT, TEXT_ALIGN_CHA_LEFT: discard of TEXT_ALIGN_END, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CHA_RIGHT: @@ -320,20 +314,20 @@ proc horizontalAlignLine(state: var InlineState, line: var LineBox, let x = max(width, line.size.w) - line.size.w for atom in line.atoms.mitems: atom.offset.x += x - state.ictx.size.w = max(atom.offset.x + atom.size.w, state.ictx.size.w) + 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 - state.ictx.size.w = max(atom.offset.x + atom.size.w, state.ictx.size.w) + ictx.size.w = max(atom.offset.x + atom.size.w, ictx.size.w) of TEXT_ALIGN_JUSTIFY: - if not computed.whitespacepre and not last: + if not state.computed.whitespacepre and not last: var sumwidth: LayoutUnit = 0 var spaces = 0 for atom in line.atoms.mitems: - if atom.t in {INLINE_SPACING, INLINE_PADDING}: + if atom.t == INLINE_SPACING: discard else: inc spaces @@ -347,26 +341,27 @@ proc horizontalAlignLine(state: var InlineState, line: var LineBox, if atom.t == INLINE_SPACING: atom.size.w = spacingwidth line.size.w += atom.size.w - state.ictx.size.w = max(width, state.ictx.size.w) #TODO this seems meaningless? + # 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(state: var InlineState) = +proc verticalAlignLine(ictx: var InlineContext) = # Start with line-height as the baseline and line height. - let lineheight = state.currentLine.lineheight - state.currentLine.size.h = lineheight + 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 in 0 ..< state.currentLine.atoms.len: - let atom = state.currentLine.atoms[i] - let iastate = state.currentLine.atomstates[i] + for i in 0 ..< ictx.currentLine.atoms.len: + 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(state.lctx, lineheight) + 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) @@ -378,40 +373,40 @@ proc verticalAlignLine(state: var InlineState) = # 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 in 0 ..< state.currentLine.atoms.len: - let atom = state.currentLine.atoms[i] - let iastate = state.currentLine.atomstates[i] + for i in 0 ..< ictx.currentLine.atoms.len: + 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.) - state.currentLine.size.h = max(state.currentLine.size.h, atom.size.h) + 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(state.lctx, lineheight) - state.currentLine.size.h = max(baseline - iastate.baseline + - atom.size.h + len, state.currentLine.size.h) + 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). - state.currentLine.size.h = max(baseline + atom.size.h div 2, - state.currentLine.size.h) + 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). - state.currentLine.size.h = max(baseline - iastate.baseline + - atom.size.h, state.currentLine.size.h) + 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 in 0 ..< state.currentLine.atoms.len: - let iastate = state.currentLine.atomstates[i] - let atom = addr state.currentLine.atoms[i] + for i in 0 ..< ictx.currentLine.atoms.len: + let iastate = ictx.currentLine.atomstates[i] + let atom = addr ictx.currentLine.atoms[i] case iastate.vertalign.keyword of VERTICAL_ALIGN_BASELINE: # Atom is placed at (line baseline) - (atom baseline) - len - let len = iastate.vertalign.length.px(state.lctx, lineheight) + 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) @@ -421,7 +416,7 @@ proc verticalAlignLine(state: var InlineState) = atom.offset.y = 0 of VERTICAL_ALIGN_BOTTOM: # Atom is placed at the bottom of the line. - atom.offset.y = state.currentLine.size.h - atom.size.h + atom.offset.y = ictx.currentLine.size.h - atom.size.h else: # See baseline (with len = 0). atom.offset.y = baseline - iastate.baseline @@ -433,35 +428,22 @@ proc verticalAlignLine(state: var InlineState) = bottomEdge = max(atom.offset.y + atom.size.h + iastate.marginBottom, bottomEdge) - # Finally, offset all atoms' y position by the largest top margin. - for atom in state.currentLine.atoms: + # Finally, offset all atoms' y position by the largest top margin and the + # line box's top padding. + for atom in ictx.currentLine.atoms: atom.offset.y += marginTop + atom.offset.y += ictx.currentLine.paddingTop # Set the line height to new top edge + old bottom edge, and set the # baseline. - state.currentLine.size.h = bottomEdge + marginTop - state.currentLine.baseline = baseline - -proc addPadding(state: var InlineState, width, height: LayoutUnit, - format: ComputedFormat) = - state.currentLine.size.w += width - let padding = InlineAtom( - t: INLINE_PADDING, - size: Size( - w: width, - h: height - ), - sformat: format, - offset: Offset(x: state.currentLine.size.w) - ) - let iastate = InlineAtomState( - baseline: height - #TODO vertalign? - ) - state.currentLine.atomstates.add(iastate) - state.currentLine.atoms.add(padding) - -proc addSpacing(state: var InlineState, width, height: LayoutUnit, + 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 + ictx.currentLine.baseline += ictx.currentLine.paddingTop + +proc addSpacing(ictx: var InlineContext, width, height: LayoutUnit, hang = false) = let spacing = InlineAtom( t: INLINE_SPACING, @@ -469,8 +451,7 @@ proc addSpacing(state: var InlineState, width, height: LayoutUnit, w: width, h: height ), - sformat: state.format, - offset: Offset(x: state.currentLine.size.w) + offset: Offset(x: ictx.currentLine.size.w) ) let iastate = InlineAtomState( baseline: height @@ -479,266 +460,273 @@ proc addSpacing(state: var InlineState, width, height: LayoutUnit, 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. - state.currentLine.size.w += width - state.currentLine.atomstates.add(iastate) - state.currentLine.atoms.add(spacing) + ictx.currentLine.size.w += width + ictx.currentLine.atomstates.add(iastate) + ictx.currentLine.atoms.add(spacing) + ictx.whitespaceFragment.atoms.add(spacing) -proc flushWhitespace(state: var InlineState, computed: CSSComputedValues, +proc flushWhitespace(ictx: var InlineContext, state: InlineState, hang = false) = - let shift = state.computeShift(computed) - state.currentLine.charwidth += state.whitespacenum - state.whitespacenum = 0 + let shift = ictx.computeShift(state) + ictx.currentLine.charwidth += ictx.whitespacenum + ictx.whitespacenum = 0 if shift > 0: - state.addSpacing(shift, state.cellheight, hang) + 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(state: var InlineState) = - state.currentLine.availableWidth = state.space.w.u - let bctx = state.bctx +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 = state.bfcOffset - let y = state.currentLine.line.offsety + bfcOffset.y + let bfcOffset = ictx.bfcOffset + let y = ictx.currentLine.line.offsety + bfcOffset.y var left = bfcOffset.x - var right = bfcOffset.x + state.currentLine.availableWidth + 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: - state.currentLine.hasExclusion = true + ictx.currentLine.hasExclusion = true if ex.t == FLOAT_LEFT: left = ex.offset.x + ex.size.w else: right = ex.offset.x - state.currentLine.line.size.w = left - bfcOffset.x - state.currentLine.availableWidth = right - bfcOffset.x + ictx.currentLine.line.size.w = left - bfcOffset.x + ictx.currentLine.availableWidth = right - bfcOffset.x -proc finishLine(state: var InlineState, computed: CSSComputedValues, - wrap: bool, force = false) = - if state.currentLine.atoms.len != 0 or force: - let whitespace = computed{"white-space"} +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: - state.flushWhitespace(computed) + ictx.flushWhitespace(state) elif whitespace == WHITESPACE_PRE_WRAP: - state.flushWhitespace(computed, hang = true) + ictx.flushWhitespace(state, hang = true) else: - state.whitespacenum = 0 - state.verticalAlignLine() + ictx.whitespacenum = 0 + ictx.verticalAlignLine() # add line to ictx - let y = state.currentLine.offsety + 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 state.ictx.lines.len == 0: - state.ictx.firstBaseline = y + state.currentLine.baseline - state.ictx.baseline = y + state.currentLine.baseline - state.ictx.size.h += state.currentLine.size.h + 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: - state.currentLine.availableWidth + 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.currentLine.size.w - state.ictx.size.w = max(state.ictx.size.w, lineWidth) - state.ictx.lines.add(state.currentLine.line) - state.currentLine = LineBoxState( - line: LineBox(offsety: y + state.currentLine.size.h) + 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( + line: LineBox(offsety: y + ictx.currentLine.size.h) ) - state.initLine() + ictx.initLine() -proc finish(state: var InlineState, computed: CSSComputedValues) = - state.finishLine(computed, wrap = false) - if state.ictx.lines.len > 0: - for i in 0 ..< state.ictx.lines.len - 1: - state.horizontalAlignLine(state.ictx.lines[i], computed, last = false) - state.horizontalAlignLine(state.ictx.lines[^1], computed, last = true) +proc finish(ictx: var InlineContext, state: var InlineState) = + ictx.finishLine(state, wrap = false) + if ictx.lines.len > 0: + for i in 0 ..< ictx.lines.len - 1: + ictx.horizontalAlignLine(state, ictx.lines[i], last = false) + ictx.horizontalAlignLine(state, ictx.lines[^1], last = true) func minwidth(atom: InlineAtom): LayoutUnit = if atom.t == INLINE_BLOCK: return atom.innerbox.xminwidth return atom.size.w -func shouldWrap(state: InlineState, w: LayoutUnit, +func shouldWrap(ictx: InlineContext, w: LayoutUnit, pcomputed: CSSComputedValues): bool = if pcomputed != nil and pcomputed.nowrap: return false - if state.space.w.t == MAX_CONTENT: + if ictx.space.w.t == MAX_CONTENT: return false # no wrap with max-content - if state.space.w.t == MIN_CONTENT: + if ictx.space.w.t == MIN_CONTENT: return true # always wrap with min-content - return state.currentLine.size.w + w > state.currentLine.availableWidth + return ictx.currentLine.size.w + w > ictx.currentLine.availableWidth -func shouldWrap2(state: InlineState, w: LayoutUnit): bool = - if not state.currentLine.hasExclusion: +func shouldWrap2(ictx: InlineContext, w: LayoutUnit): bool = + if not ictx.currentLine.hasExclusion: return false - return state.currentLine.size.w + w > state.currentLine.availableWidth + return ictx.currentLine.size.w + w > ictx.currentLine.availableWidth # Start a new line, even if the previous one is empty -proc flushLine(state: var InlineState, computed: CSSComputedValues) = - state.currentLine.applyLineHeight(state.lctx, computed) - state.finishLine(computed, wrap = false, force = true) +proc flushLine(ictx: var InlineContext, state: var InlineState) = + ictx.applyLineHeight(ictx.currentLine, state.computed) + ictx.finishLine(state, wrap = false, force = true) -# pcomputed: computed values of parent, for white-space: pre, line-height. -# This isn't necessarily the computed of ictx (e.g. they may differ for nested -# inline boxes.) +# Add an inline atom atom, with state iastate. # Returns true on newline. -proc addAtom(state: var InlineState, iastate: InlineAtomState, - atom: InlineAtom, pcomputed: CSSComputedValues): bool = +proc addAtom(ictx: var InlineContext, state: var InlineState, + iastate: InlineAtomState, atom: InlineAtom): bool = result = false - var shift = state.computeShift(pcomputed) - state.whitespacenum = 0 + var shift = ictx.computeShift(state) + ictx.whitespacenum = 0 # Line wrapping - if state.shouldWrap(atom.size.w + shift, pcomputed): - state.finishLine(pcomputed, wrap = true, force = false) + if ictx.shouldWrap(atom.size.w + shift, state.computed): + ictx.finishLine(state, wrap = true, force = false) result = true # Recompute on newline - shift = state.computeShift(pcomputed) + shift = ictx.computeShift(state) # For floats: flush lines until we can place the atom. #TODO this is inefficient - while state.shouldWrap2(atom.size.w + shift): - state.flushLine(pcomputed) + while ictx.shouldWrap2(atom.size.w + shift): + ictx.flushLine(state) # Recompute on newline - shift = state.computeShift(pcomputed) + shift = ictx.computeShift(state) if atom.size.w > 0 and atom.size.h > 0: if shift > 0: - state.addSpacing(shift, state.cellheight) - state.ictx.minwidth = max(state.ictx.minwidth, atom.minwidth) - state.currentLine.applyLineHeight(state.lctx, pcomputed) + ictx.addSpacing(shift, ictx.cellheight) + ictx.minwidth = max(ictx.minwidth, atom.minwidth) + ictx.applyLineHeight(ictx.currentLine, state.computed) if atom.t == INLINE_WORD: - state.format = atom.wformat - else: - state.currentLine.charwidth = 0 - state.format = nil - state.currentLine.atoms.add(atom) - state.currentLine.atomstates.add(iastate) - state.currentLine.atoms[^1].offset.x += state.currentLine.size.w - state.currentLine.size.w += atom.size.w - -proc addWord(state: var InlineState): bool = + 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 state.word.str != "": - state.word.str.mnormalize() #TODO this may break on EOL. - result = state.addAtom(state.wordstate, state.word, state.computed) - state.newWord() + 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(state: var InlineState): bool = +proc addWordEOL(ictx: var InlineContext, state: var InlineState): bool = result = false - if state.word.str != "": - if state.wrappos != -1: - let leftstr = state.word.str.substr(state.wrappos) - state.word.str.setLen(state.wrappos) - if state.hasshy: + 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 - state.word.str &= shy - state.hasshy = false - result = state.addWord() - state.word.str = leftstr - state.word.size.w = leftstr.width() * state.cellwidth + 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 = state.addWord() + result = ictx.addWord(state) -proc checkWrap(state: var InlineState, r: Rune) = +proc checkWrap(ictx: var InlineContext, state: var InlineState, r: Rune) = if state.computed.nowrap: return - let shift = state.computeShift(state.computed) + let shift = ictx.computeShift(state) let rw = r.width() case state.computed{"word-break"} of WORD_BREAK_NORMAL: - if rw == 2 or state.wrappos != -1: # break on cjk and wrap opportunities - let plusWidth = state.word.size.w + shift + rw * state.cellwidth - if state.shouldWrap(plusWidth, nil): - if not state.addWordEOL(): # no line wrapping occured in addAtom - state.finishLine(state.computed, wrap = true) - state.whitespacenum = 0 + 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 = state.word.size.w + shift + rw * state.cellwidth - if state.shouldWrap(plusWidth, nil): - if not state.addWordEOL(): # no line wrapping occured in addAtom - state.finishLine(state.computed, wrap = true) - state.whitespacenum = 0 + 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 = state.word.size.w + shift + rw * state.cellwidth - if state.shouldWrap(plusWidth, nil): - state.finishLine(state.computed, wrap = true) - state.whitespacenum = 0 - -proc processWhitespace(state: var InlineState, c: char) = - discard state.addWord() + 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: - state.whitespacenum = max(state.whitespacenum, 1) + if ictx.whitespacenum < 1: + ictx.whitespacenum = 1 + ictx.whitespaceFragment = state.fragment of WHITESPACE_PRE_LINE: if c == '\n': - state.flushLine(state.computed) - else: - state.whitespacenum = max(state.whitespacenum, 1) + ictx.flushLine(state) + elif ictx.whitespacenum < 1: + 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.) if c == '\n': - state.flushLine(state.computed) + ictx.flushLine(state) elif c == '\t': - let prev = state.currentLine.charwidth - state.currentLine.charwidth = ((state.currentLine.charwidth + - state.whitespacenum) div 8 + 1) * 8 - state.whitespacenum - state.whitespacenum += state.currentLine.charwidth - prev + let prev = ictx.currentLine.charwidth + ictx.currentLine.charwidth = ((ictx.currentLine.charwidth + + ictx.whitespacenum) div 8 + 1) * 8 - ictx.whitespacenum + ictx.whitespacenum += ictx.currentLine.charwidth - prev + ictx.whitespaceFragment = state.fragment else: - inc state.whitespacenum + inc ictx.whitespacenum + ictx.whitespaceFragment = state.fragment -func newInlineState(bctx: var BlockContext, space: AvailableSpace, - offset, bfcOffset: Offset, computed: CSSComputedValues): InlineState = - var state = InlineState( +func newInlineContext(bctx: var BlockContext, space: AvailableSpace, + bfcOffset: Offset, root: RootInlineFragment): InlineContext = + var ictx = InlineContext( currentLine: LineBoxState( line: LineBox() ), - ictx: InlineContext( - offset: offset, - ), bctx: addr bctx, lctx: bctx.lctx, bfcOffset: bfcOffset, - space: space + space: space, + root: root ) - state.initLine() - return state - -proc layoutText(state: var InlineState, str: string, - computed: CSSComputedValues, node: StyledNode) = - #TODO the lifetime of these is somewhat confusing, maybe move into some - # other object? - state.computed = computed - state.node = node - - state.flushWhitespace(state.computed) - state.newWord() + ictx.initLine() + return ictx + +proc layoutText(ictx: var InlineContext, state: var InlineState, str: string) = + ictx.flushWhitespace(state) + ictx.newWord(state) var i = 0 while i < str.len: let c = str[i] if c in Ascii: if c in AsciiWhitespace: - state.processWhitespace(c) + ictx.processWhitespace(state, c) else: let r = Rune(c) - state.checkWrap(r) - state.word.str &= c + ictx.checkWrap(state, r) + ictx.word.str &= c let w = r.width() - state.word.size.w += w * state.cellwidth - state.currentLine.charwidth += w + ictx.word.size.w += w * ictx.cellwidth + ictx.currentLine.charwidth += w if c == '-': # ascii dash - state.wrappos = state.word.str.len - state.hasshy = false + ictx.wrappos = ictx.word.str.len + ictx.hasshy = false inc i else: var r: Rune fastRuneAt(str, i, r) - state.checkWrap(r) + ictx.checkWrap(state, r) if r == Rune(0xAD): # soft hyphen - state.wrappos = state.word.str.len - state.hasshy = true + ictx.wrappos = ictx.word.str.len + ictx.hasshy = true else: - state.word.str &= r + ictx.word.str &= r let w = r.width() - state.word.size.w += w * state.cellwidth - state.currentLine.charwidth += w - discard state.addWord() + 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 func spx(l: CSSLength, lctx: LayoutState, p: SizeConstraint, computed: CSSComputedValues, padding: LayoutUnit): LayoutUnit = @@ -1078,9 +1066,9 @@ proc append(a: var Strut, b: LayoutUnit) = func sum(a: Strut): LayoutUnit = return a.pos + a.neg -proc buildInlines(bctx: var BlockContext, inlines: seq[BoxBuilder], - sizes: ResolvedSizes, computed: CSSComputedValues, offset, - bfcOffset: Offset): InlineContext +proc layoutRootInline(bctx: var BlockContext, inlines: seq[BoxBuilder], + space: AvailableSpace, computed: CSSComputedValues, offset, + bfcOffset: Offset): RootInlineFragment proc buildBlockLayout(bctx: var BlockContext, box: BlockBox, builder: BlockBoxBuilder, sizes: ResolvedSizes) proc buildTableLayout(lctx: LayoutState, table: BlockBox, @@ -1118,15 +1106,15 @@ proc buildInlineLayout(bctx: var BlockContext, box: BlockBox, 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.buildInlines(children, sizes, box.computed, offset, - bfcOffset) - box.xminwidth = max(box.xminwidth, box.inline.minwidth) + 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 = box.inline.offset.y + box.inline.baseline - box.firstBaseline = box.inline.offset.y + box.inline.firstBaseline + box.baseline = offset.y + box.inline.baseline + box.firstBaseline = offset.y + box.inline.firstBaseline const DisplayBlockLike = {DISPLAY_BLOCK, DISPLAY_LIST_ITEM, DISPLAY_INLINE_BLOCK} @@ -1285,9 +1273,9 @@ func toperc100(sc: SizeConstraint): Option[LayoutUnit] = return none(LayoutUnit) # parentWidth, parentHeight: width/height of the containing block. -proc addInlineBlock(state: var InlineState, builder: BlockBoxBuilder, - parentWidth, parentHeight: SizeConstraint, computed: CSSComputedValues) = - let lctx = state.lctx +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) @@ -1331,76 +1319,136 @@ proc addInlineBlock(state: var InlineState, builder: BlockBoxBuilder, marginTop: marginTop, marginBottom: bctx.marginTodo.sum() ) - discard state.addAtom(iastate, iblock, computed) + discard ictx.addAtom(state, iastate, iblock) + ictx.whitespacenum = 0 -proc buildInline(state: var InlineState, box: InlineBoxBuilder) = - let lctx = state.lctx +proc layoutInline(ictx: var InlineContext, box: InlineBoxBuilder): + InlineFragment = + let lctx = ictx.lctx + let fragment = InlineFragment(computed: box.computed, node: box.node) + if box.splitstart: + 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.line.offsety + ) + ) if box.newline: - state.flushLine(box.computed) - - let paddingformat = getComputedFormat(box.computed, box.node) + ictx.flushLine(state) if box.splitstart: - let margin_left = box.computed{"margin-left"}.px(lctx, state.space.w) - state.currentLine.size.w += margin_left - - let padding_left = box.computed{"padding-left"}.px(lctx, state.space.w) - if padding_left > 0: - # We must add spacing to the line to make sure that it is formatted - # (i.e. colored) appropriately. - # We need this so long as we have no proper inline boxes. - state.addPadding(padding_left, state.cellheight, paddingformat) + 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: - state.layoutText(text, box.computed, box.node) + ictx.layoutText(state, text) for child in box.children: case child.computed{"display"} of DISPLAY_INLINE: - let child = InlineBoxBuilder(child) - state.buildInline(child) + 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(state.space.w) - let h = state.space.h - state.addInlineBlock(child, w, h, box.computed) - state.whitespacenum = 0 + 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 box.splitend: - let padding_right = box.computed{"padding-right"}.px(lctx, state.space.w) - state.currentLine.size.w += padding_right - if padding_right > 0: - let height = max(state.currentLine.size.h, 1) - state.addPadding(padding_right, height, paddingformat) - let margin_right = box.computed{"margin-right"}.px(lctx, state.space.w) - state.currentLine.size.w += margin_right - -proc buildInlines(bctx: var BlockContext, inlines: seq[BoxBuilder], - sizes: ResolvedSizes, computed: CSSComputedValues, offset, - bfcOffset: Offset): InlineContext = - var state = bctx.newInlineState(sizes.space, offset, bfcOffset, computed) - if inlines.len > 0: - for child in inlines: - case child.computed{"display"} - of DISPLAY_INLINE: - let child = InlineBoxBuilder(child) - state.buildInline(child) - of DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE: - #TODO wtf - let child = BlockBoxBuilder(child) - let w = fitContent(state.space.w) - let h = state.space.h - state.addInlineBlock(child, w, h, computed) - state.whitespacenum = 0 - else: - assert false, "child.t is " & $child.computed{"display"} - state.finish(computed) - return state.ictx + 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 + +# Calculate the average division error for the placement of each line box. +# This is then used to evenly distribute error along line boxes. +func calculateErrorY(ictx: InlineContext): LayoutUnit = + if ictx.lines.len <= 1: return 0 + var error: LayoutUnit = 0 + for i in 0 ..< ictx.lines.high: + let dy = toInt(ictx.lines[i + 1].offsety - ictx.lines[i].offsety) + error += dy - (dy div ictx.cellheight) * ictx.cellheight + return error div ictx.lines.high + +proc positionAtoms(ictx: var InlineContext) = + let erry = ictx.calculateErrorY() + var i = 0 + for line in ictx.lines: + let erry0 = erry * i + for atom in line.atoms: + atom.offset.y += line.offsety - erry0 + inc i + +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.newInlineContext(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 + ictx.positionAtoms() + return root proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace, - lctx: LayoutState): InlineContext = + lctx: LayoutState): RootInlineFragment = let space = AvailableSpace( w: fitContent(space.w), h: space.h @@ -1408,10 +1456,9 @@ proc buildMarker(builder: MarkerBoxBuilder, space: AvailableSpace, #TODO we should put markers right before the first atom of the parent # list item or something... var bctx = BlockContext(lctx: lctx) - var state = bctx.newInlineState(space, Offset(), Offset(), builder.computed) - state.buildInline(builder) - state.finish(builder.computed) - return state.ictx + 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, @@ -2281,7 +2328,7 @@ proc getBlockBox(computed: CSSComputedValues): BlockBoxBuilder = return BlockBoxBuilder(computed: computed) proc getTextBox(computed: CSSComputedValues): InlineBoxBuilder = - return InlineBoxBuilder(inlinelayout: true, computed: computed) + return InlineBoxBuilder(computed: computed) proc getMarkerBox(computed: CSSComputedValues, listItemCounter: int): MarkerBoxBuilder = @@ -2290,7 +2337,6 @@ proc getMarkerBox(computed: CSSComputedValues, listItemCounter: int): # Use pre, so the space at the end of the default markers isn't ignored. computed{"white-space"} = WHITESPACE_PRE return MarkerBoxBuilder( - inlinelayout: true, computed: computed, text: @[computed{"list-style-type"}.listMarker(listItemCounter)] ) @@ -2338,12 +2384,14 @@ type InnerBlockContext = object 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, @@ -2409,10 +2457,11 @@ proc flushTable(ctx: var InnerBlockContext) = ctx.blockgroup.parent.children.add(ctx.anonTable) proc iflush(ctx: var InnerBlockContext) = - if ctx.ibox != nil: - assert ctx.ibox.computed{"display"} in {DISPLAY_INLINE, + if ctx.iroot != nil: + assert ctx.iroot.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE} - ctx.blockgroup.add(ctx.ibox) + ctx.blockgroup.add(ctx.iroot) + ctx.iroot = nil ctx.ibox = nil proc bflush(ctx: var InnerBlockContext) = @@ -2523,11 +2572,31 @@ proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) = 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.ibox == nil: - ctx.ibox = getTextBox(styledNode.computed.inheritProperties()) - ctx.ibox.node = 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) = @@ -2564,14 +2633,17 @@ proc generateReplacement(ctx: var InnerBlockContext, child, parent: StyledNode) ctx.generateAnonymousInlineText("[img]", parent) of CONTENT_NEWLINE: ctx.iflush() - ctx.ibox = getTextBox(parent.computed.inheritProperties()) - ctx.ibox.newline = true + #TODO ?? + # this used to set ibox (before we had iroot), now I'm not sure if we + # should reconstruct here first + ctx.iroot = getTextBox(parent.computed.inheritProperties()) + ctx.iroot.newline = true ctx.iflush() proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) = ctx.iflush() - var lbox = getTextBox(styledNode.computed) - lbox.node = styledNode + ctx.inlineStack.add(styledNode) + var lbox = ctx.reconstructInlineParents() lbox.splitstart = true ctx.ibox = lbox for child in styledNode.children: @@ -2581,18 +2653,17 @@ proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) = of STYLED_TEXT: if ctx.ibox != lbox: ctx.iflush() - lbox = getTextBox(styledNode.computed) - lbox.node = styledNode + 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 = getTextBox(styledNode.computed) - lbox.node = styledNode + lbox = ctx.reconstructInlineParents() ctx.ibox = lbox lbox.splitend = true + ctx.inlineStack.setLen(ctx.inlineStack.len - 1) ctx.iflush() proc newInnerBlockContext(styledNode: StyledNode, box: BlockBoxBuilder, |