about summary refs log tree commit diff stats
path: root/src/css/layout.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2025-02-12 21:15:38 +0100
committerbptato <nincsnevem662@gmail.com>2025-02-12 23:27:34 +0100
commit8f3c96035f22a16459ad7c8996c280a015f0d2f5 (patch)
tree183424ee7b20eb53b313d0ff76885e7e3acdc0d5 /src/css/layout.nim
parent76e2b68af52bf51854388890172bc0b2f2aaf36b (diff)
downloadchawan-8f3c96035f22a16459ad7c8996c280a015f0d2f5.tar.gz
layout: separate out tree construction logic
For now, the skeleton remains in layout.  Eventually it should be
lazily constructed during the actual layout pass (thereby making layout
"single-pass" (sometimes :p))

The end goal is to do it all in styledNode.children, so that caching can
be as simple as "next box = find next matching cached box ?? make new".

This does mean that the internal structure of cached boxes will always
have to be reconstructed, but I don't see a better way.  (I suppose it
still remains possible to optimize out the unnecessary layout pass when
only a repaint is needed (e.g. color change) by modifying computed
values in-place.)
Diffstat (limited to 'src/css/layout.nim')
-rw-r--r--src/css/layout.nim492
1 files changed, 58 insertions, 434 deletions
diff --git a/src/css/layout.nim b/src/css/layout.nim
index eddffced..0153b7e8 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -2,7 +2,7 @@ import std/algorithm
 import std/math
 
 import css/box
-import css/cascade
+import css/csstree
 import css/cssvalues
 import css/lunit
 import html/dom
@@ -47,8 +47,6 @@ type
     cellSize: Size # size(w = attrsp.ppc, h = attrsp.ppl)
     positioned: seq[PositionedItem]
     myRootProperties: CSSValues
-    # placeholder text data
-    imgText: CharacterData
     luctx: LUContext
 
 const DefaultSpan = Span(start: 0, send: LUnit.high)
@@ -120,8 +118,7 @@ func isDefinite(sc: SizeConstraint): bool =
 # children.
 func establishesBFC(computed: CSSValues): bool =
   return computed{"float"} != FloatNone or
-    computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayTableWrapper,
-      DisplayFlex} or
+    computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayFlex} or
     computed{"overflow-x"} notin {OverflowVisible, OverflowClip}
     #TODO contain, grid, multicol, column-span
 
@@ -404,7 +401,7 @@ type
     firstBaselineSet: bool
 
 # Forward declarations
-proc layoutTableWrapper(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes)
+proc layoutTable(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes)
 proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes)
 proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset;
   sizes: ResolvedSizes; includeMargin = false)
