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

                   
 
                    
                     
                 

                    
                        
                 
                  
                     
                     
 

















                                                                                
                                                      






                                 
                                               

                           
                                                                     

                              
                                                                      


                                                                        
                                                                


                                                                    


                                                    
                             
                                                                   
                        
                                   
                      
                                                            
                           
                                                           
                          
                                                              
                        
                                                        
                       
                
                               


                

                                                                     
           

                                    
              
                              
                        





                                                                         















                                                               
                                                                 




                                                           
 



                                                                              
                                 
                            


                                                         
                                          

                                             




                                                           

                                          


                                                           
                                          
                                                  
                             
                                                             
                                                  




                                                         

                                                         
                                    
                               
                     
                     
                       


                                                 
                                          
                      

                                          


                                                          
                



                                                                             


                                                        

                                              
                                     
                             
                                                        
                                        
                                            
                           
                                                            
                                               





                                                          
          
                                                                            



                                                                             
                                             
                                                        
                                   
              
 


































                                                                                



                         

                

                



                      


                                                                                  
                                 


                                






                                                                         


                                              
                                                                        
                                            
 


                                                                            
                                                             

                                                                           

                                                             



           
               



                                                           
 



                                                                          
 
                   
                      
 
                                                                      
                           

                                                           
                                         
 
                                     


                       
                      
 




                                                                          

                                   
 
                         
                                    
                                       
                        
                                                 
 
                                      
                                
                  
                                         
         
                                              

                             
                                                 
                                             
                                                                           

               
                                                                      


                                        
 
                                    

                                          

                                                
             

                                         
                           

                                      
           

                                      

                                                      

                                         
             


                                                  
 
                                                                   
                                
 

                                                                        
                                   




                                                                       
 
                                                                         
                                                                    
                                                     







                                                                            

                                   
                                                               
       
                                             
                                     
                 
                        
                                                                       
                 
                                                                   
                    
                                                                   
                  




                                                                           


                                                  

                                    

                       
                                                     

                                                        



                                                      

                                          
 
                                                                             
                                               

                                                                             
 
                                                                   
                                    

                                
                  
         
                          
                      



                                      
                                                    







                                                                                
                              
                                                  



                                        
                                      
                                                       
                                                    
                                                                               






                                                     


                                       
                                                                        
                                                               
                                                   

                                                                                
                                       
                         
                                

                                                                  
                                      

                                                   
                                 
                                                                  

                                
                                                         
                                                                
         
                                             
                                          
 
                                                                    

                                                         
                


                                                                
                                                                        
                                         
                                                   
                   
                    
                         
                       
import std/strutils
import std/unicode

import css/cssvalues
import css/stylednode
import img/bitmap
import layout/box
import layout/engine
import layout/layoutunit
import types/cell
import types/color
import types/winattrs
import utils/strwidth

type
  # A FormatCell *starts* a new terminal formatting context.
  # If no FormatCell exists before a given cell, the default formatting is used.
  FormatCell* = object
    format*: Format
    pos*: int
    node*: StyledNode

  # Following properties should hold for `formats':
  # * Position should be >= 0, <= str.width().
  # * The position of every FormatCell should be greater than the position
  #   of the previous FormatCell.
  FlexibleLine* = object
    str*: string
    formats*: seq[FormatCell]

  FlexibleGrid* = seq[FlexibleLine]

func findFormatN*(line: FlexibleLine; pos: int): int =
  var i = 0
  while i < line.formats.len:
    if line.formats[i].pos > pos:
      break
    inc i
  return i

proc addLines(grid: var FlexibleGrid; n: int) =
  grid.setLen(grid.len + n)

proc insertFormat(line: var FlexibleLine; i: int; cell: FormatCell) =
  line.formats.insert(cell, i)

proc insertFormat(line: var FlexibleLine; pos, i: int; format: Format;
    node: StyledNode = nil) =
  line.insertFormat(i, FormatCell(format: format, node: node, pos: pos))

proc addFormat(line: var FlexibleLine; pos: int; format: Format;
    node: StyledNode = nil) =
  line.formats.add(FormatCell(format: format, node: node, pos: pos))

