about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-10-26 21:17:40 +0200
committerbptato <nincsnevem662@gmail.com>2023-10-26 22:07:39 +0200
commit77fe3c3e150c619f2f4623b0bb612a19ed682f9a (patch)
tree2ac33773f8e75ff223401171cd228781a8c45c57
parent51a5d3df3e63f1b120a509daeb8a8f9dacc48490 (diff)
downloadchawan-77fe3c3e150c619f2f4623b0bb612a19ed682f9a.tar.gz
layout/engine: refactor inline atoms etc.
-rw-r--r--src/layout/box.nim42
-rw-r--r--src/layout/engine.nim425
-rw-r--r--src/render/renderdocument.nim37
3 files changed, 256 insertions, 248 deletions
diff --git a/src/layout/box.nim b/src/layout/box.nim
index e09b8e37..f7a36255 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -12,8 +12,8 @@ type
     y*: LayoutUnit
 
   Size* = object
-    width*: LayoutUnit
-    height*: LayoutUnit
+    w*: LayoutUnit
+    h*: LayoutUnit
 
   # min-content: box width is longest word's width
   # max-content: box width is content width without wrapping
@@ -64,12 +64,20 @@ type
 
   TableCaptionBoxBuilder* = ref object of BlockBoxBuilder
 
-  InlineAtom* = ref object of RootObj
+  InlineAtomType* = enum
+    INLINE_SPACING, INLINE_PADDING, INLINE_WORD, INLINE_BLOCK
+
+  InlineAtom* = ref object
     offset*: Offset
-    width*: LayoutUnit
-    height*: LayoutUnit
-    vertalign*: CSSVerticalAlign
-    baseline*: LayoutUnit
+    size*: Size
+    case t*: InlineAtomType
+    of INLINE_SPACING, INLINE_PADDING:
+      sformat*: ComputedFormat
+    of INLINE_WORD:
+      wformat*: ComputedFormat
+      str*: string
+    of INLINE_BLOCK:
+      innerbox*: BlockBox
 
   ComputedFormat* = ref object
     fontstyle*: CSSFontStyle
@@ -82,21 +90,10 @@ type
     # then properly blend them.
     bgcolor*: RGBAColor
 
-  InlineSpacing* = ref object of InlineAtom
-    format*: ComputedFormat
-
-  # Treated exactly the same as InlineSpacing, except it signifies padding.
-  InlinePadding* = ref object of InlineSpacing
-
-  InlineWord* = ref object of InlineAtom
-    str*: string
-    format*: ComputedFormat
-
   LineBox* = ref object
     atoms*: seq[InlineAtom]
-    offset*: Offset
-    width*: LayoutUnit
-    height*: LayoutUnit
+    offsety*: LayoutUnit
+    size*: Size
 
   InlineContext* = ref object
     offset*: Offset
@@ -195,11 +192,6 @@ type
     collapse*: bool
     reflow*: seq[bool]
 
-  InlineBlockBox* = ref object of InlineAtom
-    innerbox*: BlockBox
-    margin_top*: LayoutUnit
-    margin_bottom*: LayoutUnit
-
 func minContent*(): SizeConstraint =
   return SizeConstraint(t: MIN_CONTENT)
 
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 1657a3c9..d7e28a52 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -52,15 +52,23 @@ func applySizeConstraint(u: LayoutUnit, availableSize: SizeConstraint):
 
 type
   LineBoxState = object
-    line: LineBox
+    atomstates: seq[InlineAtomState]
     baseline: LayoutUnit
     lineheight: LayoutUnit
+    line: LineBox
+
+  InlineAtomState = object
+    vertalign: CSSVerticalAlign
+    baseline: LayoutUnit
+    margin_top: LayoutUnit
+    margin_bottom: LayoutUnit
 
   InlineState = object
     ictx: InlineContext
     skip: bool
     node: StyledNode
-    word: InlineWord
+    wordstate: InlineAtomState
+    word: InlineAtom
     wrappos: int # position of last wrapping opportunity, or -1
     hasshy: bool
     computed: CSSComputedValues
@@ -90,15 +98,24 @@ func cellheight(viewport: Viewport): LayoutUnit =
 func cellheight(state: InlineState): LayoutUnit =
   state.viewport.cellheight
 
