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

              
                                                                                    
                                      
 
                                                          

                                                        
                                                                  

                                                    
                                                          
                                                         
 


                                                               
                         
                     
            
            

                  
                               
 

















                                                                            

          

                                      
                                 



                                                    
                        
                   
 







                                                               

                                                                                                   
                         

                                                                        
                                                          
                      

                                          
                             
 





                                                                    
 



                                                               
 
                                      
                          
                         
                                       
                                                             
                   
 

                                                     
                                                                 
                  

                                                 

                                                                           
                                                      

                                    
                                                                                                               

                            
                                   
                         
                                                                                                               
                            
                                   
               
 

                                                         

                                          
                                




                                                              
 
                                                                                                              
                        
                             

                           
                   
                 
 

                                
           
                    
                             
                                     
           
         
                 
                           

                         
                                                          
 
                 
 
                                  
                  
 
                                                                              
                                      
                 
                          
       

                                                          
 



                                                                          



                        

                                        




                                                                  
 




                                                                                         
                                                                

                                                                         
                                              
 


                                                                                     
                      

                                                          
                                                         
                                   
                                                                
 


                                                        

                                     
                                                              
 

                                                          
                              
                                 

                      

















                                                                            

                                                                                   

















                                                              

                                                                                    


                                                  


                                     


                                                                                               

                   
                 


                                           



                                                       
                        



                                
                                                                      

                            
                
                      


                                  
                            




                                                 
 


                                                             
                
                      


                                  
                            
                                       
                       
                                                  
         
                                            

                            

                                     
                                          



                                                                                              
 







                                                                                                    


                                                                           

                      
                                        

                                 
                                 
                       
                      


                             
                                                                
                            
                           
                       
 
                                

                   



                                       



                                                            
                            
                      
 



                                                    

                             

                        

                          

                             
                                 

                              






                                                           

                                         
                           

                                                      
 
                                                                              
                             
 

                
 

                                      

                                        




                                              
 






                                           

                                                                                       
                                   

                                    
                                                                                            












                                                                    

                                 
                           



                         


                
                 


                    

                                      





                                           




                                              
 


                       


                            
                                   
              
                           
                                                                                      

                              
                         





                                                     
                      


                                                                         






                                         





                                

                               
 
                                                 
                     
                     
                 
                            
                                                                        





                                                                                                       
                          
                                


                 






                                       
                              
 
            
 


                                                                           
       





                                                               
                           
import unicode
import options

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

func cells_in(l: CSSLength, state: Viewport, d: int, p: Option[int], o: bool): int =
  return cells(l, d, state.term, p, o)

func cells_w(l: CSSLength, state: Viewport, p: int): int =
  return l.cells_in(state, state.term.ppc, p.some, true)

func cells_h(l: CSSLength, state: Viewport, p: Option[int]): int =
  return l.cells_in(state, state.term.ppl, p, false)

func cells_h(l: CSSLength, state: Viewport, p: int): int =
  return l.cells_in(state, state.term.ppl, p.some, false)

func px(l: CSSLength, state: Viewport, p = 0): int {.inline.} =
  return px(l, state.term, p)

type InlineState = object
  ictx: InlineContext
  skip: bool
  node: Node
  word: InlineWord
  maxwidth: int
  specified: CSSSpecifiedValues

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

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

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

# Whitespace between words
func computeShift(ictx: InlineContext, specified: CSSSpecifiedValues): int =
  if ictx.whitespace:
    if ictx.thisrow.atoms.len > 0 or specified.whitespacepre:
      let spacing = specified{"word-spacing"}
      if spacing.auto:
        return ictx.cellwidth
      #return spacing.cells_w(ictx.viewport, 0)
      return spacing.px(ictx.viewport)
  return 0

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

proc finishRow(ictx: InlineContext) =
  if ictx.thisrow.height != 0:
    let oldrow = ictx.thisrow
    ictx.rows.add(oldrow)
    ictx.height += oldrow.height
    ictx.width = max(ictx.width, oldrow.width)
    ictx.thisrow = InlineRow(rely: oldrow.rely + oldrow.height)

proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, specified: CSSSpecifiedValues) =
  var shift = ictx.computeShift(specified)
  ictx.whitespace = false
  # Line wrapping
  if specified{"white-space"} notin {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
    if ictx.thisrow.width + atom.width + shift > maxwidth:
      ictx.finishRow()
      # Recompute on newline
      shift = ictx.computeShift(specified)
      ictx.whitespace = false

  if atom.width > 0 and atom.height > 0:
    if shift > 0:
      let spacing = InlineSpacing(width: shift, height: atom.height)
      spacing.relx = ictx.thisrow.width
      ictx.thisrow.width += spacing.width
      ictx.thisrow.atoms.add(spacing)

    atom.relx += ictx.thisrow.width
    ictx.thisrow.width += atom.width
    ictx.thisrow.height = max(ictx.thisrow.height, atom.height)
    ictx.thisrow.atoms.add(atom)

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

# Start a new line, even if the previous one is empty
proc flushLine(ictx: InlineContext) =
  ictx.thisrow.height = max(ictx.thisrow.height, ictx.cellheight)
  ictx.finishRow()

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

proc processWhitespace(state: var InlineState, c: char) =
  state.addWord()
  case state.specified{"white-space"}
  of WHITESPACE_NORMAL, WHITESPACE_NOWRAP:
    state.ictx.whitespace = true
  of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
    if c == '\n':
      state.ictx.flushLine()
    else:
      state.ictx.whitespace = true

proc renderText*(ictx: InlineContext, str: string, maxwidth: int, specified: CSSSpecifiedValues, node: Node) =
  var state: InlineState
  state.specified = specified
  state.ictx = ictx
  state.maxwidth = maxwidth
  state.node = node
  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()

proc finish(ictx: InlineContext) =
  ictx.finishRow()

proc computedDimensions(bctx: BlockContext, width: int, height: Option[int]) =
  let pwidth = bctx.specified{"width"}
  if pwidth.auto:
    bctx.compwidth = width
  else:
    #bctx.compwidth = pwidth.cells_w(bctx.viewport, width)
    bctx.compwidth = pwidth.px(bctx.viewport, width)

  #let mlef = bctx.specified{"margin-left"}.cells_w(bctx.viewport, width)
  #let mrig = bctx.specified{"margin-right"}.cells_w(bctx.viewport, width)
  let mlef = bctx.specified{"margin-left"}.px(bctx.viewport, width)
  let mrig = bctx.specified{"margin-right"}.px(bctx.viewport, width)
  bctx.relx = mlef
  bctx.compwidth -= mlef
  bctx.compwidth -= mrig

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

proc newBlockContext_common(parent: BlockContext, box: CSSBox): BlockContext {.inline.} =
  new(result)

  result.viewport = parent.viewport
  result.specified = box.specified
  result.computedDimensions(parent.compwidth, parent.compheight)

proc newBlockContext(parent: BlockContext, box: BlockBox): BlockContext =
  result = newBlockContext_common(parent, box)

proc newInlineBlockContext(parent: BlockContext, box: InlineBlockBox): BlockContext =
  newBlockContext_common(parent, box)

# Anonymous block box.
proc newBlockContext(parent: BlockContext): BlockContext =
  new(result)
  result.specified = parent.specified.inheritProperties()
  result.viewport = parent.viewport
  result.computedDimensions(parent.compwidth, parent.compheight)

# Anonymous block box (root).
proc newBlockContext(viewport: Viewport): BlockContext =
  new(result)
  result.specified = rootProperties()
  result.viewport = viewport
  result.computedDimensions(viewport.term.width_px, none(int))

proc newInlineContext(bctx: BlockContext): InlineContext =
  new(result)
  result.thisrow = InlineRow()
  result.viewport = bctx.viewport
  bctx.inline = result

# Blocks' positions do not have to be arranged if alignBlocks is called with
# children, whence the separate procedure.
proc arrangeBlocks(bctx: BlockContext) =
  var y = 0
  var margin_todo = 0

  template apply_child(child: BlockContext) =
    child.rely = y
    y += child.height
    bctx.height += child.height
    bctx.width = max(bctx.width, child.width)
    margin_todo = child.margin_bottom

  var i = 0
  if i < bctx.nested.len:
    let child = bctx.nested[i]

    bctx.margin_top = child.margin_top
    #let mtop = bctx.specified{"margin-top"}.cells_h(bctx.viewport, bctx.compwidth)
    let mtop = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
    if mtop > bctx.margin_top or mtop < 0:
      bctx.margin_top = mtop - bctx.margin_top

    apply_child(child)
    inc i

  while i < bctx.nested.len:
    let child = bctx.nested[i]

    if child.margin_top > margin_todo or child.margin_top < 0:
      margin_todo += child.margin_top - margin_todo
    y += margin_todo
    bctx.height += margin_todo

    apply_child(child)
    inc i

  bctx.margin_bottom = margin_todo
  #let mbot = bctx.specified{"margin-bottom"}.cells_h(bctx.viewport, bctx.compwidth)
  let mbot = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)
  if mbot > bctx.margin_bottom or mbot < 0:
    bctx.margin_bottom = mbot - bctx.margin_bottom

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