func toFormat(computed: CSSComputedValues): Format =
  if computed == nil:
    return Format()
  var flags: set[FormatFlags]
  if computed{"font-style"} in {FontStyleItalic, FontStyleOblique}:
    flags.incl(ffItalic)
  if computed{"font-weight"} > 500:
    flags.incl(ffBold)
  if TextDecorationUnderline in computed{"text-decoration"}:
    flags.incl(ffUnderline)
  if TextDecorationOverline in computed{"text-decoration"}:
    flags.incl(ffOverline)
  if TextDecorationLineThrough in computed{"text-decoration"}:
    flags.incl(ffStrike)
  if TextDecorationBlink in computed{"text-decoration"}:
    flags.incl(ffBlink)
  return Format(
    fgcolor: computed{"color"},
    flags: flags
  )

proc findFirstX(line: var FlexibleLine; x: int; outi: var int): int =
  var cx = 0
  var i = 0
  while cx < x and i < line.str.len:
    var r: Rune
    let pi = i
    fastRuneAt(line.str, i, r)
    let w = r.twidth(cx)
    # we must ensure x is max(cx, x), otherwise our assumption of cx <= x
    # breaks down
    if cx + w > x:
      i = pi
      break
    cx += w
  outi = i
  return cx

proc setTextStr(line: var FlexibleLine; linestr, ostr: string;
    i, x, cx, nx, targetX: int) =
  var i = i
  let padlen = i + x - cx
  var widthError = max(nx - targetX, 0)
  let linestrTargetI = padlen + linestr.len
  line.str.setLen(linestrTargetI + widthError + ostr.len)
  while i < padlen: # place before new string
    line.str[i] = ' '
    inc i
  copyMem(addr line.str[i], unsafeAddr linestr[0], linestr.len)
  i = linestrTargetI
  while widthError > 0:
    # we ate half of a double width char; pad it out with spaces.
    line.str[i] = ' '
    dec widthError
    inc i
  if ostr.len > 0:
    copyMem(addr line.str[i], unsafeAddr ostr[0], ostr.len)

proc setTextFormat(line: var FlexibleLine; x, cx, nx: int; ostr: string;
    format: Format; node: StyledNode) =
  var fi = line.findFormatN(cx) - 1 # Skip unchanged formats before new string
  if x > cx:
    # Replace formats for padding
    var padformat = Format()
    if fi == -1:
      # No formats
      inc fi # insert after first format (meaning fi = 0)
      line.insertFormat(cx, fi, padformat)
    else:
      # First format's pos may be == cx here.
      if line.formats[fi].pos == cx:
        padformat.bgcolor = line.formats[fi].format.bgcolor
        let node = line.formats[fi].node
        line.formats.delete(fi)
        line.insertFormat(cx, fi, padformat, node)
      else:
        # First format < cx => split it up
        assert line.formats[fi].pos < cx
        padformat.bgcolor = line.formats[fi].format.bgcolor
        let node = line.formats[fi].node
        inc fi # insert after first format
        line.insertFormat(cx, fi, padformat, node)
    inc fi # skip last format
    while fi < line.formats.len and line.formats[fi].pos < x:
      # Other formats must be > cx => replace them
      padformat.bgcolor = line.formats[fi].format.bgcolor
      let node = line.formats[fi].node
      let px = line.formats[fi].pos
      line.formats.delete(fi)
      line.insertFormat(px, fi, padformat, node)
      inc fi
    dec fi # go back to previous format, so that pos <= x
    assert line.formats[fi].pos <= x
  # Now for the text's formats:
  var format = format
  var lformat: Format
  var lnode: StyledNode
  if fi == -1:
    # No formats => just insert a new format at 0
    inc fi
    line.insertFormat(x, fi, format, node)
    lformat = Format()
  else:
    # First format's pos may be == x here.
    lformat = line.formats[fi].format # save for later use
    lnode = line.formats[fi].node
    if line.formats[fi].pos == x:
      # Replace.
      # We must check if the old string's last x position is greater than
      # the new string's first x position. If not, we cannot inherit
      # its bgcolor (which is supposed to end before the new string started.)
      if nx > cx:
        format.bgcolor = line.formats[fi].format.bgcolor
      line.formats.delete(fi)
      line.insertFormat(x, fi, format, node)
    else:
      # First format's pos < x => split it up.
      assert line.formats[fi].pos < x
      if nx > cx: # see above
        format.bgcolor = line.formats[fi].format.bgcolor
      inc fi # insert after first format
      line.insertFormat(x, fi, format, node)
  inc fi # skip last format
  while fi < line.formats.len and line.formats[fi].pos < nx:
    # Other formats must be > x => replace them
    format.bgcolor = line.formats[fi].format.bgcolor
    let px = line.formats[fi].pos
    lformat = line.formats[fi].format # save for later use
    lnode = line.formats[fi].node
    line.formats.delete(fi)
    line.insertFormat(px, fi, format, node)
    inc fi
  if ostr.len > 0 and (fi >= line.formats.len or line.formats[fi].pos > nx):
    # nx < ostr.width, but we have removed all formatting in the range of our
    # string, and no formatting comes directly after it. So we insert the
    # continuation of the last format we replaced after our string.
    # (Default format when we haven't replaced anything.)
    line.insertFormat(nx, fi, lformat, lnode)
  dec fi # go back to previous format, so that pos <= nx
  assert line.formats[fi].pos <= nx
  # That's it!