+template atoms(state: LineBoxState): untyped =
+  state.line.atoms
+
+template size(state: LineBoxState): untyped =
+  state.line.size
+
+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(state: InlineState): bool =
-  for i in countdown(state.currentLine.line.atoms.high, 0):
-    let atom = state.currentLine.line.atoms[i]
-    if atom of InlineSpacing:
-      if atom of InlinePadding:
-        continue # skip padding
+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
@@ -107,7 +124,8 @@ func hasLastSpacing(state: InlineState): bool =
 func computeShift(state: InlineState, computed: CSSComputedValues):
     LayoutUnit =
   if state.whitespacenum > 0:
-    if state.currentLine.line.atoms.len > 0 and not state.hasLastSpacing() or
+    if state.currentLine.atoms.len > 0 and
+          not state.currentLine.hasLastSpacing() or
         computed.whitespacepre:
       let spacing = computed{"word-spacing"}
       if spacing.auto:
@@ -136,15 +154,21 @@ func getComputedFormat(computed: CSSComputedValues, node: StyledNode): ComputedF
   )
 
 proc newWord(state: var InlineState) =
-  let word = InlineWord()
-  word.format = getComputedFormat(state.computed, state.node)
-  word.vertalign = state.computed{"vertical-align"}
-  state.format = word.format
-  state.word = word
+  let wformat = getComputedFormat(state.computed, state.node)
+  state.format = wformat
+  state.word = InlineAtom(
+    t: INLINE_WORD,
+    wformat: wformat,
+    size: Size(h: state.cellheight)
+  )
+  state.wordstate = InlineAtomState(
+    vertalign: state.computed{"vertical-align"},
+    baseline: state.cellheight
+  )
   state.wrappos = -1
   state.hasshy = false
 
