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


               
                  

                  
                     

                 
                

                    
                  

                   

                                                       

                                                 
                                                                     
                        
                               
                      
                                                          
                           
                                                         
                          
                                                             
                        
                                                      
                       
               
 
                                                                                                       
           

                            


                       
                                                     
                                        
              
                                  
                        





                                                                         

                                   
                        


                                        
 
                         
                                          

       

                                               
                          
                      

                  

                                  







                                                                         













                                                               
                                            
                                   
                                                      



                                                               
                                            
                                          
                                                      



                                                                     
                                          

                                       
                                                    





                                                         
                     
                       


                                                 
                                                      
                         

                                          
                                                              
                                     

                                     





                                                                               
                                 
                                                        


                                              


                                                              
                                        
                                                        



                                                                    

                                                          
                                     
                                                              
                                     
                               
                                                       

          




                                                                                    
                                                 
 


                                                        
 
                                                                                                 

             
                                                     
                                                             
 
                                                     


                                   
                    
                                                                     



                                           

                                                                                                       
                                                             
 

                                                        
 
                                                                             



           
                                     
 
                                              
 
                                                                                                                                              
                               
 

                                    
 
                   
                      
 
                                                                      
                           

                                                           
                                    
 
                                


                       
                      
 




                                                                          


                          
                         


                                    
                                                

                                                  


                                      
                                             
         
                                               



                                                     
                                                                           

               
                                                                      
                                       
                                         

                                         
                                    

                                           

                                                

             







                                           


                                                      
                                          
             
                                            
                                                   
                                        
 








                                                                         
                                                                                               
 
                                                                                                           

                          

                                        

                             
                              
                         
 
                            


                        
                           

                                         
                                                                                              

                                   
                                           

                                         
                                              
         
 
                                                                                                 
                                                    
                        

                      


                                 
 


                                                                                                                   
                                                                              
                     

                                
                       

                                                                
                            

                          
                               
                                                             
 



                                                                             
 


                                                        
         

                                             
 

                                   

                                             
                                                                                                                                                                            


                             

                                                                                
                                 
                     
                          
                                                

                        
import strutils
import unicode

import buffer/cell
import css/cascade
import css/sheet
import css/stylednode
import css/values
import html/dom
import io/window
import layout/box
import layout/engine
import types/color
import utils/twtstr

func formatFromWord(computed: ComputedFormat): Format =
  result.fgcolor = computed.color.cellColor()
  if computed.bgcolor.a != 0:
    result.bgcolor = computed.bgcolor.cellColor()
  if computed.fontstyle in { FONT_STYLE_ITALIC, FONT_STYLE_OBLIQUE }:
    result.italic = true
  if computed.fontweight > 500:
    result.bold = true
  if TEXT_DECORATION_UNDERLINE in computed.textdecoration:
    result.underline = true
  if TEXT_DECORATION_OVERLINE in computed.textdecoration:
    result.overline = true
  if TEXT_DECORATION_LINE_THROUGH in computed.textdecoration:
    result.strike = true
  if TEXT_DECORATION_BLINK in computed.textdecoration:
    result.blink = true
  else: discard

proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat, x, y: int) {.inline.} =
  var i = 0
  var r: Rune
  # make sure we have line y
  while lines.len <= y:
    lines.addLine()

  var cx = 0 # first x of new string (before padding)
  while cx < x and i < lines[y].str.len:
    let pi = i
    fastRuneAt(lines[y].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

  let ostr = lines[y].str.substr(i)
  lines[y].str.setLen(i)
  let padwidth = x - cx
  if padwidth > 0:
    lines[y].str &= ' '.repeat(padwidth)

  lines[y].str &= linestr
  let linestrwidth = linestr.twidth(x) - x

  i = 0
  var nx = x # last x of new string
  while nx < x + linestrwidth and i < ostr.len:
    fastRuneAt(ostr, i, r)
    nx += r.twidth(nx)

  if i < ostr.len:
    lines[y].str &= ostr.substr(i)

  # Negative x values make no sense from here on, as text with negative x
  # coordinates can not be formatted.
  let x = max(0, x)
  if cx < 0:
    cx = 0
  if nx < 0:
    nx = 0

  # Skip unchanged formats before the new string
  var fi = lines[y].findFormatN(cx) - 1

  if padwidth > 0:
    # Replace formats for padding
    var padformat = newFormat()
    if fi == -1:
      # No formats
      inc fi # insert after first format (meaning fi = 0)
      lines[y].insertFormat(cx, fi, padformat)
    else:
      # First format's pos may be == cx here.
      if lines[y].formats[fi].pos == cx:
        padformat.bgcolor = lines[y].formats[fi].format.bgcolor
        let node = lines[y].formats[fi].node
        lines[y].formats.delete(fi)
        lines[y].insertFormat(cx, fi, padformat, node)
      else:
        # First format < cx => split it up
        assert lines[y].formats[fi].pos < cx
        padformat.bgcolor = lines[y].formats[fi].format.bgcolor
        let node = lines[y].formats[fi].node
        inc fi # insert after first format
        lines[y].insertFormat(cx, fi, padformat, node)
    inc fi # skip last format
    while fi < lines[y].formats.len and lines[y].formats[fi].pos < x:
      # Other formats must be > cx => replace them
      padformat.bgcolor = lines[y].formats[fi].format.bgcolor
      let node = lines[y].formats[fi].node
      let px = lines[y].formats[fi].pos
      lines[y].formats.delete(fi)
      lines[y].insertFormat(px, fi, padformat, node)
      inc fi
    dec fi # go back to previous format, so that pos <= x
    assert lines[y].formats[fi].pos <= x

  # Now for the text's formats:
  var format = cformat.formatFromWord()
  var lformat: Format
  var lnode: StyledNode
  if fi == -1:
    # No formats => just insert a new format at 0
    inc fi
    lines[y].insertFormat(x, fi, format, cformat.node)
    lformat = newFormat()
  else:
    # First format's pos may be == x here.
    lformat = lines[y].formats[fi].format # save for later use
    lnode = lines[y].formats[fi].node
    if lines[y].formats[fi].pos == x:
      # Replace.
      if cformat.bgcolor.a == 0: #TODO alpha blending
        # 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 = lines[y].formats[fi].format.bgcolor
      lines[y].formats.delete(fi)
      lines[y].insertFormat(x, fi, format, cformat.node)
    else:
      # First format's pos < x => split it up.
      assert lines[y].formats[fi].pos < x
      if cformat.bgcolor.a == 0: #TODO alpha blending
        if nx > cx: # see above
          format.bgcolor = lines[y].formats[fi].format.bgcolor
      inc fi # insert after first format
      lines[y].insertFormat(x, fi, format, cformat.node)
  inc fi # skip last format

  while fi < lines[y].formats.len and lines[y].formats[fi].pos < nx:
    # Other formats must be > x => replace them
    if cformat.bgcolor.a == 0: #TODO alpha blending
      format.bgcolor = lines[y].formats[fi].format.bgcolor
    let px = lines[y].formats[fi].pos
    lformat = lines[y].formats[fi].format # save for later use
    lnode = lines[y].formats[fi].node
    lines[y].formats.delete(fi)
    lines[y].insertFormat(px, fi, format, cformat.node)
    inc fi

  if i < ostr.len and (fi >= lines[y].formats.len or lines[y].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.)
    lines[y].insertFormat(nx, fi, lformat, lnode)

  dec fi # go back to previous format, so that pos <= nx
  assert lines[y].formats[fi].pos <= nx
  # That's it!

proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: WindowAttributes) =
  var r: Rune

  var y = (y + word.offset.y) div window.ppl # y cell
  if y < 0: return # y is outside the canvas, no need to draw

  var x = (x + word.offset.x) div window.ppc # x cell
  var i = 0
  while x < 0 and i < word.str.len:
    fastRuneAt(word.str, i, r)
    x += r.twidth(x)
  if x < 0: return # highest x is outside the canvas, no need to draw
  let linestr = word.str.substr(i)

  lines.setText(linestr, word.format, x, y)

proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, window: WindowAttributes) =
  var y = (y + spacing.offset.y) div window.ppl # y cell
  if y < 0: return # y is outside the canvas, no need to draw

  var x = (x + spacing.offset.x) div window.ppc # x cell
  let width = spacing.width div window.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
  let linestr = ' '.repeat(width - i)

  lines.setText(linestr, spacing.format, x, y)

