about summary refs log blame commit diff stats
path: root/src/layout/engine.nim
blob: 88d87bb15c848ddb10be1c3973977b3af7bb02ec (plain) (tree)
1
2
3
4
5
6
7
8
9
           
              
             
              
 

                     
                
               
                

                   
 
             
                                                                  
                                  
 
                         
                     
            
                  

                  
                             
 

                                                                  
 
                                                    
                     
 
                                                     


                                                     
                     

                                                      
                          

                          
                                                                          
                            
                                                                
                                            
                      
                                                  
                                                           

          







                                                                                      
 

                                      
                               




                                                     


                            
                   
 
                                                                                                                        




                                                                       
                             
                                                            
           
                                                            
                     

                                                  
                        
                                              

                                                                                        
                        
                        
                                               

                      
                             







                                                                


                                    


                                          
                                  
 
                                                                    

                                             
 


                                                           
 

                                                           

                               

                                                                        
                                                 
                                                     
                             
                                                           
         


                                                                 

                                                                              



















                                                                                       
 


                                                                      
                               


                                                                        
                             

                                                               
                          

                                              
                             

                                                 
         








                                                                             

                                     




                                                            
 







                                                                                             
 

                                                                        

                        
                                                                    
 

                                                                                                 
                          
                                  
                            
 




                                                                              
 
                                                                              


                                                                              
 




                                                       





                                                                                                            
                        
                 
                                 
                                                              
                                                 
                            
                                          
 
                                        
                                               
 
                 
                                                                      
 
                                           
                                                     
                                                               
                                        



                                           
                                    
 
                                      
                          
                         
                                       
                               
                                                                            
                   
 
                                                     
                                                                                 

                                                            

                                                 
                                                                          
          

                                                     
                          
                                                                                                                   
                     
                                                                  
                                  
                         

                                                                                                                   
                                  
               
 

                                                         
                                    
                                          
                                                               

                                                              
                                                          
         
                                  
 
                                                                                                                  
                        
                           

                           
                   
                                            
                 
 

                                
           
                    
                             
                                     
           
         
                 
                           

                         
                                                          
 
                 
 


                                 

                    








                                                                                                                                 
                 
                            
       
                                                 
 

                                                                   

                                                                    
 



                                                                        
 


                                           
 

                                            
 
                                  
                      
                                 
                                                   
                       

                                                               











                                                                                
 



                                                                                           
 
                                                                                                                                          



                                                          
                                                          
 
                                                               

                                         
                                                                 
 
                                                                              
             
                                                     
 
                                                                                                                                     
             
                                                                                        
 



                                                                                                                               
                                                        
             
                                

                                   
 


                                               
 

                                              
 
                                        
 
                                      
 

                                                                


                                                        
         
                                                        
       
                                   
 
                                                                 

                                                                             
                                                                 
 




                                                          
 
                                     



                                                                     
 
                                    





                                                                                      
 










                                                          
                                    










                                                   
                                                                                                                                          
                               
                                                                
 
                                    
                               
                                                            
       
                                                                              
 
                                        

                              


                                                                               
                                                                   
       
                                                                   
 
                                                        

                                        
 

                                                      
 
                                            
 
                                      


                                               
 
                                                                




























                                                                                                                                       
                                                                                                         
                        
                 
                                                 
 
                                                                         
                                           

                                                    
                                                                           
                      
                                                                                     
 
                                                        
                       
                                                                  

                            
                                  
                      
                                         
                           
                                               
                            
                                              
                                                                              
                                                                         
                                




                                                                                 
         
                                                              
 
                                                                             
                       
                                        
                                                                                                      
 
                                                                           
                                            
 

                                                                              

                         
                                    
                        
                                           
                         
                                                                                
                              
                                                

                                                                               
                              




                                                                               
           
                                                                
                                                  

             
                                                                                
                                      



                                                                      
                                  
                                                      
       

                                                                           













































                                                                                             



                        

                               
 
                       

                                 
                            

                                 

                      
 
                                         
                      
                      


                              
                                         


                                   















                                                                              

                                                     
                     

                                           
                                                    


                                           
           
 




















                                                                              
                                        
                                      

                      



                        
 



                             


                                        
                                   



                      

                                       
 
                                  
 

                                   
 

                                
 

                                                                                                                                          




                                                   

                                                                                                                                   




                                                   

                                                                                                      
           
           
                            

                                                          
                                                                                               

                                                                                             




                                                    
                                              
                                           
                                     


                                

                                                        
                         
                                                             
                                   
                                                                        


                                     


                                                                              







                                        
                                     
                            
         
               




                                                                                                                  
                                        
                  
                         
                        

                                     
                             
                    
                                                                               
                            
                           
                      
                           
          
                      

                                                                                                                                   

                                             
                         

                                              








                                                          
               
            
 




                                                                
                                
                                                                                    

                                  




                                                             
                               




                                                             
                                                             



                                               
                 

                    





                      
 
                                                                                               





                                                    


                                                                          
















                                                                                
                                                                   
                                     





                                                                              
                      






                                                                  




                                                                          
                                             


                                                         

                                      
                                            
                            
                      
                                      
                                             

                                                         
                                    

                                                                      
            
                

                                        
                                              
                              





                                                          
                                      
                        

                            

                                       
                            







                                                
           
                       
                         
                                                          





                                      
                         
                                             
                                        



























                                                                                                            
              
 
                                                                               
                      
                        
                                  


                                                                                   
                                                                  

                             
 
                                                                 
                                                                   
                      
                                  
                      
                                          
       
                                                   
 
                                                          
                                                                   

                                                                      


                                           
       
                                                        


                  
                                                                      




                                                                                                        

                                                                 
             

                                             
 
                                                                            
             

                                                  
 
                                                    
             
                            
                                                    
 
                                                                
             
                            
                                                
 
                                                                                        

                            
                                             
                                            
                                                                          
 
                                                                                            
             
                            
                                                         
 

                                                                
                            
 


                                                                                
                            


                                                                      
                            
 


                                                                        
                            
                    
 



                                                                              
                        
                    
                        
                      
 







                               
                                                                  
                                                                                                                          



                                                   
                                                                  





                                                                          
                                                                  

                                                                                                         
                                                                                            
                            
 
                                                                     
                 
                                                                                                   

                        
 
                                                    
                                                     
                        

                            
                                                                                  
                                                                                                  

                                                                                          
                                                                                                
                                                                                                                   
                                                                            
 
 






























                                                                           


                                       


                                 
 
                                     
                   
                

                                                            

                              

                                                                                      
                                                                              
                                                                                         

                           
                                                                  
                              
                                      
                    

                                       
                          
                
                                                         

                                                                
                   

                                                            

                              























                                                                                     
                        

























                                                                        



                                
                          
 
                                                                              


                                   
                                 
                   



























                                                                                                                  
 
                                                                                                                     
                                            
                                                                          

                   

                         
 


                                  

                                             
                                         

                             


                            
            
 

                                                                                            

                                                                         


                                                                          

            












                                                             
                                                                                          
                                               


                                                                          
                                     
            
 


                                                                       
                                           





                                                      
                                             




                                                                                                    


                                                                          
                                          

            






                                                                                                  













                                                             

                                                                                    


                                                                          
                                  
            
 
                                                                                  
                         
                                               
                                  
import math
import options
import tables
import unicode

import css/stylednode
import css/values
import html/tags
import html/dom
import io/window
import layout/box
import utils/twtstr

# Build phase
func px(l: CSSLength, viewport: Viewport, p = 0): int {.inline.} =
  return px(l, viewport.window, p)

type InlineState = object
  ictx: InlineContext
  skip: bool
  node: StyledNode
  word: InlineWord
  maxwidth: int
  computed: CSSComputedValues

func whitespacepre(computed: CSSComputedValues): bool {.inline.} =
  computed{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_WRAP}