proc setText(line: var FlexibleLine; linestr: string; x: int; format: Format;
    node: StyledNode) =
  assert x >= 0 and linestr.len != 0
  var targetX = x + linestr.twidth(x)
  var i = 0
  var cx = line.findFirstX(x, i) # first x of new string (before padding)
  var j = i
  var nx = x # last x of new string
  while nx < targetX and j < line.str.len:
    var r: Rune
    fastRuneAt(line.str, j, r)
    nx += r.twidth(nx)
  let ostr = line.str.substr(j)
  line.setTextStr(linestr, ostr, i, x, cx, nx, targetX)
  line.setTextFormat(x, cx, nx, ostr, format, node)

proc setText(grid: var FlexibleGrid; linestr: string; x, y: int; format: Format;
    node: StyledNode) =
  var x = x
  var i = 0
  var r: Rune
  while x < 0 and i < linestr.len:
    fastRuneAt(linestr, i, r)
    x += r.twidth(x)
  if x < 0:
    # highest x is outside the canvas, no need to draw
    return
  # make sure we have line y
  if grid.high < y:
    grid.addLines(y - grid.high)
  if i == 0:
    grid[y].setText(linestr, x, format, node)
  elif i < linestr.len:
    grid[y].setText(linestr.substr(i), x, format, node)

type
  PosBitmap* = ref object
    x*: int
    y*: int
    width*: int
    height*: int
    bmp*: Bitmap

  AbsolutePos = object
    offset: Offset
    size: Size

  RenderState = object
    # Position of the absolute positioning containing block:
    # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block
    absolutePos: seq[AbsolutePos]
    bgcolor: CellColor
    attrsp: ptr WindowAttributes
    images: seq[PosBitmap]

template attrs(state: RenderState): WindowAttributes =
  state.attrsp[]

proc setRowWord(grid: var FlexibleGrid; state: var RenderState;
    word: InlineAtom; offset: Offset; format: Format; node: StyledNode) =
  let y = toInt((offset.y + word.offset.y) div state.attrs.ppl) # y cell
  if y < 0:
    # y is outside the canvas, no need to draw
    return
  var x = toInt((offset.x + word.offset.x) div state.attrs.ppc) # x cell
  grid.setText(word.str, x, y, format, node)

proc setSpacing(grid: var FlexibleGrid; state: var RenderState;
    spacing: InlineAtom; offset: Offset; format: Format; node: StyledNode) =
  let y = toInt((offset.y + spacing.offset.y) div state.attrs.ppl) # y cell
  if y < 0: return # y is outside the canvas, no need to draw
  var x = toInt((offset.x + spacing.offset.x) div state.attrs.ppc) # x cell
  let width = toInt(spacing.size.w div state.attrs.ppc) # cell width
  if x + width < 0:
    return # highest x is outside the canvas, no need to draw
  var i = 0
  if x < 0:
    i -= x
    x = 0
  if i < width:
    # make sure we have line y
    if grid.high < y:
      grid.addLines(y - grid.high)
    grid[y].setText(' '.repeat(width - i), x, format, node)