proc paintBackground(lines: var FlexibleGrid, color: RGBAColor, startx, starty, endx, endy: int, node: StyledNode, window: WindowAttributes) =
  let color = color.cellColor()

  var starty = starty div window.ppl
  var endy = endy div window.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 window.ppc

  var endx = endx div window.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
  while lines.len <= endy:
    lines.addLine()

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

    # Process formatting around startx
    if lines[y].formats.len == 0:
      # No formats
      lines[y].addFormat(startx, newFormat())
    else:
      let fi = lines[y].findFormatN(startx) - 1
      if fi == -1:
        # No format <= startx
        lines[y].insertFormat(startx, 0, newFormat())
      elif lines[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 = lines[y].formats[fi]
        lines[y].formats[fi].pos = startx
        lines[y].formats.insert(copy, fi)

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

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

func calculateErrorY(ctx: InlineContext, window: WindowAttributes): int =
  if ctx.lines.len <= 1: return 0
  var error = 0
  for i in 0 ..< ctx.lines.len:
    if i < ctx.lines.high:
      let dy = ctx.lines[i + 1].offset.y - ctx.lines[i].offset.y
      error += dy - (dy div window.ppl) * window.ppl
  return error div (ctx.lines.len - 1)

proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: WindowAttributes)

proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, window: WindowAttributes) =
  let x = x + ctx.offset.x
  let y = y + ctx.offset.y
  let erry = ctx.calculateErrorY(window)
  var i = 0
  for line in ctx.lines:
    let x = x + line.offset.x
    let y0 = y + line.offset.y
    let y = y0 - erry * i

    let r = y div window.ppl
    while grid.len <= r:
      grid.addLine()

    for atom in line.atoms:
      if atom of InlineBlockBox:
        let iblock = InlineBlockBox(atom)
        grid.renderBlockBox(iblock.innerbox, x + iblock.offset.x, y + iblock.offset.y, window)
      elif atom of InlineWord:
        let word = InlineWord(atom)
        grid.setRowWord(word, x, y, window)
      elif atom of InlineSpacing:
        let spacing = InlineSpacing(atom)
        grid.setSpacing(spacing, x, y, window)
    inc i

proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: WindowAttributes) =
  var stack = newSeqOfCap[(BlockBox, int, int)](100)
  stack.add((box, x, y))

  while stack.len > 0:
    var (box, x, y) = stack.pop()
    x += box.offset.x
    y += box.offset.y

    if box.computed{"background-color"}.a != 0: #TODO color blending
      grid.paintBackground(box.computed{"background-color"}, x, y, x + box.width, y + box.height, box.node, window)
    if box.computed{"background-image"}.t == CONTENT_IMAGE and box.computed{"background-image"}.s != "":
      # ugly hack for background-image display... TODO actually display images
      let s = "[img]"
      let w = s.len * window.ppc
      var x = x
      if box.width < w:
        # text is larger than image; center it to minimize error
        x -= w div 2
        x += box.width div 2
      x = x div window.ppc
      y = y div window.ppl
      if y >= 0 and x + w >= 0:
        grid.setText(s, ComputedFormat(node: box.node), x, y)

    if box of ListItemBox:
      let box = ListItemBox(box)
      if box.marker != nil:
        grid.renderInlineContext(box.marker, x - box.marker.width, y, window)

    if box.inline != nil:
      assert box.nested.len == 0
      grid.renderInlineContext(box.inline, x, y, window)
    else:
      for i in countdown(box.nested.high, 0):
        stack.add((box.nested[i], x, y))

const css = staticRead"res/ua.css"
let uastyle = css.parseStylesheet()
const quirk = css & staticRead"res/quirk.css"
let quirkstyle = quirk.parseStylesheet()
proc renderDocument*(document: Document, window: WindowAttributes, userstyle: CSSStylesheet, layout: var Viewport, previousStyled: StyledNode): (FlexibleGrid, StyledNode) =
  var uastyle = uastyle
  if document.mode == QUIRKS:
    uastyle = quirkstyle
  let styledNode = document.applyStylesheets(uastyle, userstyle, previousStyled)
  result[1] = styledNode
  layout.renderLayout(styledNode)
  result[0].setLen(0)
  for root in layout.root:
    result[0].renderBlockBox(root, 0, 0, window)
  if result[0].len == 0:
    result[0].addLine()