func cellwidth(viewport: Viewport): int {.inline.} =
  viewport.window.ppc

func cellwidth(ictx: InlineContext): int {.inline.} =
  ictx.viewport.cellwidth

func cellheight(viewport: Viewport): int {.inline.} =
  viewport.window.ppl

func cellheight(ictx: InlineContext): int {.inline.} =
  ictx.viewport.cellheight

# Whitespace between words
func computeShift(ictx: InlineContext, computed: CSSComputedValues): int =
  if ictx.whitespacenum > 0:
    if ictx.currentLine.atoms.len > 0 or computed.whitespacepre:
      let spacing = computed{"word-spacing"}
      if spacing.auto:
        return ictx.cellwidth * ictx.whitespacenum
      return spacing.px(ictx.viewport) * ictx.whitespacenum
  return 0

proc applyLineHeight(viewport: Viewport, line: LineBox, computed: CSSComputedValues) =
  #TODO this should be computed during cascading.
  let lineheight = if computed{"line-height"}.auto: # ergo normal
    viewport.cellheight
  else:
    # Percentage: refers to the font size of the element itself.
    computed{"line-height"}.px(viewport, viewport.cellheight)
  line.lineheight = max(lineheight, line.lineheight)

proc newWord(state: var InlineState) =
  let word = InlineWord()
  let format = ComputedFormat()
  let computed = state.computed
  format.color = computed{"color"}
  format.fontstyle = computed{"font-style"}
  format.fontweight = computed{"font-weight"}
  format.textdecoration = computed{"text-decoration"}
  format.node = state.node
  word.format = format
  state.ictx.format = format
  state.word = word

proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComputedValues, maxwidth: int, last = false) =
  let maxwidth = if ictx.shrink:
    ictx.maxwidth
  else:
    maxwidth
  # we don't support directions for now so left = start and right = end
  case 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:
    # move everything
    let x = max(maxwidth, line.width) - line.width
    for atom in line.atoms:
      atom.offset.x += x
  of TEXT_ALIGN_CENTER, TEXT_ALIGN_CHA_CENTER:
    let x = max((max(maxwidth - line.offset.x, line.width)) div 2 - line.width div 2, 0)
    for atom in line.atoms:
      atom.offset.x += x
  of TEXT_ALIGN_JUSTIFY:
    if not computed.whitespacepre and not last:
      var sumwidth = 0
      var spaces = 0
      for atom in line.atoms:
        if atom of InlineSpacing:
          discard
        else:
          inc spaces
          sumwidth += atom.width
      dec spaces
      if spaces > 0:
        let spacingwidth = (ictx.maxwidth - 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

# Align atoms (inline boxes, text, etc.) vertically inside the line.
proc verticalAlignLine(ictx: InlineContext) =
  let line = ictx.currentLine

  # Start with line-height as the baseline and line height.
  line.height = line.lineheight
  line.baseline = line.height

  # Calculate the line's baseline based on atoms' baseline.
  for atom in line.atoms:
    case atom.vertalign.keyword
    of VERTICAL_ALIGN_BASELINE:
      let len = atom.vertalign.length.px(ictx.viewport, line.lineheight)
      line.baseline = max(line.baseline, atom.baseline + len)
    of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM:
      line.baseline = max(line.baseline, atom.height)
    of VERTICAL_ALIGN_MIDDLE:
      line.baseline = max(line.baseline, atom.height div 2)
    else:
      line.baseline = max(line.baseline, atom.baseline)

  # 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:
    # 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
    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(ictx.viewport, line.lineheight)
      line.height = max(line.baseline - atom.baseline + atom.height + len, line.height)
    of VERTICAL_ALIGN_MIDDLE:
      # Line height must be at least
      # (line baseline) + (atom height / 2).
      line.height = max(line.baseline + atom.height div 2, line.height)
    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(line.baseline - atom.baseline + atom.height, line.height)

  # Now we can calculate the actual position of atoms inside the line.
  for atom in line.atoms:
    case atom.vertalign.keyword
    of VERTICAL_ALIGN_BASELINE:
      # Atom is placed at (line baseline) - (atom baseline) - len
      let len = atom.vertalign.length.px(ictx.viewport, line.lineheight)
      atom.offset.y = line.baseline - atom.baseline - len
    of VERTICAL_ALIGN_MIDDLE:
      # Atom is placed at (line baseline) - ((atom height) / 2)
      atom.offset.y = line.baseline - atom.height 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
    else:
      # See baseline (with len = 0).
      atom.offset.y = line.baseline - atom.baseline

  # Finally, find the inline block with the largest block margins, then apply
  # these to the line itself.
  var margin_top = 0
  var margin_bottom = 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 += margin_top

  line.height += margin_top
  line.height += margin_bottom

proc addSpacing(line: LineBox, width, height: int, format: ComputedFormat) {.inline.} =
  let spacing = InlineSpacing(width: width, height: height, baseline: height, format: format)
  spacing.offset.x = line.width
  line.width += spacing.width
  line.atoms.add(spacing)

proc flushWhitespace(ictx: InlineContext, computed: CSSComputedValues) =
  let shift = ictx.computeShift(computed)
  ictx.whitespacenum = 0
  if shift > 0:
    ictx.currentLine.addSpacing(shift, ictx.cellheight, ictx.format)

proc finishLine(ictx: InlineContext, computed: CSSComputedValues, maxwidth: int, force = false) =
  if ictx.currentLine.atoms.len != 0 or force:
    ictx.whitespacenum = 0
    ictx.flushWhitespace(computed)
    ictx.verticalAlignLine()

    let line = ictx.currentLine
    ictx.lines.add(line)
    ictx.height += line.height
    ictx.maxwidth = max(ictx.maxwidth, line.width)
    ictx.currentLine = LineBox(offset: Offset(y: line.offset.y + line.height))

proc finish(ictx: InlineContext, computed: CSSComputedValues, maxwidth: int) =
  ictx.finishLine(computed, maxwidth)
  for line in ictx.lines:
    ictx.horizontalAlignLine(line, computed, maxwidth, line == ictx.lines[^1])

func minwidth(atom: InlineAtom): int =
  if atom of InlineBlockBox:
    return cast[InlineBlockBox](atom).innerbox.minwidth
  return atom.width

# pcomputed: computed values of parent, for white-space: pre, line-height
# computed: computed values of child, for vertical-align
# (TODO: surely there's a better way to do this? like storing pcomputed in ictx
# or something...)
proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, pcomputed, computed: CSSComputedValues) =
  var shift = ictx.computeShift(pcomputed)
  ictx.whitespacenum = 0
  # Line wrapping
  if not pcomputed.whitespacepre:
    if ictx.currentLine.width + atom.width + shift > maxwidth:
      ictx.finishLine(pcomputed, maxwidth, false)
      # Recompute on newline
      shift = ictx.computeShift(pcomputed)

  if atom.width > 0 and atom.height > 0:
    atom.vertalign = computed{"vertical-align"}

    if shift > 0:
      ictx.currentLine.addSpacing(shift, ictx.cellheight, ictx.format)

    atom.offset.x += ictx.currentLine.width
    ictx.minwidth = max(ictx.minwidth, atom.minwidth)
    applyLineHeight(ictx.viewport, ictx.currentLine, pcomputed)
    ictx.currentLine.width += atom.width
    if atom of InlineWord:
      ictx.format = InlineWord(atom).format
    else:
      ictx.format = nil
    ictx.currentLine.atoms.add(atom)

proc addWord(state: var InlineState) =
  if state.word.str != "":
    var word = state.word
    word.height = state.ictx.cellheight
    word.baseline = word.height
    state.ictx.addAtom(word, state.maxwidth, state.computed, state.computed)
    state.newWord()

# Start a new line, even if the previous one is empty
proc flushLine(ictx: InlineContext, computed: CSSComputedValues, maxwidth: int) =
  applyLineHeight(ictx.viewport, ictx.currentLine, computed)
  ictx.finishLine(computed, maxwidth, true)