proc paintBackground(grid: var FlexibleGrid; state: var RenderState;
    color: CellColor; startx, starty, endx, endy: int; node: StyledNode) =
  var starty = starty div state.attrs.ppl
  var endy = endy div state.attrs.ppl

  if starty > endy:
    swap(starty, endy)

  if endy <= 0: return # highest y is outside canvas, no need to paint
  if starty < 0: starty = 0
  if starty == endy: return # height is 0, no need to paint

  var startx = startx div state.attrs.ppc

  var endx = endx div state.attrs.ppc
  if endy < 0: endy = 0

  if startx > endx:
    swap(startx, endx)

  if endx <= 0: return # highest x is outside the canvas, no need to paint
  if startx < 0: startx = 0
  if startx == endx: return # width is 0, no need to paint

  # make sure we have line y
  if grid.high < endy:
    grid.addLines(endy - grid.high)

  for y in starty..<endy:
    # Make sure line.width() >= endx
    let linewidth = grid[y].str.width()
    if linewidth < endx:
      grid[y].str &= ' '.repeat(endx - linewidth)

    # Process formatting around startx
    if grid[y].formats.len == 0:
      # No formats
      grid[y].addFormat(startx, Format())
    else:
      let fi = grid[y].findFormatN(startx) - 1
      if fi == -1:
        # No format <= startx
        grid[y].insertFormat(startx, 0, Format())
      elif grid[y].formats[fi].pos == startx:
        # Last format equals startx => next comes after, nothing to be done
        discard
      else:
        # Last format lower than startx => separate format from startx
        let copy = grid[y].formats[fi]
        grid[y].formats[fi].pos = startx
        grid[y].insertFormat(fi, copy)

    # Process formatting around endx
    assert grid[y].formats.len > 0
    let fi = grid[y].findFormatN(endx) - 1
    if fi == -1:
      # Last format > endx -> nothing to be done
      discard
    elif grid[y].formats[fi].pos != endx:
      let copy = grid[y].formats[fi]
      if linewidth != endx:
        grid[y].formats[fi].pos = endx
        grid[y].insertFormat(fi, copy)
      else:
        grid[y].formats.delete(fi)
        grid[y].insertFormat(fi, copy)

    # Paint format backgrounds between startx and endx
    for fi in 0..grid[y].formats.high:
      if grid[y].formats[fi].pos >= endx:
        break
      if grid[y].formats[fi].pos >= startx:
        grid[y].formats[fi].format.bgcolor = color
        grid[y].formats[fi].node = node

proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
  box: BlockBox; offset: Offset)

proc paintInlineFragment(grid: var FlexibleGrid; state: var RenderState;
    fragment: InlineFragment; offset: Offset; bgcolor: CellColor) =
  for area in fragment.state.areas:
    let x1 = toInt(offset.x + area.offset.x)
    let y1 = toInt(offset.y + area.offset.y)
    let x2 = toInt(offset.x + area.offset.x + area.size.w)
    let y2 = toInt(offset.y + area.offset.y + area.size.h)
    grid.paintBackground(state, bgcolor, x1, y1, x2, y2, fragment.node)

proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
    fragment: InlineFragment; offset: Offset; bgcolor0: ARGBColor) =
  let bgcolor = fragment.computed{"background-color"}
  var bgcolor0 = bgcolor0
  case bgcolor.t
  of ctNone: discard
  of ctANSI: grid.paintInlineFragment(state, fragment, offset, bgcolor)
  of ctRGB:
    bgcolor0 = bgcolor0.blend(bgcolor.argbcolor)
    if bgcolor0.a > 0:
      grid.paintInlineFragment(state, fragment, offset, cellColor(bgcolor0))
  if fragment.t == iftParent:
    for child in fragment.children:
      grid.renderInlineFragment(state, child, offset, bgcolor0)
  else:
    let format = fragment.computed.toFormat()
    for atom in fragment.state.atoms:
      case atom.t
      of iatInlineBlock:
        grid.renderBlockBox(state, atom.innerbox, offset + atom.offset)
      of iatWord:
        grid.setRowWord(state, atom, offset, format, fragment.node)
      of iatSpacing:
        grid.setSpacing(state, atom, offset, format, fragment.node)
      of iatImage:
        let x1 = offset.x.toInt
        let y1 = offset.y.toInt
        let x2 = (offset.x + atom.size.w).toInt
        let y2 = (offset.y + atom.size.h).toInt
        grid.paintBackground(state, bgcolor, x1, y1, x2, y2, fragment.node)
        state.images.add(PosBitmap(
          x: (offset.x div state.attrs.ppc).toInt,
          y: (offset.y div state.attrs.ppl).toInt,
          width: atom.size.w.toInt,
          height: atom.size.h.toInt,
          bmp: atom.bmp
        ))
  if fragment.computed{"position"} != PositionStatic:
    if fragment.splitType != {stSplitStart, stSplitEnd}:
      if stSplitStart in fragment.splitType:
        state.absolutePos.add(AbsolutePos(
          offset: offset + fragment.state.startOffset,
          # looks like it's OK to set size to 0 here
        ))
      if stSplitEnd in fragment.splitType:
        discard state.absolutePos.pop()

