about summary refs log tree commit diff stats
path: root/src/css/render.nim
diff options
context:
space:
mode:
Diffstat (limited to 'src/css/render.nim')
-rw-r--r--src/css/render.nim244
1 files changed, 144 insertions, 100 deletions
diff --git a/src/css/render.nim b/src/css/render.nim
index 06fa02a7..9f869ad9 100644
--- a/src/css/render.nim
+++ b/src/css/render.nim
@@ -29,6 +29,42 @@ type
 
   FlexibleGrid* = seq[FlexibleLine]
 
+  PosBitmap* = ref object
+    x*: int
+    y*: int
+    offx*: int
+    offy*: int
+    width*: int
+    height*: int
+    bmp*: NetworkBitmap
+
+  ClipBox = object
+    start: Offset
+    send: Offset
+
+  StackItem = ref object
+    box: BlockBox
+    offset: Offset
+    apos: Offset
+    clipBox: ClipBox
+    index: int
+
+  RenderState = object
+    # Position of the absolute positioning containing block:
+    # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block
+    absolutePos: seq[Offset]
+    clipBoxes: seq[ClipBox]
+    bgcolor: CellColor
+    attrsp: ptr WindowAttributes
+    images: seq[PosBitmap]
+    nstack: seq[StackItem]
+
+template attrs(state: RenderState): WindowAttributes =
+  state.attrsp[]
+
+template clipBox(state: RenderState): ClipBox =
+  state.clipBoxes[^1]
+
 func findFormatN*(line: FlexibleLine; pos: int): int =
   var i = 0
   while i < line.formats.len:
@@ -91,18 +127,18 @@ proc findFirstX(line: var FlexibleLine; x: int; outi: var int): int =
   outi = i
   return cx
 