proc checkWrap(state: var InlineState, r: Rune) =
  if state.computed{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
    return
  let shift = state.ictx.computeShift(state.computed)
  case state.computed{"word-break"}
  of WORD_BREAK_BREAK_ALL:
    if state.ictx.currentLine.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
      state.addWord()
      state.ictx.finishLine(state.computed, state.maxwidth, false)
      state.ictx.whitespacenum = 0
  of WORD_BREAK_KEEP_ALL:
    if state.ictx.currentLine.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
      state.ictx.finishLine(state.computed, state.maxwidth, false)
      state.ictx.whitespacenum = 0
  else: discard

proc processWhitespace(state: var InlineState, c: char) =
  state.addWord()
  case state.computed{"white-space"}
  of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
    state.ictx.whitespacenum = max(state.ictx.whitespacenum, 1)
  of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
    if c == '\n':
      state.ictx.flushLine(state.computed, state.maxwidth)
    else:
      inc state.ictx.whitespacenum

proc renderText*(ictx: InlineContext, str: string, maxwidth: int, computed: CSSComputedValues, node: StyledNode) =
  var state: InlineState
  state.computed = computed
  state.ictx = ictx
  state.maxwidth = maxwidth
  state.node = node
  state.ictx.flushWhitespace(state.computed)
  state.newWord()

  #if str.strip().len > 0:
    #eprint "start", str.strip()
  var i = 0
  while i < str.len:
    if str[i].isWhitespace():
      state.processWhitespace(str[i])
      inc i
    else:
      var r: Rune
      fastRuneAt(str, i, r)
      state.checkWrap(r)
      state.word.str &= r
      state.word.width += r.width() * state.ictx.cellwidth

  state.addWord()

type PreferredDimensions = object
  compwidth: int
  compheight: Option[int]
  margin_top: int
  margin_bottom: int
  margin_left: int
  margin_right: int
  padding_left: int
  padding_right: int
  padding_top: int
  padding_bottom: int

proc preferredDimensions(computed: CSSComputedValues, viewport: Viewport, width: int, height: Option[int]): PreferredDimensions =
  let pwidth = computed{"width"}
  if pwidth.auto:
    result.compwidth = width
  else:
    result.compwidth = pwidth.px(viewport, width)

  result.margin_top = computed{"margin-top"}.px(viewport, width)
  result.margin_bottom = computed{"margin-top"}.px(viewport, width)
  result.margin_left = computed{"margin-left"}.px(viewport, width)
  result.margin_right = computed{"margin-right"}.px(viewport, width)

  result.padding_top = computed{"padding-top"}.px(viewport, width)
  result.padding_bottom = computed{"padding-bottom"}.px(viewport, width)
  result.padding_left = computed{"padding-left"}.px(viewport, width)
  result.padding_right = computed{"padding-right"}.px(viewport, width)

  if result.compwidth >= width:
    result.compwidth -= result.margin_left
    result.compwidth -= result.margin_right

    result.compwidth -= result.padding_left
    result.compwidth -= result.padding_right

  let pheight = computed{"height"}
  if not pheight.auto:
    if pheight.unit != UNIT_PERC:
      result.compheight = pheight.px(viewport).some
    elif height.issome:
      result.compheight = pheight.px(viewport, height.get).some

proc setPreferredDimensions(box: BlockBox, width: int, height: Option[int]) =
  let preferred = preferredDimensions(box.computed, box.viewport, width, height)
  box.compwidth = preferred.compwidth
  box.compheight = preferred.compheight
  box.padding_top = preferred.padding_top
  box.padding_bottom = preferred.padding_bottom
  box.padding_left = preferred.padding_left
  box.padding_right = preferred.padding_right
  box.margin_top = preferred.margin_top
  box.margin_bottom = preferred.margin_bottom
  box.margin_left = preferred.margin_left
  box.margin_right = preferred.margin_right

proc newBlockBox_common2(box: BlockBox, parent: BlockBox, builder: BoxBuilder) {.inline.} =
  box.viewport = parent.viewport
  box.computed = builder.computed
  box.setPreferredDimensions(parent.compwidth, parent.compheight)

proc newFlowRootBox(viewport: Viewport, box: BoxBuilder, parentWidth: int, parentHeight = none(int), shrink = true): BlockBox {.inline.} =
  new(result)
  result.viewport = viewport
  result.computed = box.computed
  result.setPreferredDimensions(parentWidth, parentHeight)
  result.shrink = result.computed{"width"}.auto and shrink

proc newBlockBox(parent: BlockBox, box: BoxBuilder): BlockBox =
  new(result)
  result.newBlockBox_common2(parent, box)
  result.shrink = result.computed{"width"}.auto and parent.shrink

proc newListItem(parent: BlockBox, builder: ListItemBoxBuilder): ListItemBox =
  new(result)
  result.newBlockBox_common2(parent, builder.content)

proc newInlineBlock(viewport: Viewport, builder: InlineBlockBoxBuilder, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
  new(result)
  result.innerbox = newFlowRootBox(viewport, builder.content, parentWidth, parentHeight)

proc newInlineTable(viewport: Viewport, builder: TableBoxBuilder, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
  new(result)
  result.innerbox = newFlowRootBox(viewport, builder, parentWidth, parentHeight)

proc newInlineContext(parent: BlockBox): InlineContext =
  new(result)
  result.currentLine = LineBox()
  result.viewport = parent.viewport
  result.shrink = parent.shrink

proc positionInlines(parent: BlockBox) =
  parent.width += parent.padding_left
  parent.inline.offset.x += parent.padding_left

  parent.height += parent.padding_top
  parent.inline.offset.y += parent.padding_top

  parent.height += parent.padding_bottom

  parent.width += parent.padding_right

  parent.minwidth = max(parent.minwidth, parent.inline.minwidth)

  if parent.computed{"width"}.auto:
    if parent.shrink:
      parent.width = min(parent.width, parent.compwidth)
    else:
      parent.width = max(parent.width, parent.compwidth)
  else:
    parent.width = parent.compwidth

proc buildBlock(box: BlockBoxBuilder, parent: BlockBox): BlockBox
proc buildInlines(parent: BlockBox, inlines: seq[BoxBuilder]): InlineContext
proc buildBlocks(parent: BlockBox, blocks: seq[BoxBuilder], node: StyledNode)
proc buildTable(box: TableBoxBuilder, parent: BlockBox): BlockBox

proc applyInlineDimensions(parent: BlockBox) =
  parent.height += parent.inline.height
  if parent.compheight.issome:
    parent.height = parent.compheight.get
  parent.width = max(parent.width, parent.inline.maxwidth)

# Builder only contains inline boxes.
proc buildInlineLayout(parent: BlockBox, children: seq[BoxBuilder]) =
  parent.inline = parent.buildInlines(children)
  parent.applyInlineDimensions()
  parent.positionInlines()

# Builder only contains block boxes.
proc buildBlockLayout(parent: BlockBox, children: seq[BoxBuilder], node: StyledNode) =
  if parent.computed{"position"} == POSITION_ABSOLUTE:
    parent.viewport.absolutes.add(parent)
  parent.buildBlocks(children, node)
  if parent.computed{"position"} == POSITION_ABSOLUTE:
    discard parent.viewport.absolutes.pop()

#TODO this is horribly inefficient
func firstBaseline(bctx: BlockBox): int =
  if bctx.inline != nil:
    if bctx.inline.lines.len > 0:
      return bctx.offset.y + bctx.inline.lines[0].baseline
    return bctx.offset.y
  if bctx.nested.len > 0:
    return bctx.offset.y + bctx.nested[^1].firstBaseline
  bctx.offset.y

#TODO ditto
func baseline(bctx: BlockBox): int =
  if bctx.inline != nil:
    var y = 0
    for line in bctx.inline.lines:
      if line == bctx.inline.lines[^1]:
        return bctx.offset.y + y + line.baseline
      y += line.height
    return bctx.offset.y
  if bctx.nested.len > 0:
    return bctx.offset.y + bctx.nested[^1].baseline
  bctx.offset.y

proc buildInlineBlock(builder: InlineBlockBoxBuilder, parent: InlineContext, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
  assert builder.content != nil
  result = newInlineBlock(parent.viewport, builder, parentWidth)

  let blockbuilder = builder.content
  if blockbuilder.inlinelayout:
    result.innerbox.buildInlineLayout(blockbuilder.children)
  else:
    result.innerbox.buildBlockLayout(blockbuilder.children, blockbuilder.node)

  let pwidth = builder.computed{"width"}
  if pwidth.auto:
    # Half-baked shrink-to-fit
    # Currently the misery that is determining content width is deferred to the
    # inline layouting algorithm, which doesn't work that great but that's what
    # we have.
    result.innerbox.width = min(parentWidth, result.innerbox.width)
  else:
    result.innerbox.width = pwidth.px(parent.viewport, parentWidth)

  # 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

# Copy-pasted wholesale from above. TODO generalize this somehow
proc buildInlineTableBox(builder: TableBoxBuilder, parent: InlineContext, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
  result = newInlineTable(parent.viewport, builder, parentWidth)

  result.innerbox.nested.add(buildTable(builder, result.innerbox))

  let pwidth = builder.computed{"width"}
  if pwidth.auto:
    # Half-baked shrink-to-fit
    # Currently the misery that is determining content width is deferred to the
    # inline layouting algorithm, which doesn't work that great but that's what
    # we have.
    result.innerbox.width = min(parentWidth, result.innerbox.width)
  else:
    result.innerbox.width = pwidth.px(parent.viewport, parentWidth)

  # 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

proc buildInline(viewport: Viewport, box: InlineBoxBuilder, parentWidth: int, parentHeight = none(int)) =
  assert box.ictx != nil
  if box.newline:
    box.ictx.flushLine(box.computed, parentWidth)

  let margin_left = box.computed{"margin-left"}.px(viewport, parentWidth)
  box.ictx.currentLine.width += margin_left

  let paddingformat = ComputedFormat(node: box.node)
  let padding_left = box.computed{"padding-left"}.px(viewport, parentWidth)
  if padding_left > 0:
    box.ictx.currentLine.addSpacing(padding_left, box.ictx.cellheight, paddingformat)

  assert not (box.children.len > 0 and box.text.len > 0)
  for text in box.text:
    box.ictx.renderText(text, parentWidth, box.computed, box.node)

  for child in box.children:
    case child.computed{"display"}
    of DISPLAY_INLINE:
      let child = InlineBoxBuilder(child)
      child.ictx = box.ictx
      buildInline(viewport, child, parentWidth)
    of DISPLAY_INLINE_BLOCK:
      let child = InlineBlockBoxBuilder(child)
      let iblock = child.buildInlineBlock(box.ictx, parentWidth, parentHeight)
      box.ictx.addAtom(iblock, parentWidth, box.computed, child.computed)
      box.ictx.whitespacenum = 0
    of DISPLAY_INLINE_TABLE:
      let child = TableBoxBuilder(child)
      let iblock = child.buildInlineTableBox(box.ictx, parentWidth, parentHeight)
      box.ictx.addAtom(iblock, parentWidth, box.computed, child.computed)
      box.ictx.whitespacenum = 0
    else:
      assert false, "child.t is " & $child.computed{"display"}

  let padding_right = box.computed{"padding-right"}.px(viewport, parentWidth)
  if padding_right > 0:
    # I don't like this, but it works...
    box.ictx.currentLine.addSpacing(padding_right, max(box.ictx.currentLine.height, 1), paddingformat)

  let margin_right = box.computed{"margin-right"}.px(viewport, parentWidth)
  box.ictx.currentLine.width += margin_right

proc buildInlines(parent: BlockBox, inlines: seq[BoxBuilder]): InlineContext =
  let ictx = parent.newInlineContext()
  if inlines.len > 0:
    for child in inlines:
      case child.computed{"display"}
      of DISPLAY_INLINE:
        let child = InlineBoxBuilder(child)
        child.ictx = ictx
        buildInline(parent.viewport, child, parent.compwidth, parent.compheight)
      of DISPLAY_INLINE_BLOCK:
        let child = InlineBlockBoxBuilder(child)
        let iblock = child.buildInlineBlock(ictx, parent.compwidth)
        ictx.addAtom(iblock, parent.compwidth, parent.computed, child.computed)
        ictx.whitespacenum = 0
      of DISPLAY_INLINE_TABLE:
        let child = TableBoxBuilder(child)
        let iblock = child.buildInlineTableBox(ictx, parent.compwidth)
        ictx.addAtom(iblock, parent.compwidth, parent.computed, child.computed)
        ictx.whitespacenum = 0
      else:
        assert false, "child.t is " & $child.computed{"display"}
    ictx.finish(parent.computed, parent.compwidth)
  return ictx

proc buildListItem(builder: ListItemBoxBuilder, parent: BlockBox): ListItemBox =
  result = parent.newListItem(builder)

  if builder.marker != nil:
    result.marker = result.buildInlines(@[BoxBuilder(builder.marker)])

  if builder.content.inlinelayout:
    result.buildInlineLayout(builder.content.children)
  else:
    result.buildBlockLayout(builder.content.children, builder.content.node)

proc positionFixed(box: BlockBox, last: BlockBox = box.viewport.root[0]) =
  #TODO for now this is a good approximation, but fixed actually means
  # something completely different...
  box.offset.x += last.offset.x
  box.offset.y += last.offset.y
  #TODO TODO TODO subtract these from width/height
  let left = box.computed{"left"}
  let right = box.computed{"right"}
  let top = box.computed{"top"}
  let bottom = box.computed{"bottom"}
  if not left.auto:
    box.offset.x += left.px(box.viewport, last.compwidth)
    box.offset.x += box.margin_left
  elif not right.auto:
    box.offset.x += last.width - right.px(box.viewport, box.compwidth) - box.width
    box.offset.x -= box.margin_right
  if not top.auto:
    box.offset.y += top.px(box.viewport, box.compheight.get(0))
    box.offset.y += box.margin_top
  elif not bottom.auto:
    box.offset.y += last.height - bottom.px(box.viewport, box.compheight.get(0)) - box.height
    box.offset.y -= box.margin_bottom
  box.viewport.root.add(box)

proc positionAbsolute(box: BlockBox) =
  let last = if box.viewport.absolutes.len > 0:
    box.viewport.absolutes[^1]
  else:
    box.viewport.root[0]
  box.positionFixed(last)

proc positionRelative(parent, box: BlockBox) =
  let left = box.computed{"left"}
  let right = box.computed{"right"}
  let top = box.computed{"top"}
  let bottom = box.computed{"bottom"}
  if not left.auto:
    box.offset.x += right.px(parent.viewport)
  elif not right.auto:
    box.offset.x += parent.width - right.px(parent.viewport) - box.width
  if not top.auto:
    box.offset.y += top.px(parent.viewport)
  elif not top.auto:
    box.offset.y -= parent.height - bottom.px(parent.viewport) - box.height

proc positionBlocks(box: BlockBox) =
  var y = 0
  var x = 0
  var margin_todo: Strut

  y += box.padding_top
  box.height += box.padding_top

  x += box.padding_left
  case box.computed{"text-align"}
  of TEXT_ALIGN_CHA_CENTER:
    x += box.compwidth div 2
  of TEXT_ALIGN_CHA_LEFT: discard
  of TEXT_ALIGN_CHA_RIGHT:
    x += box.compwidth
  else: discard

  template apply_child(child: BlockBox) =
    child.offset.y = y
    child.offset.x = x
    case
    box.computed{"text-align"}
    of TEXT_ALIGN_CHA_CENTER:
      child.offset.x -= child.width div 2
    of TEXT_ALIGN_CHA_LEFT: discard
    of TEXT_ALIGN_CHA_RIGHT:
      child.offset.x -= child.width
    elif not child.computed{"width"}.auto and child.compwidth < box.compwidth:
      let margin_left = child.computed{"margin-left"}
      let margin_right = child.computed{"margin-right"}
      if margin_left.auto and margin_right.auto:
        child.margin_left += box.compwidth div 2
        child.margin_left -= child.compwidth div 2
        child.margin_right += box.compwidth div 2
        child.margin_right -= child.compwidth div 2
      elif margin_left.auto:
        child.margin_left += box.compwidth
        child.margin_left -= child.compwidth
      elif margin_right.auto:
        child.margin_right += box.compwidth
        child.margin_right -= child.compwidth
    child.offset.x += child.margin_left

    if box.computed{"position"} == POSITION_RELATIVE:
      box.positionRelative(child)
    y += child.height
    box.height += child.height
    box.width = max(box.width, child.width)
    box.minwidth = max(box.minwidth, child.minwidth)
    margin_todo = Strut()
    margin_todo.append(child.margin_bottom)

  var i = 0

  template position_out_of_flow() =
    # Skip absolute, fixed, sticky
    while i < box.nested.len:
      case box.nested[i].computed{"position"}
      of POSITION_STATIC, POSITION_RELATIVE:
        break
      of POSITION_ABSOLUTE:
        positionAbsolute(box.nested[i])
      of POSITION_FIXED:
        box.positionFixed(box.nested[i])
      of POSITION_STICKY:
        #TODO implement this once relayouting every scroll isn't too expensive
        break
      box.nested.delete(i)

  position_out_of_flow

  if i < box.nested.len:
    let child = box.nested[i]

    margin_todo.append(box.margin_top)
    margin_todo.append(child.margin_top)
    box.margin_top = margin_todo.sum()

    apply_child(child)
    inc i

  while true:
    position_out_of_flow

    if i >= box.nested.len:
      break

    let child = box.nested[i]

    margin_todo.append(child.margin_top)
    y += margin_todo.sum()
    box.height += margin_todo.sum()

    apply_child(child)
    inc i

  margin_todo.append(box.margin_bottom)
  box.margin_bottom = margin_todo.sum()

  box.height += box.padding_bottom

  if box.compheight.issome:
    box.height = box.compheight.get

  box.width += box.padding_left
  box.width += box.padding_right

proc buildTableCaption(viewport: Viewport, box: TableCaptionBoxBuilder, maxwidth: int, maxheight: Option[int], shrink = false): BlockBox =
  result = viewport.newFlowRootBox(box, maxwidth, maxheight, shrink)
  if box.inlinelayout:
    result.buildInlineLayout(box.children)
  else:
    result.buildBlockLayout(box.children, box.node)

proc buildTableCell(viewport: Viewport, box: TableCellBoxBuilder, maxwidth: int, maxheight: Option[int], shrink = true): BlockBox =
  result = viewport.newFlowRootBox(box, maxwidth, maxheight, shrink)
  if box.inlinelayout:
    result.buildInlineLayout(box.children)
  else:
    result.buildBlockLayout(box.children, box.node)

proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: BlockBox): RowContext =
  var ctx = RowContext(builder: box, cells: newSeq[CellWrapper](box.children.len))
  var n = 0
  var i = 0
  for child in box.children:
    assert child.computed{"display"} == DISPLAY_TABLE_CELL
    let cellbuilder = TableCellBoxBuilder(child)
    let cell = parent.viewport.buildTableCell(cellbuilder, parent.compwidth, parent.compheight)
    ctx.cells[i] = CellWrapper(box: cell, builder: cellbuilder, colspan: cellbuilder.colspan)
    let pwidth = cellbuilder.computed{"width"}
    if pctx.cols.len < n + cellbuilder.colspan:
      pctx.cols.setLen(n + cellbuilder.colspan)
    if ctx.reflow.len < n + cellbuilder.colspan:
      ctx.reflow.setLen(n + cellbuilder.colspan)
    let minw = cell.minwidth div cellbuilder.colspan
    let w = cell.width div cellbuilder.colspan
    for i in n ..< n + cellbuilder.colspan:
      ctx.width += pctx.inlinespacing
      pctx.cols[i].maxwidth = w
      if pctx.cols[i].width < w:
        pctx.cols[i].width = w
        if ctx.reflow.len <= i: ctx.reflow.setLen(i + 1)
        ctx.reflow[i] = true
      if not pwidth.auto:
        let ww = pwidth.px(parent.viewport, parent.compwidth)
        if pctx.cols[i].wspecified:
          # A specified column already exists; we take the larger width.
          if ww > pctx.cols[i].width:
            pctx.cols[i].width = ww
            ctx.reflow[i] = true
        else:
          # This is the first specified column. Replace colwidth with whatever
          # we have.
          pctx.cols[i].wspecified = true
          pctx.cols[i].width = ww
      if pctx.cols[i].minwidth < minw:
        pctx.cols[i].minwidth = minw
        if pctx.cols[i].width < minw:
          pctx.cols[i].width = minw
          ctx.reflow[i] = true
      ctx.width += pctx.cols[i].width
      ctx.width += pctx.inlinespacing
    n += cellbuilder.colspan
    inc i
  ctx.ncols = n
  return ctx

proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, builder: TableRowBoxBuilder): BlockBox =
  var x = 0
  var n = 0
  let row = newBlockBox(parent, builder)
  var baseline = 0
  for cellw in ctx.cells:
    var cell = cellw.box
    var w = 0
    for i in n ..< n + cellw.colspan:
      w += pctx.cols[i].width
    if cellw.reflow:
      cell = parent.viewport.buildTableCell(cellw.builder, w, none(int), false)
      w = max(w, cell.width)
    x += pctx.inlinespacing
    cell.offset.x += x
    x += pctx.inlinespacing
    x += w
    n += cellw.colspan
    if cell.computed{"vertical-align"}.keyword notin {VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM}: # baseline
      baseline = max(cell.firstBaseline, baseline)
    row.nested.add(cell)
    row.height = max(row.height, cell.height)
  for cell in row.nested:
    cell.height = max(cell.height, row.height)
  for cell in row.nested:
    case cell.computed{"vertical-align"}.keyword
    of VERTICAL_ALIGN_TOP:
      cell.offset.y = 0
    of VERTICAL_ALIGN_MIDDLE:
      cell.offset.y = row.height div 2 - cell.height div 2
    of VERTICAL_ALIGN_BOTTOM:
      cell.offset.y = row.height - cell.height
    else:
      cell.offset.y = baseline - cell.firstBaseline
  row.width = x
  return row

iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} =
  var header: seq[TableRowBoxBuilder]
  var body: seq[TableRowBoxBuilder]
  var footer: seq[TableRowBoxBuilder]
  var caption: TableCaptionBoxBuilder
  for child in builder.children:
    assert child.computed{"display"} in ProperTableChild, $child.computed{"display"}
    case child.computed{"display"}
    of DISPLAY_TABLE_ROW:
      body.add(TableRowBoxBuilder(child))
    of DISPLAY_TABLE_HEADER_GROUP:
      for child in child.children:
        assert child.computed{"display"} == DISPLAY_TABLE_ROW
        header.add(TableRowBoxBuilder(child))
    of DISPLAY_TABLE_ROW_GROUP:
      for child in child.children:
        assert child.computed{"display"} == DISPLAY_TABLE_ROW
        body.add(TableRowBoxBuilder(child))
    of DISPLAY_TABLE_FOOTER_GROUP:
      for child in child.children:
        assert child.computed{"display"} == DISPLAY_TABLE_ROW
        footer.add(TableRowBoxBuilder(child))
    of DISPLAY_TABLE_CAPTION:
      if caption == nil:
        caption = TableCaptionBoxBuilder(child)
    else: discard
  if caption != nil:
    yield caption
  for child in header:
    yield child
  for child in body:
    yield child
  for child in footer:
    yield child

proc calcUnspecifiedColIndices(ctx: var TableContext, dw: int, weight: var float64): seq[int] =
  var avail = newSeqUninitialized[int](ctx.cols.len)
  var i = 0
  var j = 0
  while i < ctx.cols.len:
    if not ctx.cols[i].wspecified:
      avail[j] = i
      let w = sqrt(float64(ctx.cols[i].width) / float64(dw)) * float64(dw)
      ctx.cols[i].weight = w
      weight += w
      inc j
    else:
      avail.del(j)
    inc i
  return avail

# Table layout. We try to emulate w3m's behavior here:
# 1. Calculate minimum and preferred width of each column
# 2. If column width is not auto, set width to max(min_col_width, specified)
# 3. Calculate the maximum preferred row width. If this is
# a) less than the specified table width:
#      distribute (max_row_width - table_width) among cells with an unspecified
#      width.
# b) greater than the specified table width:
#      distribute -(table_width - max_row_width) among cells with an unspecified
#      width. If this would give any cell a width < min_width, distribute the
#      difference too.
proc buildTable(box: TableBoxBuilder, parent: BlockBox): BlockBox =
  let table = parent.newBlockBox(box)
  var ctx = TableContext(
    collapse: table.computed{"border-collapse"} == BORDER_COLLAPSE_COLLAPSE
  )
  if not ctx.collapse:
    ctx.inlinespacing = table.computed{"border-spacing"}.a.px(parent.viewport)
    ctx.blockspacing = table.computed{"border-spacing"}.b.px(parent.viewport)
  for row in box.rows:
    if unlikely(row.computed{"display"} == DISPLAY_TABLE_CAPTION):
      ctx.caption = TableCaptionBoxBuilder(row)
    else:
      let row = TableRowBoxBuilder(row)
      let rctx = ctx.preBuildTableRow(row, table)
      ctx.rows.add(rctx)
      ctx.maxwidth = max(rctx.width, ctx.maxwidth)
  var forceresize = false
  if not table.computed{"width"}.auto:
    forceresize = true
  var reflow = newSeq[bool](ctx.cols.len)
  if table.compwidth > ctx.maxwidth and (not table.shrink or forceresize):
    let dw = (table.compwidth - ctx.maxwidth)
    var weight: float64
    var avail = ctx.calcUnspecifiedColIndices(dw, weight)
    let unit = float64(dw) / weight
    for i in countdown(avail.high, 0):
      let j = avail[i]
      let x = int(unit * ctx.cols[j].weight)
      ctx.cols[j].width += x
      reflow[j] = true
  elif table.compwidth < ctx.maxwidth:
    var dw = (ctx.maxwidth - table.compwidth)
    var weight: float64
    var avail = ctx.calcUnspecifiedColIndices(dw, weight)
    while avail.len > 0 and dw != 0:
      # divide delta width by sum of sqrt(width) for all elem in avail
      let unit = float64(dw) / weight
      dw = 0
      weight = 0
      for i in countdown(avail.high, 0):
        let j = avail[i]
        let x = int(unit * ctx.cols[j].weight)
        ctx.cols[j].width -= x
        if ctx.cols[j].minwidth > ctx.cols[j].width:
          let d = ctx.cols[j].minwidth - ctx.cols[j].width
          dw += d
          ctx.cols[j].width = ctx.cols[j].minwidth
          avail.del(i)
        else:
          weight += ctx.cols[j].weight
        reflow[j] = true
  for col in ctx.cols:
    table.width += col.width
  for i in countdown(ctx.rows.high, 0):
    var row = addr ctx.rows[i]
    var n = ctx.cols.len - 1
    for j in countdown(row.cells.high, 0):
      let m = n - row.cells[j].colspan
      while n > m:
        if reflow[n]:
          row.cells[j].reflow = true
        if n < row.reflow.len and row.reflow[n]:
          reflow[n] = true
        dec n
  var y = 0
  for roww in ctx.rows:
    y += ctx.blockspacing
    let row = ctx.buildTableRow(roww, table, roww.builder)
    row.offset.y += y
    row.offset.x += table.padding_left
    row.width += table.padding_left
    row.width += table.padding_right
    y += ctx.blockspacing
    y += row.height
    table.nested.add(row)
    table.width = max(row.width, table.width)
  table.height = table.compheight.get(y)
  if ctx.caption != nil:
    case ctx.caption.computed{"caption-side"}
    of CAPTION_SIDE_TOP, CAPTION_SIDE_BLOCK_START:
      let caption = table.viewport.buildTableCaption(ctx.caption, table.width, none(int), false)
      for r in table.nested:
        r.offset.y += caption.height
      table.nested.insert(caption, 0)
      table.height += caption.height
      table.width = max(table.width, caption.width)
    of CAPTION_SIDE_BOTTOM, CAPTION_SIDE_BLOCK_END:
      let caption = table.viewport.buildTableCaption(ctx.caption, table.width, none(int), false)
      caption.offset.y += table.width
      table.nested.add(caption)
      table.height += caption.height
      table.width = max(table.width, caption.width)
    of CAPTION_SIDE_LEFT, CAPTION_SIDE_INLINE_START:
      let caption = table.viewport.buildTableCaption(ctx.caption, table.compwidth, some(table.height), true)
      for r in table.nested:
        r.offset.x += caption.width
      table.nested.insert(caption, 0)
      table.width += caption.width
      table.height = max(table.height, caption.height)
    of CAPTION_SIDE_RIGHT, CAPTION_SIDE_INLINE_END:
      let caption = table.viewport.buildTableCaption(ctx.caption, table.compwidth, some(table.height), true)
      caption.offset.x += table.width
      table.nested.add(caption)
      table.width += caption.width
      table.height = max(table.height, caption.height)
  return table

proc buildBlocks(parent: BlockBox, blocks: seq[BoxBuilder], node: StyledNode) =
  for child in blocks:
    var cblock: BlockBox
    case child.computed{"display"}
    of DISPLAY_BLOCK: cblock = buildBlock(BlockBoxBuilder(child), parent)
    of DISPLAY_LIST_ITEM: cblock = buildListItem(ListItemBoxBuilder(child), parent)
    of DISPLAY_TABLE: cblock = buildTable(TableBoxBuilder(child), parent)
    else: assert false, "child.t is " & $child.computed{"display"}
    parent.nested.add(cblock)
  parent.positionBlocks()

# Build a block box inside another block box, based on a builder.
proc buildBlock(box: BlockBoxBuilder, parent: BlockBox): BlockBox =
  assert parent != nil
  result = parent.newBlockBox(box)
  if box.inlinelayout:
    result.buildInlineLayout(box.children)
  else:
    result.buildBlockLayout(box.children, box.node)

# Establish a new flow-root context and build a block box.
proc buildRootBlock(viewport: Viewport, builder: BlockBoxBuilder) =
  let box = viewport.newFlowRootBox(builder, viewport.window.width_px)
  #box.shrink = false
  viewport.root.add(box)
  if builder.inlinelayout:
    box.buildInlineLayout(builder.children)
  else:
    box.buildBlockLayout(builder.children, builder.node)

# Generation phase

#WARNING yes there is a {}= macro but that modifies the computed value
# reference itself and those are copied across arrays...
#TODO figure out something here
proc setDisplay(computed: var CSSComputedValues, display: CSSDisplay) =
  computed[PROPERTY_DISPLAY] = CSSComputedValue(t: PROPERTY_DISPLAY, v: VALUE_DISPLAY, display: display)

# Returns a block box, disregarding the computed value of display
proc getBlockBox(computed: CSSComputedValues): BlockBoxBuilder =
  new(result)
  result.computed = computed.copyProperties()
  result.computed.setDisplay(DISPLAY_BLOCK)

proc getInlineBlockBox(computed: CSSComputedValues): InlineBlockBoxBuilder =
  new(result)
  result.computed = computed.copyProperties()
  result.computed.setDisplay(DISPLAY_INLINE_BLOCK)

proc getTextBox(box: BoxBuilder): InlineBoxBuilder =
  new(result)
  result.inlinelayout = true
  result.computed = box.computed.inheritProperties()

proc getTextBox(computed: CSSComputedValues): InlineBoxBuilder =
  new(result)
  result.inlinelayout = true
  result.computed = computed.inheritProperties()

proc getMarkerBox(computed: CSSComputedValues, listItemCounter: int): MarkerBoxBuilder =
  new(result)
  result.inlinelayout = true
  result.computed = computed.copyProperties()
  result.computed.setDisplay(DISPLAY_INLINE)
  result.text.add(computed{"list-style-type"}.listMarker(listItemCounter))

proc getListItemBox(computed: CSSComputedValues, listItemCounter: int): ListItemBoxBuilder =
  new(result)
  result.computed = computed
  result.marker = getMarkerBox(computed, listItemCounter)

proc getTableBox(computed: CSSComputedValues): TableBoxBuilder =
  new(result)
  result.computed = computed

# Also known as <tbody>.
proc getTableRowGroupBox(computed: CSSComputedValues): TableRowGroupBoxBuilder =
  new(result)
  result.computed = computed

proc getTableRowBox(computed: CSSComputedValues): TableRowBoxBuilder =
  new(result)
  result.computed = computed

# For <th> and <td>.
proc getTableCellBox(computed: CSSComputedValues): TableCellBoxBuilder =
  new(result)
  result.computed = computed
  result.colspan = 1

proc getTableCaptionBox(computed: CSSComputedValues): TableCaptionBoxBuilder =
  new(result)
  result.computed = computed

type BlockGroup = object
  parent: BoxBuilder
  boxes: seq[BoxBuilder]
  listItemCounter: int

type InnerBlockContext = object
  styledNode: StyledNode
  blockgroup: BlockGroup
  viewport: Viewport
  ibox: InlineBoxBuilder
  anonRow: TableRowBoxBuilder
  anonTable: TableBoxBuilder

proc add(blockgroup: var BlockGroup, box: BoxBuilder) {.inline.} =
  assert box.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_TABLE, DISPLAY_INLINE_BLOCK}, $box.computed{"display"}
  blockgroup.boxes.add(box)

proc flush(blockgroup: var BlockGroup) {.inline.} =
  if blockgroup.boxes.len > 0:
    assert blockgroup.parent.computed{"display"} != DISPLAY_INLINE
    let bbox = getBlockBox(blockgroup.parent.computed.inheritProperties())
    bbox.inlinelayout = true
    bbox.children = blockgroup.boxes
    blockgroup.parent.children.add(bbox)
    blockgroup.boxes.setLen(0)

# Don't generate empty anonymous inline blocks between block boxes
func canGenerateAnonymousInline(blockgroup: BlockGroup, computed: CSSComputedValues, str: string): bool =
  return blockgroup.boxes.len > 0 and blockgroup.boxes[^1].computed{"display"} == DISPLAY_INLINE or
    computed{"white-space"} in {WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP} or
    not str.onlyWhitespace()

proc iflush(blockgroup: var BlockGroup, ibox: var InlineBoxBuilder) =
  if ibox != nil:
    assert ibox.computed{"display"} in {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK, DISPLAY_INLINE_TABLE}
    blockgroup.add(ibox)
    ibox = nil

proc newBlockGroup(parent: BoxBuilder): BlockGroup =
  assert parent.computed{"display"} != DISPLAY_INLINE
  result.parent = parent
  result.listItemCounter = 1

proc generateTableBox(styledNode: StyledNode, viewport: Viewport): TableBoxBuilder
proc generateTableRowGroupBox(styledNode: StyledNode, viewport: Viewport): TableRowGroupBoxBuilder
proc generateTableRowBox(styledNode: StyledNode, viewport: Viewport): TableRowBoxBuilder
proc generateTableCellBox(styledNode: StyledNode, viewport: Viewport): TableCellBoxBuilder
proc generateTableCaptionBox(styledNode: StyledNode, viewport: Viewport): TableCaptionBoxBuilder
proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder
proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode)