-proc horizontalAlignLine(state: InlineState, line: LineBox,
+proc horizontalAlignLine(state: var InlineState, line: var LineBox,
     computed: CSSComputedValues, last = false) =
   let width = case state.availableWidth.t
   of MIN_CONTENT, MAX_CONTENT:
@@ -159,147 +183,165 @@ proc horizontalAlignLine(state: InlineState, line: LineBox,
     discard
   of TEXT_ALIGN_END, TEXT_ALIGN_RIGHT, TEXT_ALIGN_CHA_RIGHT:
     # move everything
-    let x = max(width, line.width) - line.width
-    for atom in line.atoms:
+    let x = max(width, line.size.w) - line.size.w
+    for atom in line.atoms.mitems:
       atom.offset.x += x
-      state.ictx.width = max(atom.offset.x + atom.width, state.ictx.width)
+      state.ictx.width = max(atom.offset.x + atom.size.w, state.ictx.width)
   of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER:
-    let x = max((max(width - line.offset.x, line.width)) div 2 - line.width div 2, 0)
-    for atom in line.atoms:
+    # 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.width = max(atom.offset.x + atom.width, state.ictx.width)
+      state.ictx.width = max(atom.offset.x + atom.size.w, state.ictx.width)
   of TEXT_ALIGN_JUSTIFY:
     if not computed.whitespacepre and not last:
       var sumwidth: LayoutUnit = 0
       var spaces = 0
-      for atom in line.atoms:
-        if atom of InlineSpacing:
+      for atom in line.atoms.mitems:
+        if atom.t in {INLINE_SPACING, INLINE_PADDING}:
           discard
         else:
           inc spaces
-          sumwidth += atom.width
+          sumwidth += atom.size.w
       dec spaces
       if spaces > 0:
         let spacingwidth = (width - sumwidth) div spaces
-        line.width = 0
-        for atom in line.atoms:
-          atom.offset.x = line.width
-          if atom of InlineSpacing:
-            let atom = InlineSpacing(atom)
-            atom.width = spacingwidth
-          line.width += atom.width
+        line.size.w = 0
+        for atom in line.atoms.mitems:
+          atom.offset.x = line.size.w
+          if atom.t == INLINE_SPACING:
+            atom.size.w = spacingwidth
+          line.size.w += atom.size.w
   state.ictx.width = max(width, state.ictx.width) #TODO this seems meaningless?
 
-# Align atoms (inline boxes, text, etc.) vertically inside the line.
+# Align atoms (inline boxes, text, etc.) vertically (i.e. along the inline
+# axis) inside the line.
 proc verticalAlignLine(state: var InlineState) =
-  let line = state.currentLine.line
-
   # Start with line-height as the baseline and line height.
   let lineheight = state.currentLine.lineheight
-  line.height = lineheight
+  state.currentLine.size.h = lineheight
   var baseline = lineheight
 
   # Calculate the line's baseline based on atoms' baseline.
-  for atom in line.atoms:
-    case atom.vertalign.keyword
+  # Also, collect the maximum vertical margins of inline blocks.
+  var margin_top: LayoutUnit = 0
+  var margin_bottom: LayoutUnit = 0
+  for i in 0 ..< state.currentLine.atoms.len:
+    let atom = state.currentLine.atoms[i]
+    let iastate = state.currentLine.atomstates[i]
+    case iastate.vertalign.keyword
     of VERTICAL_ALIGN_BASELINE:
-      let len = atom.vertalign.length.px(state.viewport, lineheight)
-      baseline = max(baseline, atom.baseline + len)
+      let len = iastate.vertalign.length.px(state.viewport, lineheight)
+      baseline = max(baseline, iastate.baseline + len)
     of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM:
-      baseline = max(baseline, atom.height)
+      baseline = max(baseline, atom.size.h)
     of VERTICAL_ALIGN_MIDDLE:
-      baseline = max(baseline, atom.height div 2)
+      baseline = max(baseline, atom.size.h div 2)
     else:
-      baseline = max(baseline, atom.baseline)
+      baseline = max(baseline, iastate.baseline)
+    margin_top = max(iastate.margin_top, margin_top)
+    margin_bottom = max(iastate.margin_bottom, margin_bottom)
 
   # 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 atom in line.atoms:
+  for i in 0 ..< state.currentLine.atoms.len:
+    let atom = state.currentLine.atoms[i]
+    let iastate = state.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.)
-    line.height = max(line.height, atom.height)
-    case atom.vertalign.keyword
+    state.currentLine.size.h = max(state.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 = atom.vertalign.length.px(state.viewport, lineheight)
-      line.height = max(baseline - atom.baseline + atom.height + len, line.height)
+      let len = iastate.vertalign.length.px(state.viewport, lineheight)
+      state.currentLine.size.h = max(baseline - iastate.baseline +
+        atom.size.h + len, state.currentLine.size.h)
     of VERTICAL_ALIGN_MIDDLE:
       # Line height must be at least
       # (line baseline) + (atom height / 2).
-      line.height = max(baseline + atom.height div 2, line.height)
+      state.currentLine.size.h = max(baseline + atom.size.h div 2,
+        state.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).
-      line.height = max(baseline - atom.baseline + atom.height, line.height)
+      state.currentLine.size.h = max(baseline - iastate.baseline +
+        atom.size.h, state.currentLine.size.h)
 
   # Now we can calculate the actual position of atoms inside the line.
-  for atom in line.atoms:
-    case atom.vertalign.keyword
+  for i in 0 ..< state.currentLine.atoms.len:
+    let iastate = state.currentLine.atomstates[i]
+    let atom = addr state.currentLine.atoms[i]
+    case iastate.vertalign.keyword
     of VERTICAL_ALIGN_BASELINE:
       # Atom is placed at (line baseline) - (atom baseline) - len
-      let len = atom.vertalign.length.px(state.viewport, lineheight)
-      atom.offset.y = baseline - atom.baseline - len
+      let len = iastate.vertalign.length.px(state.viewport, 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.height div 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 = line.height - atom.height
+      atom.offset.y = state.currentLine.size.h - atom.size.h
     else:
       # See baseline (with len = 0).
-      atom.offset.y = baseline - atom.baseline
-
-  # Finally, find the inline block with the largest block margins, then apply
-  # these to the line itself.
-  var margin_top: LayoutUnit = 0
-  var margin_bottom: LayoutUnit = 0
-
-  for atom in line.atoms:
-    if atom of InlineBlockBox:
-      let atom = InlineBlockBox(atom)
-      margin_top = max(atom.margin_top, margin_top)
-      margin_bottom = max(atom.margin_bottom, margin_bottom)
-
-  for atom in line.atoms:
+      atom.offset.y = baseline - iastate.baseline
+    # Offset the atom's y position by the largest margin_top value.
     atom.offset.y += margin_top
 
-  line.height += margin_top
-  line.height += margin_bottom
+  # Grow the line by the largest margin_top and margin_bottom, and set
+  # its baseline.
+  state.currentLine.size.h += margin_top
+  state.currentLine.size.h += margin_bottom
   state.currentLine.baseline = baseline
 
-proc addPadding(line: LineBox, width, height: LayoutUnit,
+proc addPadding(state: var InlineState, width, height: LayoutUnit,
     format: ComputedFormat) =
-  let padding = InlinePadding(
-    width: width,
-    height: height,
-    baseline: height,
-    format: format
+  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,
+    hang = false) =
+  let spacing = InlineAtom(
+    t: INLINE_SPACING,
+    size: Size(
+      w: width,
+      h: height
+    ),
+    sformat: state.format,
+    offset: Offset(x: state.currentLine.size.w)
   )
-  padding.offset.x = line.width
-  line.width += width
-  line.atoms.add(padding)
-
-proc addSpacing(line: LineBox, width, height: LayoutUnit,
-    format: ComputedFormat, hang = false) =
-  let spacing = InlineSpacing(
-    width: width,
-    height: height,
-    baseline: height,
-    format: format
+  let iastate = InlineAtomState(
+    baseline: height
+    #TODO vertalign?
   )
-  spacing.offset.x = line.width
   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.
-    line.width += spacing.width
-  line.atoms.add(spacing)
+    state.currentLine.size.w += width
+  state.currentLine.atomstates.add(iastate)
+  state.currentLine.atoms.add(spacing)
 
 proc flushWhitespace(state: var InlineState, computed: CSSComputedValues,
     hang = false) =
@@ -307,12 +349,11 @@ proc flushWhitespace(state: var InlineState, computed: CSSComputedValues,
   state.charwidth += state.whitespacenum
   state.whitespacenum = 0
   if shift > 0:
-    let line = state.currentLine.line
-    line.addSpacing(shift, state.cellheight, state.format, hang)
+    state.addSpacing(shift, state.cellheight, hang)
 
 proc finishLine(state: var InlineState, computed: CSSComputedValues,
     force = false) =
-  if state.currentLine.line.atoms.len != 0 or force:
+  if state.currentLine.atoms.len != 0 or force:
     let whitespace = computed{"white-space"}
     if whitespace == WHITESPACE_PRE:
       state.flushWhitespace(computed)
@@ -322,33 +363,31 @@ proc finishLine(state: var InlineState, computed: CSSComputedValues,
       state.whitespacenum = 0
     state.verticalAlignLine()
     # add line to ictx
-    let line = state.currentLine.line
-    let y = line.offset.y
+    let y = state.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.lines.add(line)
-    state.ictx.height += line.height
-    state.ictx.width = max(state.ictx.width, line.width)
-    let newLine = LineBox(offset: Offset(y: y + line.height))
-    state.currentLine = LineBoxState(line: newLine)
+    state.ictx.height += state.currentLine.size.h
+    state.ictx.width = max(state.ictx.width, state.currentLine.size.w)
+    state.ictx.lines.add(state.currentLine.line)
+    state.currentLine = LineBoxState(
+      line: LineBox(offsety: y + state.currentLine.size.h)
+    )
     state.charwidth = 0 #TODO put this in LineBoxState?
 
 proc finish(state: var InlineState, computed: CSSComputedValues) =
   state.finishLine(computed)
   if state.ictx.lines.len > 0:
     for i in 0 ..< state.ictx.lines.len - 1:
-      let line = state.ictx.lines[i]
-      state.horizontalAlignLine(line, computed, last = false)
-    let lastLine = state.ictx.lines[^1]
-    state.horizontalAlignLine(lastLine, computed, last = true)
+      state.horizontalAlignLine(state.ictx.lines[i], computed, last = false)
+    state.horizontalAlignLine(state.ictx.lines[^1], computed, last = true)
 
 func minwidth(atom: InlineAtom): LayoutUnit =
-  if atom of InlineBlockBox:
-    return cast[InlineBlockBox](atom).innerbox.xminwidth
-  return atom.width
+  if atom.t == INLINE_BLOCK:
+    return atom.innerbox.xminwidth
+  return atom.size.w
 
 func shouldWrap(state: InlineState, w: LayoutUnit,
     pcomputed: CSSComputedValues): bool =
@@ -358,58 +397,60 @@ func shouldWrap(state: InlineState, w: LayoutUnit,
     return false # no wrap with max-content
   if state.availableWidth.t == MIN_CONTENT:
     return true # always wrap with min-content
-  return state.currentLine.line.width + w > state.availableWidth.u
+  return state.currentLine.size.w + w > state.availableWidth.u
 
 # 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.)
-proc addAtom(state: var InlineState, atom: InlineAtom,
-    pcomputed: CSSComputedValues) =
+# Returns true on newline.
+proc addAtom(state: var InlineState, iastate: InlineAtomState,
+    atom: InlineAtom, pcomputed: CSSComputedValues): bool =
+  result = false
   var shift = state.computeShift(pcomputed)
   state.whitespacenum = 0
   # Line wrapping
-  if state.shouldWrap(atom.width + shift, pcomputed):
+  if state.shouldWrap(atom.size.w + shift, pcomputed):
     state.finishLine(pcomputed, false)
+    result = true
     # Recompute on newline
     shift = state.computeShift(pcomputed)
-
-  if atom.width > 0 and atom.height > 0:
+  if atom.size.w > 0 and atom.size.h > 0:
     if shift > 0:
-      state.currentLine.line.addSpacing(shift, state.cellheight, state.format)
-
-    atom.offset.x += state.currentLine.line.width
+      state.addSpacing(shift, state.cellheight)
     state.ictx.minwidth = max(state.ictx.minwidth, atom.minwidth)
     state.currentLine.applyLineHeight(state.viewport, pcomputed)
-    state.currentLine.line.width += atom.width
-    if atom of InlineWord:
-      state.format = InlineWord(atom).format
+    if atom.t == INLINE_WORD:
+      state.format = atom.wformat
     else:
       state.charwidth = 0
       state.format = nil
-    state.currentLine.line.atoms.add(atom)
+    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) =
+proc addWord(state: var InlineState): bool =
+  result = false
   if state.word.str != "":
-    var word = state.word
-    word.str.mnormalize() #TODO this may break on EOL.
-    word.height = state.cellheight
-    word.baseline = word.height
-    state.addAtom(word, state.computed)
+    state.word.str.mnormalize() #TODO this may break on EOL.
+    result = state.addAtom(state.wordstate, state.word, state.computed)
     state.newWord()
 
-proc addWordEOL(state: var InlineState) =
+proc addWordEOL(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 = state.word.str.substr(0, state.wrappos - 1)
+      state.word.str.setLen(state.wrappos)
       if state.hasshy:
-        state.word.str &= $Rune(0xAD) # soft hyphen
+        const shy = $Rune(0xAD) # soft hyphen
+        state.word.str &= shy
         state.hasshy = false
-      state.addWord()
+      result = state.addWord()
       state.word.str = leftstr
-      state.word.width = leftstr.width() * state.cellwidth
+      state.word.size.w = leftstr.width() * state.cellwidth
     else:
-      state.addWord()
+      result = state.addWord()
 
 # Start a new line, even if the previous one is empty
 proc flushLine(state: var InlineState, computed: CSSComputedValues) =
@@ -424,29 +465,25 @@ proc checkWrap(state: var InlineState, r: Rune) =
   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.width + shift + rw * state.cellwidth
+      let plusWidth = state.word.size.w + shift + rw * state.cellwidth
       if state.shouldWrap(plusWidth, nil):
-        let l = state.currentLine
-        state.addWordEOL()
-        if l == state.currentLine: # no line wrapping occured in addAtom
+        if not state.addWordEOL(): # no line wrapping occured in addAtom
           state.finishLine(state.computed)
           state.whitespacenum = 0
   of WORD_BREAK_BREAK_ALL:
-    let plusWidth = state.word.width + shift + rw * state.cellwidth
+    let plusWidth = state.word.size.w + shift + rw * state.cellwidth
     if state.shouldWrap(plusWidth, nil):
-      let l = state.currentLine
-      state.addWordEOL()
-      if l == state.currentLine: # no line wrapping occured in addAtom
+      if not state.addWordEOL(): # no line wrapping occured in addAtom
         state.finishLine(state.computed)
         state.whitespacenum = 0
   of WORD_BREAK_KEEP_ALL:
-    let plusWidth = state.word.width + shift + rw * state.cellwidth
+    let plusWidth = state.word.size.w + shift + rw * state.cellwidth
     if state.shouldWrap(plusWidth, nil):
       state.finishLine(state.computed)
       state.whitespacenum = 0
 
 proc processWhitespace(state: var InlineState, c: char) =
-  state.addWord()
+  discard state.addWord()
   case state.computed{"white-space"}
   of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
     state.whitespacenum = max(state.whitespacenum, 1)
@@ -468,11 +505,13 @@ proc processWhitespace(state: var InlineState, c: char) =
     else:
       inc state.whitespacenum
 
-func newInlineState(ictx: InlineContext, viewport: Viewport,
+func newInlineState(viewport: Viewport,
     availableWidth, availableHeight: SizeConstraint): InlineState =
   return InlineState(
-    currentLine: LineBoxState(line: LineBox()),
-    ictx: ictx,
+    currentLine: LineBoxState(
+      line: LineBox()
+    ),
+    ictx: InlineContext(),
     viewport: viewport,
     availableWidth: availableWidth,
     availableHeight: availableHeight
@@ -503,13 +542,12 @@ proc layoutText(state: var InlineState, str: string,
       else:
         state.word.str &= r
         let w = r.width()
-        state.word.width += w * state.cellwidth
+        state.word.size.w += w * state.cellwidth
         state.charwidth += w
         if r == Rune('-'): # ascii dash
           state.wrappos = state.word.str.len
           state.hasshy = false
-
-  state.addWord()
+  discard state.addWord()
 
 func isOuterBlock(computed: CSSComputedValues): bool =
   return computed{"display"} in {DISPLAY_BLOCK, DISPLAY_TABLE}
@@ -837,19 +875,6 @@ proc newListItem(parent: BlockBox, builder: ListItemBoxBuilder): ListItemBox =
   box.calcAvailableSizes(availableWidth, availableHeight, percHeight)
   return box
 
-proc newInlineBlock(viewport: Viewport, builder: BoxBuilder,
-    parentWidth, parentHeight: SizeConstraint,
-    percHeight: Option[LayoutUnit]): InlineBlockBox =
-  let box = InlineBlockBox(
-    innerbox: newFlowRootBox(viewport, builder, parentWidth,
-      maxContent(), percHeight),
-    vertalign: builder.computed{"vertical-align"}
-  )
-  return box
-
-proc newInlineContext(): InlineContext =
-  return InlineContext()
-
 proc buildBlock(builder: BlockBoxBuilder, parent: BlockBox): BlockBox
 proc buildInlines(parent: BlockBox, inlines: seq[BoxBuilder]): InlineContext
 proc buildBlocks(parent: BlockBox, blocks: seq[BoxBuilder], node: StyledNode)
@@ -903,32 +928,34 @@ func toperc100(sc: SizeConstraint): Option[LayoutUnit] =
   return none(LayoutUnit)
 
 # parentWidth, parentHeight: width/height of the containing block.
-proc buildInlineBlock(builder: BlockBoxBuilder, viewport: Viewport,
-    parentWidth, parentHeight: SizeConstraint): InlineBlockBox =
-  result = newInlineBlock(viewport, builder, fitContent(parentWidth),
-    maxContent(), parentHeight.toperc100())
-
+proc addInlineBlock(state: var InlineState, builder: BlockBoxBuilder,
+    parentWidth, parentHeight: SizeConstraint, computed: CSSComputedValues) =
+  let innerbox = newFlowRootBox(state.viewport, builder,
+    fitContent(parentWidth), maxContent(), parentHeight.toperc100())
   case builder.computed{"display"}
   of DISPLAY_INLINE_BLOCK:
-    result.innerbox.buildLayout(builder)
+    innerbox.buildLayout(builder)
   of DISPLAY_INLINE_TABLE:
-    result.innerbox.buildTableLayout(TableBoxBuilder(builder))
+    innerbox.buildTableLayout(TableBoxBuilder(builder))
   else:
     assert false, $builder.computed{"display"}
-
   # Apply the block box's properties to the atom itself.
-  result.width = result.innerbox.width
-  result.height = result.innerbox.height
-
-  result.margin_top = result.innerbox.margin_top
-  result.margin_bottom = result.innerbox.margin_bottom
-
-  result.baseline = result.innerbox.baseline
-
-  # I don't like this, but it works...
-  result.offset.x = result.innerbox.margin_left
-  result.width += result.innerbox.margin_left
-  result.width += result.innerbox.margin_right
+  let iblock = InlineAtom(
+    t: INLINE_BLOCK,
+    innerbox: innerbox,
+    offset: Offset(x: innerbox.margin_left),
+    size: Size(
+      w: innerbox.width + innerbox.margin_left + innerbox.margin_right,
+      h: innerbox.height
+    )
+  )
+  let iastate = InlineAtomState(
+    baseline: iblock.innerbox.baseline,
+    vertalign: builder.computed{"vertical-align"},
+    margin_top: iblock.innerbox.margin_top,
+    margin_bottom: iblock.innerbox.margin_bottom
+  )
+  discard state.addAtom(iastate, iblock, computed)
 
 proc buildInline(state: var InlineState, box: InlineBoxBuilder) =
   let viewport = state.viewport
@@ -939,16 +966,15 @@ proc buildInline(state: var InlineState, box: InlineBoxBuilder) =
   if box.splitstart:
     let margin_left = box.computed{"margin-left"}.px(viewport,
       state.availableWidth)
-    state.currentLine.line.width += margin_left
+    state.currentLine.size.w += margin_left
 
     let padding_left = box.computed{"padding-left"}.px(viewport,
       state.availableWidth)
     if padding_left > 0:
       # We must add spacing to the line to make sure that it is formatted
-      # appropriately.
+      # (i.e. colored) appropriately.
       # We need this so long as we have no proper inline boxes.
-      state.currentLine.line.addPadding(padding_left, state.cellheight,
-        paddingformat)
+      state.addPadding(padding_left, state.cellheight, paddingformat)
 
   assert box.children.len == 0 or box.text.len == 0
   for text in box.text:
@@ -963,8 +989,7 @@ proc buildInline(state: var InlineState, box: InlineBoxBuilder) =
       let child = BlockBoxBuilder(child)
       let w = fitContent(state.availableWidth)
       let h = state.availableHeight
-      let iblock = child.buildInlineBlock(viewport, w, h)
-      state.addAtom(iblock, box.computed)
+      state.addInlineBlock(child, w, h, box.computed)
       state.whitespacenum = 0
     else:
       assert false, "child.t is " & $child.computed{"display"}
@@ -972,19 +997,17 @@ proc buildInline(state: var InlineState, box: InlineBoxBuilder) =
   if box.splitend:
     let padding_right = box.computed{"padding-right"}.px(viewport,
       state.availableWidth)
-    state.currentLine.line.width += padding_right
+    state.currentLine.size.w += padding_right
     if padding_right > 0:
-      let height = max(state.currentLine.line.height, 1)
-      state.currentLine.line.addPadding(padding_right, height, paddingformat)
+      let height = max(state.currentLine.size.h, 1)
+      state.addPadding(padding_right, height, paddingformat)
     let margin_right = box.computed{"margin-right"}.px(viewport,
       state.availableWidth)
-    state.currentLine.line.width += margin_right
+    state.currentLine.size.w += margin_right
 
 proc buildInlines(parent: BlockBox, inlines: seq[BoxBuilder]): InlineContext =
-  let ictx = newInlineContext()
-  var state = newInlineState(ictx, parent.viewport, parent.availableWidth,
+  var state = newInlineState(parent.viewport, parent.availableWidth,
     parent.availableHeight)
-  let viewport = state.viewport
   if inlines.len > 0:
     for child in inlines:
       case child.computed{"display"}
@@ -996,21 +1019,19 @@ proc buildInlines(parent: BlockBox, inlines: seq[BoxBuilder]): InlineContext =
         let child = BlockBoxBuilder(child)
         let w = fitContent(state.availableWidth)
         let h = state.availableHeight
-        let iblock = child.buildInlineBlock(viewport, w, h)
-        state.addAtom(iblock, parent.computed)
+        state.addInlineBlock(child, w, h, parent.computed)
         state.whitespacenum = 0
       else:
         assert false, "child.t is " & $child.computed{"display"}
     state.finish(parent.computed)
-  return ictx
+  return state.ictx
 
 proc buildMarker(builder: MarkerBoxBuilder, parent: BlockBox): InlineContext =
-  let ictx = newInlineContext()
-  var state = newInlineState(ictx, parent.viewport,
+  var state = newInlineState(parent.viewport,
     fitContent(parent.availableWidth), parent.availableHeight)
   state.buildInline(builder)
   state.finish(builder.computed)
-  return ictx
+  return state.ictx
 
 proc buildListItem(builder: ListItemBoxBuilder, parent: BlockBox): ListItemBox =
   result = parent.newListItem(builder)
@@ -2113,7 +2134,7 @@ proc generateTableBox(styledNode: StyledNode, viewport: Viewport,
   box.generateTableChildWrappers()
   return box
 
-proc renderLayout*(viewport: var Viewport, root: StyledNode): BlockBox =
+proc renderLayout*(viewport: Viewport, root: StyledNode): BlockBox =
   viewport.positioned.setLen(0)
   let builder = root.generateBlockBox(viewport)
   return viewport.buildRootBlock(builder)
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index 95e2687b..dbdb2a6e 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -171,7 +171,7 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat,
   assert lines[y].formats[fi].pos <= nx
   # That's it!
 
-proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: LayoutUnit,
+proc setRowWord(lines: var FlexibleGrid, word: InlineAtom, x, y: LayoutUnit,
     window: WindowAttributes) =
   var r: Rune
 
@@ -185,16 +185,15 @@ proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: LayoutUnit,
     x += r.twidth(x)
   if x < 0: return # highest x is outside the canvas, no need to draw
   let linestr = word.str.substr(i)
+  lines.setText(linestr, word.wformat, x, y)
 
-  lines.setText(linestr, word.format, x, y)
-
-proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: LayoutUnit,
+proc setSpacing(lines: var FlexibleGrid, spacing: InlineAtom, x, y: LayoutUnit,
     window: WindowAttributes) =
   var y = toInt((y + spacing.offset.y) div window.ppl) # y cell
   if y < 0: return # y is outside the canvas, no need to draw
 
   var x = toInt((x + spacing.offset.x) div window.ppc) # x cell
-  let width = toInt(spacing.width div window.ppc) # cell width
+  let width = toInt(spacing.size.w div window.ppc) # cell width
 
   if x + width < 0: return # highest x is outside the canvas, no need to draw
   var i = 0
@@ -202,8 +201,7 @@ proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: LayoutUni
     i -= x
     x = 0
   let linestr = ' '.repeat(width - i)
-
-  lines.setText(linestr, spacing.format, x, y)
+  lines.setText(linestr, spacing.sformat, x, y)
 
 proc paintBackground(lines: var FlexibleGrid, color: RGBAColor, startx,
     starty, endx, endy: int, node: StyledNode, window: WindowAttributes) =
@@ -290,7 +288,7 @@ func calculateErrorY(ctx: InlineContext, window: WindowAttributes):
   var error = 0
   for i in 0 ..< ctx.lines.len:
     if i < ctx.lines.high:
-      let dy = toInt(ctx.lines[i + 1].offset.y - ctx.lines[i].offset.y)
+      let dy = toInt(ctx.lines[i + 1].offsety - ctx.lines[i].offsety)
       error += dy - (dy div window.ppl) * window.ppl
   return error div (ctx.lines.len - 1)
 
@@ -305,8 +303,7 @@ proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext,
   let erry = ctx.calculateErrorY(window)
   var i = 0
   for line in ctx.lines:
-    let x = x + line.offset.x
-    let y0 = y + line.offset.y
+    let y0 = y + line.offsety
     let y = y0 - erry * i
 
     let r = y div window.ppl
@@ -314,17 +311,15 @@ proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext,
       grid.addLine()
 
     for atom in line.atoms:
-      if atom of InlineBlockBox:
-        let iblock = InlineBlockBox(atom)
-        let x = x + iblock.offset.x
-        let y = y + iblock.offset.y
-        grid.renderBlockBox(iblock.innerbox, x, y, window, posx, posy)
-      elif atom of InlineWord:
-        let word = InlineWord(atom)
-        grid.setRowWord(word, x, y, window)
-      elif atom of InlineSpacing:
-        let spacing = InlineSpacing(atom)
-        grid.setSpacing(spacing, x, y, window)
+      case atom.t
+      of INLINE_BLOCK:
+        let x = x + atom.offset.x
+        let y = y + atom.offset.y
+        grid.renderBlockBox(atom.innerbox, x, y, window, posx, posy)
+      of INLINE_WORD:
+        grid.setRowWord(atom, x, y, window)
+      of INLINE_SPACING, INLINE_PADDING:
+        grid.setSpacing(atom, x, y, window)
     inc i
 
 proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: LayoutUnit,