-proc setTextStr(line: var FlexibleLine; linestr, ostr: string;
+proc setTextStr(line: var FlexibleLine; s, ostr: openArray[char];
     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)
+  let targeti = padlen + s.len
+  line.str.setLen(targeti + 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
+  copyMem(addr line.str[i], unsafeAddr s[0], s.len)
+  i = targeti
   while widthError > 0:
     # we ate half of a double width char; pad it out with spaces.
     line.str[i] = ' '
@@ -195,90 +231,71 @@ proc setTextFormat(line: var FlexibleLine; x, cx, nx: int; ostr: string;
   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.width()
+proc setText0(line: var FlexibleLine; s: openArray[char]; x, targetX: int;
+    format: Format; node: StyledNode) =
+  assert x >= 0 and s.len != 0
   var i = 0
-  var cx = line.findFirstX(x, i) # first x of new string (before padding)
+  let 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:
     nx += line.str.nextUTF8(j).width()
   let ostr = line.str.substr(j)
-  line.setTextStr(linestr, ostr, i, x, cx, nx, targetX)
+  line.setTextStr(s, 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
-  while x < 0 and i < linestr.len:
-    x += linestr.nextUTF8(i).width()
-  if x < 0:
-    # highest x is outside the canvas, no need to draw
+proc setText(grid: var FlexibleGrid; state: var RenderState; s: string;
+    offset: Offset; format: Format; node: StyledNode) =
+  if offset.y notin state.clipBox.start.y ..< state.clipBox.send.y:
     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
-    offx*: int
-    offy*: int
-    width*: int
-    height*: int
-    bmp*: NetworkBitmap
-
-  StackItem = ref object
-    box: BlockBox
-    offset: Offset
-    apos: Offset
-    index: int
-
-  RenderState = object
-    # Position of the absolute positioning containing block:
-    # https://drafts.csswg.org/css-position/#absolute-positioning-containing-block
-    absolutePos: seq[Offset]
-    bgcolor: CellColor
-    attrsp: ptr WindowAttributes
-    images: seq[PosBitmap]
-    nstack: seq[StackItem]
-
-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
+  if offset.x > state.clipBox.send.x:
     return
-  var x = toInt((offset.x + word.offset.x) div state.attrs.ppc) # x cell
-  grid.setText(word.str, x, y, format, node)
+  var x = (offset.x div state.attrs.ppc).toInt
+  # Give room for rounding errors.
+  #TODO I'm sure there is a better way to do this, but this seems OK for now.
+  let sx = max((state.clipBox.start.x - state.attrs.ppc) div state.attrs.ppc, 0)
+  var i = 0
+  while x < sx and i < s.len:
+    x += s.nextUTF8(i).width()
+  if x < sx: # highest x is outside the clipping box, no need to draw
+    return
+  #TODO starting to think lunit should just clamp on overflow
+  var ex = int.high
+  if state.clipBox.send.x < LayoutUnit.high - state.attrs.ppc:
+    let tmp = (state.clipBox.send.x + state.attrs.ppc) div state.attrs.ppc
+    ex = tmp.toInt
+  var j = i
+  var targetX = x
+  while targetX < ex and j < s.len:
+    targetX += s.nextUTF8(j).width()
+  if i < j:
+    let y = (offset.y div state.attrs.ppl).toInt
+    # make sure we have line y
+    if grid.high < y:
+      grid.addLines(y - grid.high)
+    grid[y].setText0(s.toOpenArray(i, j - 1), x, targetX, format, node)
 
 proc paintBackground(grid: var FlexibleGrid; state: var RenderState;
     color: CellColor; startx, starty, endx, endy: int; node: StyledNode;
     noPaint = false) =
-  var starty = max(starty div state.attrs.ppl, 0)
-  var endy = max(endy div state.attrs.ppl, 0)
-  var startx = max(startx div state.attrs.ppc, 0)
-  var endx = max(endx div state.attrs.ppc, 0)
-  if starty == endy or startx == endx:
-    return # size is 0, no need to paint
+  let clipBox = addr state.clipBox
+  var startx = startx
+  var starty = starty
+  var endx = endx
+  var endy = endy
   if starty > endy:
     swap(starty, endy)
   if startx > endx:
     swap(startx, endx)
+  starty = max(starty, clipBox.start.y.toInt) div state.attrs.ppl
+  endy = min(endy, clipBox.send.y.toInt) div state.attrs.ppl
+  startx = max(startx, clipBox.start.x.toInt) div state.attrs.ppc
+  endx = min(endx, clipBox.send.x.toInt) div state.attrs.ppc
+  if starty >= endy or startx >= endx:
+    return
   if grid.high < endy: # make sure we have line y
     grid.addLines(endy - grid.high)
-  for y in starty..<endy:
+  for y in starty ..< endy:
     # Make sure line.width() >= endx
     for i in grid[y].str.width() ..< endx:
       grid[y].str &= ' '
@@ -361,29 +378,36 @@ proc renderInlineFragment(grid: var FlexibleGrid; state: var RenderState;
         grid.renderBlockBox(state, atom.innerbox, offset + atom.offset)
       of iatWord:
         if fragment.computed{"visibility"} == VisibilityVisible:
-          grid.setRowWord(state, atom, offset, format, fragment.node)
+          grid.setText(state, atom.str, offset + atom.offset, format,
+            fragment.node)
       of iatImage:
         if fragment.computed{"visibility"} == VisibilityVisible:
-          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
-          # add StyledNode to background (but don't actually color it)
-          grid.paintBackground(state, defaultColor, x1, y1, x2, y2,
-            fragment.node, noPaint = true)
-          let x = (offset.x div state.attrs.ppc).toInt
-          let y = (offset.y div state.attrs.ppl).toInt
-          let offx = (offset.x - x.toLayoutUnit * state.attrs.ppc).toInt
-          let offy = (offset.y - y.toLayoutUnit * state.attrs.ppl).toInt
-          state.images.add(PosBitmap(
-            x: x,
-            y: y,
-            offx: offx,
-            offy: offy,
-            width: atom.size.w.toInt,
-            height: atom.size.h.toInt,
-            bmp: atom.bmp
-          ))
+          let x2p = offset.x + atom.size.w
+          let y2p = offset.y + atom.size.h
+          let clipBox = addr state.clipBoxes[^1]
+          #TODO implement proper image clipping
+          if offset.x < clipBox.send.y and offset.y < clipBox.send.y and
+              x2p >= clipBox.start.x and y2p >= clipBox.start.y:
+            let x1 = offset.x.toInt
+            let y1 = offset.y.toInt
+            let x2 = x2p.toInt
+            let y2 = y2p.toInt
+            # add StyledNode to background (but don't actually color it)
+            grid.paintBackground(state, defaultColor, x1, y1, x2, y2,
+              fragment.node, noPaint = true)
+            let x = (offset.x div state.attrs.ppc).toInt
+            let y = (offset.y div state.attrs.ppl).toInt
+            let offx = (offset.x - x.toLayoutUnit * state.attrs.ppc).toInt
+            let offy = (offset.y - y.toLayoutUnit * state.attrs.ppl).toInt
+            state.images.add(PosBitmap(
+              x: x,
+              y: y,
+              offx: offx,
+              offy: offy,
+              width: atom.size.w.toInt,
+              height: atom.size.h.toInt,
+              bmp: atom.bmp
+            ))
   if position notin PositionStaticLike and stSplitEnd in fragment.splitType:
     discard state.absolutePos.pop()
 
@@ -397,6 +421,7 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
       box: box,
       offset: offset,
       apos: state.absolutePos[^1],
+      clipBox: state.clipBox,
       index: zindex
     ))
     return
@@ -410,6 +435,21 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
   box.render.offset = offset
   if position notin PositionStaticLike:
     state.absolutePos.add(offset)
+  let overflowX = box.computed{"overflow-x"}
+  let overflowY = box.computed{"overflow-y"}
+  let hasClipBox = overflowX != OverflowVisible or overflowY != OverflowVisible
+  if hasClipBox:
+    var clipBox = state.clipBox
+    if overflowX in OverflowHiddenLike:
+      clipBox.start.x = max(offset.x, clipBox.start.x)
+      clipBox.send.x = min(offset.x + box.state.size.w, clipBox.send.x)
+    else: # scroll like
+      clipBox.start.x = min(offset.x, clipBox.start.x)
+      clipBox.send.x = max(offset.x + box.state.size.w, clipBox.start.x)
+    if overflowY in OverflowHiddenLike:
+      clipBox.start.y = max(offset.y, clipBox.start.y)
+      clipBox.send.y = min(offset.y + box.state.size.h, clipBox.send.y)
+    state.clipBoxes.add(clipBox)
   if box.computed{"visibility"} == VisibilityVisible:
     #TODO maybe blend with the terminal background?
     let bgcolor = box.computed{"background-color"}.cellColor()
@@ -427,24 +467,25 @@ proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState;
       grid.paintBackground(state, bgcolor, ix, iy, iex, iey, box.node)
     if box.computed{"background-image"}.t == ContentImage:
       # ugly hack for background-image display... TODO actually display images
-      let s = "[img]"
+      const s = "[img]"
       let w = s.len * state.attrs.ppc
-      var ix = offset.x
+      var offset = offset
       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)
+        offset.x -= w div 2
+        offset.x += box.state.size.w div 2
+      grid.setText(state, s, offset, box.computed.toFormat(), box.node)
   if box.inline != nil:
     assert box.children.len == 0
-    if box.computed{"visibility"} == VisibilityVisible:
+    if box.computed{"visibility"} == VisibilityVisible and
+        state.clipBox.start.x < state.clipBox.send.x and
+        state.clipBox.start.y < state.clipBox.send.y:
       grid.renderInlineFragment(state, box.inline, offset, rgba(0, 0, 0, 0))
   else:
     for child in box.children:
       grid.renderBlockBox(state, child, offset)
+  if hasClipBox:
+    discard state.clipBoxes.pop()
   if position notin PositionStaticLike:
     discard state.absolutePos.pop()
 
@@ -457,14 +498,17 @@ proc renderDocument*(grid: var FlexibleGrid; bgcolor: var CellColor;
     return
   var state = RenderState(
     absolutePos: @[offset(0, 0)],
+    clipBoxes: @[ClipBox(send: offset(LayoutUnit.high, LayoutUnit.high))],
     attrsp: attrsp,
     bgcolor: defaultColor
   )
-  var stack = @[StackItem(box: rootBox)]
+  var stack = @[StackItem(box: rootBox, clipBox: state.clipBox)]
   while stack.len > 0:
     for it in stack:
       state.absolutePos.add(it.apos)
+      state.clipBoxes.add(it.clipBox)
       grid.renderBlockBox(state, it.box, it.offset, true)
+      discard state.clipBoxes.pop()
       discard state.absolutePos.pop()
     stack = move(state.nstack)
     stack.sort(proc(x, y: StackItem): int = cmp(x.index, y.index))