proc flushTableRow(ctx: var InnerBlockContext) =
  if ctx.anonRow != nil:
    if ctx.blockgroup.parent.computed{"display"} == DISPLAY_TABLE_ROW:
      ctx.blockgroup.parent.children.add(ctx.anonRow)
    else:
      var wrappervals = ctx.styledNode.computed.inheritProperties()
      wrappervals.setDisplay(DISPLAY_TABLE)
      if ctx.anonTable == nil:
        ctx.anonTable = getTableBox(wrappervals)
      ctx.anonTable.children.add(ctx.anonRow)
    ctx.anonRow = nil

proc flushTable(ctx: var InnerBlockContext) =
  ctx.flushTableRow()
  if ctx.anonTable != nil:
    ctx.blockgroup.parent.children.add(ctx.anonTable)

proc iflush(ctx: var InnerBlockContext) =
  ctx.blockgroup.iflush(ctx.ibox)

proc bflush(ctx: var InnerBlockContext) =
  ctx.iflush()
  ctx.blockgroup.flush()

proc flush(ctx: var InnerBlockContext) =
  ctx.blockgroup.flush()
  ctx.flushTableRow()
  ctx.flushTable()

proc generateFromElem(ctx: var InnerBlockContext, styledNode: StyledNode) =
  let box = ctx.blockgroup.parent
  if styledNode.node != nil:
    let elem = Element(styledNode.node)
    if elem.tagType == TAG_BR:
      ctx.ibox = box.getTextBox()
      ctx.ibox.newline = true
      ctx.iflush()

  case styledNode.computed{"display"}
  of DISPLAY_BLOCK:
    ctx.iflush()
    ctx.flush()
    let childbox = styledNode.generateBlockBox(ctx.viewport)
    box.children.add(childbox)
  of DISPLAY_LIST_ITEM:
    ctx.flush()
    let childbox = getListItemBox(styledNode.computed, ctx.blockgroup.listItemCounter)
    if childbox.computed{"list-style-position"} == LIST_STYLE_POSITION_INSIDE:
      childbox.content = styledNode.generateBlockBox(ctx.viewport, some(childbox.marker))
      childbox.marker = nil
    else:
      childbox.content = styledNode.generateBlockBox(ctx.viewport)
    box.children.add(childbox)
    inc ctx.blockgroup.listItemCounter
  of DISPLAY_INLINE:
    ctx.iflush()
    ctx.generateInlineBoxes(styledNode)
  of DISPLAY_INLINE_BLOCK:
    ctx.iflush()
    let childbox = getInlineBlockBox(styledNode.computed)
    childbox.content = styledNode.generateBlockBox(ctx.viewport)
    ctx.blockgroup.add(childbox)
  of DISPLAY_TABLE:
    ctx.flush()
    let childbox = styledNode.generateTableBox(ctx.viewport)
    box.children.add(childbox)
  of DISPLAY_TABLE_ROW:
    ctx.bflush()
    ctx.flushTableRow()
    let childbox = styledNode.generateTableRowBox(ctx.viewport)
    if box.computed{"display"} in ProperTableRowParent:
      box.children.add(childbox)
    else:
      if ctx.anonTable == nil:
        var wrappervals = box.computed.inheritProperties()
        #TODO make this an inline-table if we're in an inline context
        wrappervals.setDisplay(DISPLAY_TABLE)
        ctx.anonTable = getTableBox(wrappervals)
      ctx.anonTable.children.add(childbox)
  of DISPLAY_TABLE_ROW_GROUP, DISPLAY_TABLE_HEADER_GROUP, DISPLAY_TABLE_FOOTER_GROUP:
    ctx.bflush()
    ctx.flushTableRow()
    let childbox = styledNode.generateTableRowGroupBox(ctx.viewport)
    if box.computed{"display"} in {DISPLAY_TABLE, DISPLAY_INLINE_TABLE}:
      box.children.add(childbox)
    else:
      if ctx.anonTable == nil:
        var wrappervals = box.computed.inheritProperties()
        #TODO make this an inline-table if we're in an inline context
        wrappervals.setDisplay(DISPLAY_TABLE)
        ctx.anonTable = getTableBox(wrappervals)
  of DISPLAY_TABLE_CELL:
    ctx.bflush()
    let childbox = styledNode.generateTableCellBox(ctx.viewport)
    if box.computed{"display"} == DISPLAY_TABLE_ROW:
      box.children.add(childbox)
    else:
      if ctx.anonRow == nil:
        var wrappervals = box.computed.inheritProperties()
        wrappervals.setDisplay(DISPLAY_TABLE_ROW)
        ctx.anonRow = getTableRowBox(wrappervals)
      ctx.anonRow.children.add(childbox)
  of DISPLAY_INLINE_TABLE:
    ctx.iflush()
    let childbox = styledNode.generateTableBox(ctx.viewport)
    ctx.blockgroup.add(childbox)
  of DISPLAY_TABLE_CAPTION:
    ctx.bflush()
    ctx.flushTableRow()
    let childbox = styledNode.generateTableCaptionBox(ctx.viewport)
    if box.computed{"display"} in {DISPLAY_TABLE, DISPLAY_INLINE_TABLE}:
      box.children.add(childbox)
    else:
      if ctx.anonTable == nil:
        var wrappervals = box.computed.inheritProperties()
        #TODO make this an inline-table if we're in an inline context
        wrappervals.setDisplay(DISPLAY_TABLE)
        ctx.anonTable = getTableBox(wrappervals)
  of DISPLAY_TABLE_COLUMN:
    discard #TODO
  of DISPLAY_TABLE_COLUMN_GROUP:
    discard #TODO
  of DISPLAY_NONE: discard

proc generateInlineBoxes(ctx: var InnerBlockContext, styledNode: StyledNode) =
  for child in styledNode.children:
    case child.t
    of STYLED_ELEMENT:
      ctx.generateFromElem(child)
    of STYLED_TEXT:
      if ctx.ibox == nil:
        ctx.ibox = getTextBox(styledNode.computed)
        ctx.ibox.node = styledNode
      ctx.ibox.text.add(child.text)
  ctx.iflush()

proc newInnerBlockContext(styledNode: StyledNode, blockgroup: BlockGroup, viewport: Viewport): InnerBlockContext =
  return InnerBlockContext(
    styledNode: styledNode,
    blockgroup: blockgroup,
    viewport: viewport
  )

proc generateInnerBlockBox(ctx: var InnerBlockContext) =
  let box = ctx.blockgroup.parent
  assert box.computed{"display"} != DISPLAY_INLINE
  for child in ctx.styledNode.children:
    case child.t
    of STYLED_ELEMENT:
      ctx.iflush()
      ctx.generateFromElem(child)
    of STYLED_TEXT:
      if canGenerateAnonymousInline(ctx.blockgroup, box.computed, child.text):
        if ctx.ibox == nil:
          ctx.ibox = getTextBox(ctx.styledNode.computed)
          ctx.ibox.node = ctx.styledNode
        ctx.ibox.text.add(child.text)
  ctx.iflush()

proc generateBlockBox(styledNode: StyledNode, viewport: Viewport, marker = none(MarkerBoxBuilder)): BlockBoxBuilder =
  let box = getBlockBox(styledNode.computed)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)

  if marker.issome:
    ctx.ibox = marker.get
    ctx.iflush()

  ctx.generateInnerBlockBox()

  if ctx.blockgroup.boxes.len > 0:
    # Avoid unnecessary anonymous block boxes
    if box.children.len == 0:
      box.children = ctx.blockgroup.boxes
      box.inlinelayout = true
    else:
      ctx.blockgroup.flush()
  ctx.flushTableRow()
  ctx.flushTable()
  return box

proc generateTableCellBox(styledNode: StyledNode, viewport: Viewport): TableCellBoxBuilder =
  let box = getTableCellBox(styledNode.computed)
  if styledNode.node != nil and styledNode.node.nodeType == ELEMENT_NODE:
    box.colspan = Element(styledNode.node).attri("colspan").get(1)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)
  ctx.generateInnerBlockBox()
  ctx.flush()
  return box

proc generateTableRowChildWrappers(box: TableRowBoxBuilder) =
  var newchildren = newSeqOfCap[BoxBuilder](box.children.len)
  var wrappervals = box.computed.inheritProperties()
  wrappervals.setDisplay(DISPLAY_TABLE_CELL)
  for child in box.children:
    if child.computed{"display"} == DISPLAY_TABLE_CELL:
      newchildren.add(child)
    else:
      let wrapper = getTableCellBox(wrappervals)
      wrapper.children.add(child)
      newchildren.add(wrapper)
  box.children = newchildren