proc alignBlock(box: BlockBox)

proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox, parentcss: CSSSpecifiedValues) =
  if box.bctx.done:
    return
  alignBlock(box)
  box.bctx.rely += box.bctx.margin_top
  box.bctx.height += box.bctx.margin_top
  box.bctx.height += box.bctx.margin_bottom
  box.ictx.addAtom(box.bctx, bctx.compwidth, parentcss)
  box.ictx.whitespace = false

proc alignInline(bctx: BlockContext, box: InlineBox) =
  assert box.ictx != nil
  if box.newline:
    box.ictx.flushLine()
  for text in box.text:
    assert box.children.len == 0
    box.ictx.renderText(text, bctx.compwidth, box.specified, box.node)

  for child in box.children:
    case child.t
    of DISPLAY_INLINE:
      let child = InlineBox(child)
      child.ictx = box.ictx
      bctx.alignInline(child)
    of DISPLAY_INLINE_BLOCK:
      let child = InlineBlockBox(child)
      child.ictx = box.ictx
      bctx.alignInlineBlock(child, box.specified)
    else:
      assert false, "child.t is " & $child.t

proc alignInlines(bctx: BlockContext, inlines: seq[CSSBox]) =
  let ictx = bctx.newInlineContext()
  for child in inlines:
    case child.t
    of DISPLAY_INLINE:
      let child = InlineBox(child)
      child.ictx = ictx
      bctx.alignInline(child)
    of DISPLAY_INLINE_BLOCK:
      let child = InlineBlockBox(child)
      child.ictx = ictx
      bctx.alignInlineBlock(child, bctx.specified)
    else:
      assert false, "child.t is " & $child.t
  ictx.finish()
  bctx.height += ictx.height
  if bctx.compheight.issome:
    bctx.height = bctx.compheight.get
  bctx.width = max(ictx.width, ictx.width)
  #bctx.margin_top = bctx.specified{"margin-top"}.cells_h(bctx.viewport, bctx.compwidth)
  #bctx.margin_bottom = bctx.specified{"margin-bottom"}.cells_h(bctx.viewport, bctx.compwidth)
  bctx.margin_top = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
  bctx.margin_bottom = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)

template flush_group() =
  if blockgroup.len > 0:
    let gctx = newBlockContext(bctx)
    gctx.alignInlines(blockgroup)
    blockgroup.setLen(0)
    bctx.nested.add(gctx)

proc alignBlocks(bctx: BlockContext, blocks: seq[CSSBox], blockgroup: var seq[CSSBox], node: Node) =
  # Box contains block boxes.
  # If present, group inline boxes together in anonymous block boxes. Place
  # block boxes inbetween these.
  for child in blocks:
    case child.t
    of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
      let child = BlockBox(child)
      flush_group()
      bctx.nested.add(child.bctx)
      alignBlock(child)
    of DISPLAY_INLINE:
      if child.inlinelayout:
        blockgroup.add(child)
      else:
        bctx.alignBlocks(child.children, blockgroup, child.node)
    of DISPLAY_INLINE_BLOCK:
      blockgroup.add(child)
    else: discard #TODO

proc alignBlock(box: BlockBox) =
  if box.bctx.done:
    return
  if box.inlinelayout:
    # Box only contains inline boxes.
    box.bctx.alignInlines(box.children)
  else:
    var blockgroup: seq[CSSBox]
    box.bctx.alignBlocks(box.children, blockgroup, box.node)
    let bctx = box.bctx
    flush_group()
    box.bctx.arrangeBlocks()
  box.bctx.done = true

proc getBox(specified: CSSSpecifiedValues): CSSBox =
  case specified{"display"}
  of DISPLAY_BLOCK:
    result = BlockBox()
  of DISPLAY_INLINE_BLOCK:
    result = InlineBlockBox()
  of DISPLAY_INLINE:
    result = InlineBox()
  of DISPLAY_LIST_ITEM:
    result = ListItemBox()
  of DISPLAY_NONE: return nil
  else: return nil
  result.t = specified{"display"}
  result.specified = specified

# Returns a block box, disregarding the specified value
proc getBlockBox(specified: CSSSpecifiedValues): BlockBox =
  new(result)
  result.t = DISPLAY_BLOCK
  result.specified = specified.copyProperties()
  result.specified{"display"} = DISPLAY_BLOCK

proc getTextBox(box: CSSBox): InlineBox =
  new(result)
  result.t = DISPLAY_INLINE
  result.inlinelayout = true
  result.specified = box.specified.inheritProperties()