proc renderRootInlineFragment(grid: var FlexibleGrid; state: var RenderState;
    root: RootInlineFragment; offset: Offset) =
  grid.renderInlineFragment(state, root.fragment, root.state.offset + offset,
    rgba(0, 0, 0, 0))

proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
    box: BlockBox; offset: Offset) =
  var stack = newSeqOfCap[tuple[
    box: BlockBox,
    offset: Offset
  ]](100)
  stack.add((box, offset))
  while stack.len > 0:
    var (box, offset) = stack.pop()
    if box == nil: # positioned marker
      discard state.absolutePos.pop()
      continue
    if box.computed{"position"} == PositionAbsolute:
      if not box.computed{"left"}.auto:
        offset.x = state.absolutePos[^1].offset.x
      elif not box.computed{"right"}.auto:
        offset.x = state.absolutePos[^1].offset.x + state.absolutePos[^1].size.w
      if not box.computed{"top"}.auto:
        offset.y = state.absolutePos[^1].offset.y
      elif not box.computed{"bottom"}.auto:
        offset.y = state.absolutePos[^1].offset.y + state.absolutePos[^1].size.h
    offset += box.state.offset
    if box.computed{"position"} != PositionStatic:
      state.absolutePos.add(AbsolutePos(
        offset: offset,
        size: box.state.size
      ))
      stack.add((nil, offset(-1, -1)))
    if box.computed{"visibility"} == VisibilityVisible:
      let bgcolor = box.computed{"background-color"}
      if bgcolor.t == ctANSI or bgcolor.t == ctRGB and bgcolor.argbcolor.a > 0:
        if box.computed{"-cha-bgcolor-is-canvas"} and
            state.bgcolor == defaultColor:
          #TODO bgimage
          state.bgcolor = bgcolor
        #TODO color blending
        let ix = toInt(offset.x)
        let iy = toInt(offset.y)
        let e = offset + box.state.size
        let iex = toInt(e.x)
        let iey = toInt(e.y)
        grid.paintBackground(state, bgcolor, ix, iy, iex, iey, box.node)
      if box.computed{"background-image"}.t == ContentImage and
          box.computed{"background-image"}.s != "":
        # ugly hack for background-image display... TODO actually display images
        let s = "[img]"
        let w = s.len * state.attrs.ppc
        var ix = offset.x
        if box.state.size.w < w:
          # text is larger than image; center it to minimize error
          ix -= w div 2
          ix += box.state.size.w div 2
        let x = toInt(ix div state.attrs.ppc)
        let y = toInt(offset.y div state.attrs.ppl)
        if y >= 0 and x + w >= 0:
          grid.setText(s, x, y, box.computed.toFormat(), box.node)
    if box.inline != nil:
      assert box.nested.len == 0
      if box.computed{"visibility"} == VisibilityVisible:
        grid.renderRootInlineFragment(state, box.inline, offset)
    else:
      for i in countdown(box.nested.high, 0):
        stack.add((box.nested[i], offset))

proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
    styledRoot: StyledNode; attrsp: ptr WindowAttributes;
    images: var seq[PosBitmap]) =
  grid.setLen(0)
  if styledRoot == nil:
    # no HTML element when we run cascade; just clear all lines.
    return
  var state = RenderState(absolutePos: @[AbsolutePos()], attrsp: attrsp)
  let rootBox = styledRoot.layout(attrsp)
  grid.renderBlockBox(state, rootBox, offset(0, 0))
  if grid.len == 0:
    grid.addLines(1)
  bgcolor = state.bgcolor
  images = state.images