proc generateTableRowBox(styledNode: StyledNode, viewport: Viewport): TableRowBoxBuilder =
  let box = getTableRowBox(styledNode.computed)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)
  ctx.generateInnerBlockBox()
  ctx.flush()
  box.generateTableRowChildWrappers()
  return box

proc generateTableRowGroupChildWrappers(box: TableRowGroupBoxBuilder) =
  var newchildren = newSeqOfCap[BoxBuilder](box.children.len)
  var wrappervals = box.computed.inheritProperties()
  wrappervals.setDisplay(DISPLAY_TABLE_ROW)
  for child in box.children:
    if child.computed{"display"} == DISPLAY_TABLE_ROW:
      newchildren.add(child)
    else:
      let wrapper = getTableRowBox(wrappervals)
      wrapper.children.add(child)
      wrapper.generateTableRowChildWrappers()
      newchildren.add(wrapper)
  box.children = newchildren

proc generateTableRowGroupBox(styledNode: StyledNode, viewport: Viewport): TableRowGroupBoxBuilder =
  let box = getTableRowGroupBox(styledNode.computed)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)
  ctx.generateInnerBlockBox()
  ctx.flush()
  box.generateTableRowGroupChildWrappers()
  return box

proc generateTableCaptionBox(styledNode: StyledNode, viewport: Viewport): TableCaptionBoxBuilder =
  let box = getTableCaptionBox(styledNode.computed)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)
  ctx.generateInnerBlockBox()
  ctx.flush()
  return box

proc generateTableChildWrappers(box: TableBoxBuilder) =
  var newchildren = newSeqOfCap[BoxBuilder](box.children.len)
  var wrappervals = box.computed.inheritProperties()
  wrappervals.setDisplay(DISPLAY_TABLE_ROW)
  for child in box.children:
    if child.computed{"display"} in ProperTableChild:
      newchildren.add(child)
    else:
      let wrapper = getTableRowBox(wrappervals)
      wrapper.children.add(child)
      wrapper.generateTableRowChildWrappers()
      newchildren.add(wrapper)
  box.children = newchildren

proc generateTableBox(styledNode: StyledNode, viewport: Viewport): TableBoxBuilder =
  let box = getTableBox(styledNode.computed)
  var ctx = newInnerBlockContext(styledNode, newBlockGroup(box), viewport)
  ctx.generateInnerBlockBox()
  ctx.flush()
  box.generateTableChildWrappers()
  return box

proc renderLayout*(viewport: var Viewport, document: Document, root: StyledNode) =
  viewport.root.setLen(0)
  let builder = root.generateBlockBox(viewport)
  viewport.buildRootBlock(builder)