proc getPseudoBox(bctx: BlockContext, specified: CSSSpecifiedValues): CSSBox =
  let box = getBox(specified)

  if box == nil:
    return nil

  case box.specified{"display"}
  of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
    let box = BlockBox(box)
    box.bctx = bctx.newBlockContext(box)
  of DISPLAY_INLINE_BLOCK:
    let box = InlineBlockBox(box)
    box.bctx = bctx.newInlineBlockContext(box)
  else:
    discard

  box.inlinelayout = true
  if specified{"content"}.len > 0:
    let content = getTextBox(box)
    content.text.add($specified{"content"})
    box.children.add(content)
  return box

proc generateBox(elem: Element, viewport: Viewport, bctx: BlockContext = nil): CSSBox =
  elem.rendered = true
  if viewport.map[elem.uid] != nil:
    let box = viewport.map[elem.uid]
    var bctx = bctx
    if box.specified{"display"} in {DISPLAY_BLOCK, DISPLAY_LIST_ITEM, DISPLAY_INLINE_BLOCK}:
      let box = BlockBox(box)
      if bctx == nil:
        box.bctx = viewport.newBlockContext()
      else:
        box.bctx = bctx.newBlockContext(box)
      bctx = box.bctx

    var i = 0
    while i < box.children.len:
      let child = box.children[i]
      if child.element != nil:
        box.children[i] = generateBox(child.element, viewport, bctx)
      inc i
    return viewport.map[elem.uid]

  let box = if bctx != nil:
    getBox(elem.css)
  else:
    getBlockBox(elem.css)

  if box == nil:
    return nil

  box.node = elem
  box.element = elem

  var bctx = bctx
  case box.specified{"display"}
  of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
    let box = BlockBox(box)
    if bctx == nil:
      box.bctx = viewport.newBlockContext()
    else:
      box.bctx = bctx.newBlockContext(box)
    bctx = box.bctx
  of DISPLAY_INLINE_BLOCK:
    let box = InlineBlockBox(box)
    box.bctx = bctx.newInlineBlockContext(box)
    bctx = box.bctx
  else: discard

  var ibox: InlineBox
  template add_ibox() =
    if ibox != nil:
      box.children.add(ibox)
      ibox = nil

  template add_box(child: CSSBox) =
    add_ibox()
    box.children.add(child)
    if child.t notin {DISPLAY_INLINE, DISPLAY_INLINE_BLOCK} or not child.inlinelayout:
      box.inlinelayout = false

  box.inlinelayout = true

  if box.t == DISPLAY_LIST_ITEM:
    var ordinalvalue = 1
    if elem.tagType == TAG_LI:
      ordinalvalue = HTMLLIElement(elem).ordinalvalue
    let marker = box.getTextBox()
    marker.node = elem
    marker.text.add(elem.css{"list-style-type"}.listMarker(ordinalvalue))
    add_box(marker)

  let before = elem.pseudo[PSEUDO_BEFORE]
  if before != nil:
    let bbox = bctx.getPseudoBox(before)
    if bbox != nil:
      bbox.node = elem
      add_box(bbox)

  for child in elem.childNodes:
    case child.nodeType
    of ELEMENT_NODE:
      let elem = Element(child)
      if elem.tagType == TAG_BR:
        add_ibox()
        ibox = box.getTextBox()
        ibox.newline = true

      let cbox = elem.generateBox(viewport, bctx)
      if cbox != nil:
        add_box(cbox)
    of TEXT_NODE:
      let text = Text(child)
      # Don't generate empty anonymous inline blocks between block boxes
      if box.specified{"display"} == DISPLAY_INLINE or
          box.children.len > 0 and box.children[^1].specified{"display"} == DISPLAY_INLINE or
          box.specified{"white-space"} in {WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP} or
          not text.data.onlyWhitespace():
        if ibox == nil:
          ibox = box.getTextBox()
          ibox.node = elem
        ibox.text.add(text.data)
    else: discard
  add_ibox()

  let after = elem.pseudo[PSEUDO_AFTER]
  if after != nil:
    let abox = bctx.getPseudoBox(after)
    if abox != nil:
      abox.node = elem
      add_box(abox)

  viewport.map[elem.uid] = box

  return box

proc renderLayout*(viewport: var Viewport, document: Document) =
  if viewport.root == nil or document.all_elements.len != viewport.map.len:
    viewport.map = newSeq[CSSBox](document.all_elements.len)
  else:
    var i = 0
    while i < viewport.map.len:
      if not document.all_elements[i].rendered:
        viewport.map[i] = nil
      inc i
  viewport.root = BlockBox(document.root.generateBox(viewport))
  alignBlock(viewport.root)