@@ -2107,16 +2104,6 @@ proc layoutListItem(bctx: var BlockContext; box: BlockBox;
 #      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.
-const RowGroupBox = {
-  # Note: caption is not included here
-  DisplayTableRowGroup, DisplayTableHeaderGroup, DisplayTableFooterGroup
-}
-const ProperTableChild = RowGroupBox + {
-  DisplayTableRow, DisplayTableColumn, DisplayTableColumnGroup
-}
-const DisplayInnerTable = {DisplayTable, DisplayInlineTable}
-const ProperTableRowParent = RowGroupBox + DisplayInnerTable
-
 type
   CellWrapper = ref object
     box: BlockBox
@@ -2414,7 +2401,7 @@ proc preLayoutTableRows(tctx: var TableContext; table: BlockBox) =
     of DisplayTableFooterGroup:
       for it in child.children:
         tfoot.add(BlockBox(it))
-    else: assert false
+    else: assert false, $child.computed{"display"}
   tctx.preLayoutTableRows(thead, table)
   tctx.preLayoutTableRows(tbody, table)
   tctx.preLayoutTableRows(tfoot, table)
@@ -2553,13 +2540,13 @@ proc layoutCaption(tctx: TableContext; parent, box: BlockBox) =
   parent.state.size.h += outerHeight
   parent.state.intr.h += outerHeight - box.state.size.h + box.state.intr.h
 
-proc layoutTable(tctx: var TableContext; table, parent: BlockBox;
+proc layoutInnerTable(tctx: var TableContext; table, parent: BlockBox;
     sizes: ResolvedSizes) =
   if table.computed{"border-collapse"} != BorderCollapseCollapse:
     let spc = table.computed{"border-spacing"}
     if spc != nil:
-      tctx.inlineSpacing = table.computed{"border-spacing"}.a.px(0)
-      tctx.blockSpacing = table.computed{"border-spacing"}.b.px(0)
+      tctx.inlineSpacing = spc.a.px(0)
+      tctx.blockSpacing = spc.b.px(0)
   tctx.preLayoutTableRows(table) # first pass
   # Percentage sizes have been resolved; switch the table's space to
   # fit-content if its width is auto.
@@ -2583,12 +2570,11 @@ proc layoutTable(tctx: var TableContext; table, parent: BlockBox;
 
 # As per standard, we must put the caption outside the actual table, inside a
 # block-level wrapper box.
-proc layoutTableWrapper(bctx: BlockContext; box: BlockBox;
-    sizes: ResolvedSizes) =
+proc layoutTable(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes) =
   let table = BlockBox(box.children[0])
   table.state = BoxLayoutState()
   var tctx = TableContext(lctx: bctx.lctx, space: sizes.space)
-  tctx.layoutTable(table, box, sizes)
+  tctx.layoutInnerTable(table, box, sizes)
   box.state.size = table.state.size
   box.state.baseline = table.state.size.h
   box.state.firstBaseline = table.state.size.h
@@ -2606,8 +2592,8 @@ proc layout(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes;
     bctx.layoutFlow(box, sizes, canClear)
   of DisplayListItem:
     bctx.layoutListItem(box, sizes)
-  of DisplayTableWrapper, DisplayInlineTableWrapper:
-    bctx.layoutTableWrapper(box, sizes)
+  of DisplayInnerTable:
+    bctx.layoutTable(box, sizes)
   of DisplayInnerFlex:
     bctx.layoutFlex(box, sizes)
   else:
@@ -2878,125 +2864,29 @@ proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset;
   box.state.marginBottom = marginBottom
 
 # 1st pass: build tree
-#TODO ideally we should move this to layout and/or cascade.
-# (Anon box generation probably belongs in cascade, so that caching
-# existing boxes becomes feasible.)
+#TODO this tree traversal is largely redundant, and should be moved
+# to csstree.  (Then it should be collapsed into the second pass by
+# lazily generating child boxes, taking boxes from the previous pass
+# when possible.)
 
-type
-  BoxBuilderContext = object
-    styledNode: StyledNode
-    lctx: LayoutContext
-    anonRow: BlockBox
-    anonTableWrapper: BlockBox
-    quoteLevel: int
-    listItemCounter: int
+proc build(ctx: var TreeContext; styledNode: StyledNode;
+  children: var seq[CSSBox])
 
-  ParentType = enum
-    ptBlock, ptInline
-
-# Forward declarations
-proc build(ctx: var BoxBuilderContext; box: BlockBox)
-proc buildInline(pctx: var BoxBuilderContext; styledNode: StyledNode):
-  InlineBox
-proc initBoxBuilderContext(styledNode: StyledNode; lctx: LayoutContext;
-  parent: ptr BoxBuilderContext): BoxBuilderContext
-proc flushTable(ctx: var BoxBuilderContext; parent: BlockBox)
-
-template initBoxBuilderContext(styledNode: StyledNode;
-    pctx: var BoxBuilderContext): BoxBuilderContext =
-  initBoxBuilderContext(styledNode, pctx.lctx, addr pctx)
-
-proc newMarkerBox(computed: CSSValues; listItemCounter: int): InlineBox =
-  let computed = computed.inheritProperties()
-  # Use pre, so the space at the end of the default markers isn't ignored.
-  computed{"white-space"} = WhitespacePre
-  let s = computed{"list-style-type"}.listMarker(listItemCounter)
-  return InlineBox(
-    t: ibtText,
-    computed: computed,
-    text: newCharacterData(s)
+proc buildInline(ctx: var TreeContext; styledNode: StyledNode): InlineBox =
+  let ibox = InlineBox(
+    t: ibtParent,
+    computed: styledNode.computed,
+    element: styledNode.element
   )
+  ctx.build(styledNode, ibox.children)
+  return ibox
 
-proc createAnonTable(ctx: var BoxBuilderContext; parentType: ParentType):
-    BlockBox =
-  if ctx.anonTableWrapper == nil:
-    let (outer, inner) = rootProperties().splitTable()
-    outer{"display"} = case parentType
-    of ptInline: DisplayInlineTableWrapper
-    of ptBlock: DisplayTableWrapper
-    let innerTable = BlockBox(computed: inner)
-    let box = BlockBox(
-      computed: outer,
-      children: @[CSSBox(innerTable)]
-    )
-    ctx.anonTableWrapper = box
-    return box
-  return ctx.anonTableWrapper
-
-proc createAnonRow(ctx: var BoxBuilderContext; parent: CSSBox): BlockBox =
-  if ctx.anonRow == nil:
-    let wrapperVals = parent.computed.inheritProperties()
-    wrapperVals{"display"} = DisplayTableRow
-    let box = BlockBox(computed: wrapperVals)
-    ctx.anonRow = box
-    return box
-  return ctx.anonRow
-
-proc flushTableRow(ctx: var BoxBuilderContext; parent: CSSBox;
-    parentType: ParentType) =
-  if ctx.anonRow != nil:
-    let parentDisplay = parent.computed{"display"}
-    if parentDisplay in ProperTableRowParent:
-      BlockBox(parent).children.add(ctx.anonRow)
-    else:
-      let anonTableWrapper = ctx.createAnonTable(parentType)
-      BlockBox(anonTableWrapper.children[0]).children.add(ctx.anonRow)
-    ctx.anonRow = nil
-
-proc flushTable(ctx: var BoxBuilderContext; parent: BlockBox) =
-  ctx.flushTableRow(parent, ptBlock)
-  if ctx.anonTableWrapper != nil:
-    parent.children.add(ctx.anonTableWrapper)
-    ctx.anonTableWrapper = nil
-
-proc flushInlineTable(ctx: var BoxBuilderContext; parent: InlineBox) =
-  ctx.flushTableRow(parent, ptInline)
-  if ctx.anonTableWrapper != nil:
-    assert ctx.anonTableWrapper.computed{"display"} == DisplayInlineTableWrapper
-    parent.children.add(InlineBox(
-      t: ibtBox,
-      computed: ctx.anonTableWrapper.computed.inheritProperties(),
-      box: ctx.anonTableWrapper
-    ))
-    ctx.anonTableWrapper = nil
-
-proc buildBlock(pctx: var BoxBuilderContext; styledNode: StyledNode):
-    BlockBox =
+proc buildBlock(ctx: var TreeContext; styledNode: StyledNode): BlockBox =
   let box = BlockBox(computed: styledNode.computed, element: styledNode.element)
-  var ctx = initBoxBuilderContext(styledNode, pctx)
-  ctx.build(box)
-  pctx.quoteLevel = ctx.quoteLevel
+  ctx.build(styledNode, box.children)
   return box
 
-proc buildInlineText(parent: StyledNode; text: CharacterData;
-    computed: CSSValues): InlineBox =
-  return InlineBox(
-    t: ibtText,
-    computed: computed,
-    element: parent.element,
-    text: text
-  )
-
-proc buildInlineText(parent: StyledNode; text: CharacterData): InlineBox =
-  return InlineBox(
-    t: ibtText,
-    computed: parent.computed,
-    element: parent.element,
-    text: text
-  )
-
-proc buildInlineBlock(ctx: var BoxBuilderContext; styledNode: StyledNode):
-    InlineBox =
+proc buildInlineBlock(ctx: var TreeContext; styledNode: StyledNode): InlineBox =
   return InlineBox(
     t: ibtBox,
     computed: styledNode.computed.inheritProperties(),
@@ -3004,313 +2894,51 @@ proc buildInlineBlock(ctx: var BoxBuilderContext; styledNode: StyledNode):
     box: ctx.buildBlock(styledNode)
   )
 
-proc buildListItem(ctx: var BoxBuilderContext; styledNode: StyledNode):
-    BlockBox =
-  inc ctx.listItemCounter
-  let marker = newMarkerBox(styledNode.computed, ctx.listItemCounter)
-  let position = styledNode.computed{"list-style-position"}
-  let content = BlockBox(
-    computed: styledNode.computed,
-    element: styledNode.element
-  )
-  var contentCtx = initBoxBuilderContext(styledNode, ctx.lctx, addr ctx)
-  case position
-  of ListStylePositionOutside:
-    contentCtx.build(content)
-    content.computed = content.computed.copyProperties()
-    content.computed{"display"} = DisplayBlock
-    let markerComputed = marker.computed.copyProperties()
-    markerComputed{"display"} = markerComputed{"display"}.blockify()
-    let marker = BlockBox(
-      computed: markerComputed,
-      children: @[CSSBox(marker)]
-    )
-    return BlockBox(
-      computed: styledNode.computed,
-      children: @[CSSBox(marker), CSSBox(content)]
-    )
-  of ListStylePositionInside:
-    content.children.add(marker)
-    contentCtx.build(content)
-    return content
-
-proc buildFromElem(ctx: var BoxBuilderContext; styledNode: StyledNode):
-    CSSBox =
+proc buildFromElem(ctx: var TreeContext; styledNode: StyledNode): CSSBox =
   return case styledNode.computed{"display"}
   of DisplayBlock, DisplayFlowRoot, DisplayFlex, DisplayTable,
-      DisplayTableCaption, DisplayTableCell, RowGroupBox, DisplayTableRow:
+      DisplayTableCaption, DisplayTableCell, RowGroupBox, DisplayTableRow,
+      DisplayTableWrapper, DisplayListItem:
     ctx.buildBlock(styledNode)
   of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex:
     ctx.buildInlineBlock(styledNode)
-  of DisplayListItem: ctx.buildListItem(styledNode)
   of DisplayInline: ctx.buildInline(styledNode)
-  of DisplayTableColumn, DisplayTableColumnGroup: nil #TODO
-  of DisplayNone: nil
-  of DisplayTableWrapper, DisplayInlineTableWrapper:
+  of DisplayTableColumn, DisplayTableColumnGroup, #TODO
+      DisplayNone:
     assert false
     nil
 
-proc buildReplacement(ctx: var BoxBuilderContext; child: StyledNode;
-    computed: CSSValues): InlineBox =
-  case child.content.t
-  of ContentOpenQuote:
-    let quotes = child.computed{"quotes"}
-    let s = if quotes == nil:
-      quoteStart(ctx.quoteLevel)
-    elif quotes.qs.len > 0:
-      quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].s
-    else:
-      return
-    let node = newCharacterData(s)
-    result = child.buildInlineText(node, computed)
-    inc ctx.quoteLevel
-  of ContentCloseQuote:
-    if ctx.quoteLevel > 0:
-      dec ctx.quoteLevel
-    let quotes = child.computed{"quotes"}
-    let s = if quotes == nil:
-      quoteEnd(ctx.quoteLevel)
-    elif quotes.qs.len > 0:
-      quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].e
-    else:
-      return
-    let text = newCharacterData(s)
-    return child.buildInlineText(text, computed)
-  of ContentNoOpenQuote:
-    inc ctx.quoteLevel
-    return nil
-  of ContentNoCloseQuote:
-    if ctx.quoteLevel > 0:
-      dec ctx.quoteLevel
-    return nil
-  of ContentString:
-    #TODO make CharacterData in cssvalues?
-    let text = newCharacterData(child.content.s)
-    return child.buildInlineText(text, computed)
-  of ContentImage:
-    if child.content.bmp != nil and child.content.bmp.cacheId != -1:
-      return InlineBox(
-        t: ibtBitmap,
-        computed: computed,
-        element: child.element,
-        image: InlineImage(bmp: child.content.bmp)
-      )
-    return child.buildInlineText(ctx.lctx.imgText, computed)
-  of ContentNewline:
-    return InlineBox(
-      t: ibtNewline,
-      computed: computed,
-      element: child.element
-    )
-
-proc buildInline(pctx: var BoxBuilderContext; styledNode: StyledNode):
-    InlineBox =
-  let ibox = InlineBox(
-    t: ibtParent,
-    computed: styledNode.computed,
-    element: styledNode.element
-  )
-  var ctx = initBoxBuilderContext(styledNode, pctx)
-  for child in styledNode.children:
-    case child.t
-    of stElement:
-      let child = ctx.buildFromElem(child)
-      if child != nil:
-        case child.computed{"display"}
-        of DisplayTableRow:
-          ctx.flushTableRow(ibox, ptInline)
-          let anonTableWrapper = ctx.createAnonTable(ptInline)
-          BlockBox(anonTableWrapper.children[0]).children.add(child)
-        of RowGroupBox:
-          ctx.flushTableRow(ibox, ptInline)
-          let anonTableWrapper = ctx.createAnonTable(ptInline)
-          BlockBox(anonTableWrapper.children[0]).children.add(child)
-        of DisplayTableCell:
-          ctx.createAnonRow(ibox).children.add(child)
-        of DisplayTableCaption:
-          ctx.flushTableRow(ibox, ptInline)
-          let anonTableWrapper = ctx.createAnonTable(ptInline)
-          # only add first caption
-          if anonTableWrapper.children.len == 1:
-            anonTableWrapper.children.add(child)
-        else:
-          ctx.flushInlineTable(ibox)
-          ibox.children.add(child)
-    of stText:
-      ctx.flushInlineTable(ibox)
-      ibox.children.add(styledNode.buildInlineText(child.text))
-    of stReplacement:
-      ctx.flushInlineTable(ibox)
-      let child = ctx.buildReplacement(child, styledNode.computed)
-      if child != nil:
-        ibox.children.add(child)
-  ctx.flushInlineTable(ibox)
-  pctx.quoteLevel = ctx.quoteLevel
-  return ibox
-
-proc initBoxBuilderContext(styledNode: StyledNode; lctx: LayoutContext;
-    parent: ptr BoxBuilderContext): BoxBuilderContext =
-  result = BoxBuilderContext(styledNode: styledNode, lctx: lctx)
-  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
-
-proc buildTableRowChildWrappers(box: BlockBox) =
-  var wrapperVals: CSSValues = nil
-  for child in box.children:
-    if child.computed{"display"} != DisplayTableCell:
-      wrapperVals = box.computed.inheritProperties()
-      wrapperVals{"display"} = DisplayTableCell
-      break
-  if wrapperVals != nil:
-    # fixup row: put wrappers around runs of misparented children
-    var children = newSeqOfCap[CSSBox](box.children.len)
-    var wrapper: BlockBox = nil
-    for child in box.children:
-      if child.computed{"display"} != DisplayTableCell:
-        if wrapper == nil:
-          wrapper = BlockBox(computed: wrapperVals)
-          children.add(wrapper)
-        wrapper.children.add(child)
-      else:
-        wrapper = nil
-        children.add(child)
-    box.children = move(children)
-
-proc buildTableRowGroupChildWrappers(box: BlockBox) =
-  var wrapperVals: CSSValues = nil
-  for child in box.children:
-    if child.computed{"display"} != DisplayTableRow:
-      wrapperVals = box.computed.inheritProperties()
-      wrapperVals{"display"} = DisplayTableRow
-      break
-  if wrapperVals != nil:
-    # fixup row group: put wrappers around runs of misparented children
-    var wrapper: BlockBox = nil
-    var children = newSeqOfCap[CSSBox](box.children.len)
-    for child in box.children:
-      if child.computed{"display"} != DisplayTableRow:
-        if wrapper == nil:
-          wrapper = BlockBox(computed: wrapperVals, children: @[child])
-          children.add(wrapper)
-        wrapper.children.add(child)
-      else:
-        if wrapper != nil:
-          wrapper.buildTableRowChildWrappers()
-          wrapper = nil
-        children.add(child)
-    if wrapper != nil:
-      wrapper.buildTableRowChildWrappers()
-    box.children = move(children)
-
-proc buildTableChildWrappers(box: BlockBox; computed: CSSValues) =
-  let innerTable = BlockBox(computed: computed, element: box.element)
-  let wrapperVals = box.computed.inheritProperties()
-  wrapperVals{"display"} = DisplayTableRow
-  var caption: CSSBox = nil
-  var wrapper: BlockBox = nil
-  for child in box.children:
-    if child.computed{"display"} in ProperTableChild:
-      if wrapper != nil:
-        wrapper.buildTableRowChildWrappers()
-        wrapper = nil
-      innerTable.children.add(child)
-    elif child.computed{"display"} == DisplayTableCaption:
-      if caption == nil:
-        caption = child
-    else:
-      if wrapper == nil:
-        wrapper = BlockBox(computed: wrapperVals)
-        innerTable.children.add(wrapper)
-      wrapper.children.add(child)
-  if wrapper != nil:
-    wrapper.buildTableRowChildWrappers()
-  box.children = @[CSSBox(innerTable)]
-  if caption != nil:
-    box.children.add(caption)
-
-# Don't build empty anonymous inline blocks between block boxes
-func canBuildAnonInline(ctx: BoxBuilderContext; box: BlockBox; s: string):
-    bool =
-  return box.children.len > 0 and ctx.anonTableWrapper == nil and
-    ctx.anonRow == nil and
-    box.children[^1].computed{"display"} in DisplayOuterInline or
-    box.computed.whitespacepre or not s.onlyWhitespace()
-
-proc build(ctx: var BoxBuilderContext; box: BlockBox) =
-  let inlineComputed = box.computed.inheritProperties()
-  for child in ctx.styledNode.children:
+proc build(ctx: var TreeContext; styledNode: StyledNode;
+    children: var seq[CSSBox]) =
+  for child in styledNode.children(ctx):
     case child.t
     of stElement:
-      let child = ctx.buildFromElem(child)
-      if child != nil:
-        case child.computed{"display"}
-        of DisplayTableRow:
-          ctx.flushTableRow(box, ptBlock)
-          if box.computed{"display"} in ProperTableRowParent:
-            box.children.add(child)
-          else:
-            let anonTableWrapper = ctx.createAnonTable(ptBlock)
-            BlockBox(anonTableWrapper.children[0]).children.add(child)
-        of RowGroupBox:
-          ctx.flushTableRow(box, ptBlock)
-          if box.computed{"display"} in DisplayInnerTable:
-            box.children.add(child)
-          else:
-            let anonTableWrapper = ctx.createAnonTable(ptBlock)
-            BlockBox(anonTableWrapper.children[0]).children.add(child)
-        of DisplayTableCell:
-          if box.computed{"display"} == DisplayTableRow:
-            box.children.add(child)
-          else:
-            ctx.createAnonRow(box).children.add(child)
-        of DisplayTableCaption:
-          ctx.flushTableRow(box, ptBlock)
-          if box.computed{"display"} in DisplayInnerTable:
-            box.children.add(child)
-          else:
-            let anonTableWrapper = ctx.createAnonTable(ptBlock)
-            # only add first caption
-            if anonTableWrapper.children.len == 1:
-              anonTableWrapper.children.add(child)
-        else:
-          ctx.flushTable(box)
-          box.children.add(child)
+      children.add(ctx.buildFromElem(child))
     of stText:
-      let text = child.text
-      if ctx.canBuildAnonInline(box, text.data):
-        ctx.flushTable(box)
-        let child = ctx.styledNode.buildInlineText(text, inlineComputed)
-        box.children.add(child)
-    of stReplacement:
-      let child = ctx.buildReplacement(child, inlineComputed)
-      if child != nil:
-        ctx.flushTable(box)
-        box.children.add(child)
-  ctx.flushTable(box)
-  case box.computed{"display"}
-  of DisplayInnerFlex:
-    let blockComputed = inlineComputed.copyProperties()
-    blockComputed{"display"} = DisplayBlock
-    for it in box.children.mitems:
-      if it.computed{"display"} in DisplayOuterInline:
-        it = BlockBox(computed: blockComputed, children: @[it])
-  of DisplayInnerTable:
-    let (outer, inner) = box.computed.splitTable()
-    box.computed = outer
-    outer{"display"} = outer{"display"}.toTableWrapper()
-    box.buildTableChildWrappers(inner)
-  of RowGroupBox:
-    box.buildTableRowGroupChildWrappers()
-  of DisplayTableRow:
-    box.buildTableRowChildWrappers()
-  else:
-    discard
+      children.add(InlineBox(
+        t: ibtText,
+        computed: child.computed,
+        element: child.element,
+        text: child.text
+      ))
+    of stBr:
+      children.add(InlineBox(
+        t: ibtNewline,
+        computed: child.computed,
+        element: child.element
+      ))
+    of stImage:
+      children.add(InlineBox(
+        t: ibtBitmap,
+        computed: child.computed,
+        element: child.element,
+        image: InlineImage(bmp: child.bmp)
+      ))
 
 proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
+  let box = BlockBox(computed: root.computed, element: root.element)
+  var ctx = TreeContext()
+  ctx.build(root, box.children)
   let space = availableSpace(
     w = stretch(attrsp[].widthPx),
     h = stretch(attrsp[].heightPx)
@@ -3320,12 +2948,8 @@ proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox =
     cellSize: size(w = attrsp.ppc, h = attrsp.ppl),
     positioned: @[PositionedItem(), PositionedItem()],
     myRootProperties: rootProperties(),
-    imgText: newCharacterData("[img]"),
     luctx: LUContext()
   )
-  let box = BlockBox(computed: root.computed, element: root.element)
-  var ctx = initBoxBuilderContext(root, lctx, nil)
-  ctx.build(box)
   let sizes = lctx.resolveBlockSizes(space, box.computed)
   # the bottom margin is unused.
   lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes)