about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/css/cascade.nim5
-rw-r--r--src/css/cssvalues.nim36
-rw-r--r--src/css/layout.nim22
-rw-r--r--src/css/render.nim244
4 files changed, 201 insertions, 106 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index d0cba1de..6a2172d7 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -274,6 +274,11 @@ func buildComputedValues(rules: CSSValueEntryMap;
     let display = result{"display"}.blockify()
     if display != result{"display"}:
       result{"display"} = display
+  if (result{"overflow-x"} in {OverflowVisible, OverflowClip}) !=
+      (result{"overflow-y"} in {OverflowVisible, OverflowClip}):
+    result{"overflow-x"} = result{"overflow-x"}.bfcify()
+    result{"overflow-y"} = result{"overflow-y"}.bfcify()
+
 
 proc add(map: var CSSValueEntryObj; rules: seq[CSSRuleDef]) =
   for rule in rules:
diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim
index d22fd414..96721e7c 100644
--- a/src/css/cssvalues.nim
+++ b/src/css/cssvalues.nim
@@ -25,6 +25,7 @@ type
     cstListStyle = "list-style"
     cstFlex = "flex"
     cstFlexFlow = "flex-flow"
+    cstOverflow = "overflow"
 
   CSSUnit* = enum
     cuAuto = ""
@@ -65,7 +66,8 @@ type
     cptClear = "clear"
     cptTextTransform = "text-transform"
     cptFlexDirection = "flex-direction"
-    cptOverflow = "overflow"
+    cptOverflowX = "overflow-x"
+    cptOverflowY = "overflow-y"
     cptFlexWrap = "flex-wrap"
     cptBgcolorIsCanvas = "-cha-bgcolor-is-canvas"
     cptFontStyle = "font-style"
@@ -309,6 +311,7 @@ type
     OverflowClip = "clip"
     OverflowScroll = "scroll"
     OverflowAuto = "auto"
+    OverflowOverlay = "overlay"
 
 type
   CSSLengthType* = enum
@@ -425,7 +428,8 @@ const ValueTypes = [
   cptClear: cvtClear,
   cptTextTransform: cvtTextTransform,
   cptFlexDirection: cvtFlexDirection,
-  cptOverflow: cvtOverflow,
+  cptOverflowX: cvtOverflow,
+  cptOverflowY: cvtOverflow,
   cptFlexWrap: cvtFlexWrap,
   cptBgcolorIsCanvas: cvtBgcolorIsCanvas,
   cptFontStyle: cvtFontStyle,
@@ -477,6 +481,9 @@ const PositionStaticLike* = {
   PositionStatic, PositionSticky
 }
 
+const OverflowScrollLike* = {OverflowScroll, OverflowAuto, OverflowOverlay}
+const OverflowHiddenLike* = {OverflowHidden, OverflowClip}
+
 func isBit*(t: CSSPropertyType): bool =
   return t <= cptFontStyle
 
@@ -574,6 +581,13 @@ func blockify*(display: CSSDisplay): CSSDisplay =
   of DisplayInlineFlex:
     return DisplayFlex
 
+func bfcify*(overflow: CSSOverflow): CSSOverflow =
+  if overflow == OverflowVisible:
+    return OverflowAuto
+  if overflow == OverflowClip:
+    return OverflowHidden
+  return overflow
+
 const UpperAlphaMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toPoints()
 const LowerAlphaMap = "abcdefghijklmnopqrstuvwxyz".toPoints()
 const LowerGreekMap = "αβγδεζηθικλμνξοπρστυφχψω".toPoints()
@@ -1449,6 +1463,24 @@ proc parseComputedValues*(res: var seq[CSSComputedEntry]; name: string;
     else:
       res.add(makeEntry(cptFlexDirection, global))
       res.add(makeEntry(cptFlexWrap, global))
+  of cstOverflow:
+    if global == cgtNone:
+      var i = 0
+      cvals.skipWhitespace(i)
+      if i >= cvals.len:
+        return err()
+      if (let xx = parseIdent[CSSOverflow](cvals[i]); xx.isSome):
+        var x = CSSValueBit(overflow: xx.get)
+        var y = x
+        inc i
+        cvals.skipWhitespace(i)
+        if i < cvals.len:
+          y.overflow = ?parseIdent[CSSOverflow](cvals[i])
+        res.add(makeEntry(cptOverflowX, x))
+        res.add(makeEntry(cptOverflowY, y))
+    else:
+      res.add(makeEntry(cptOverflowX, global))
+      res.add(makeEntry(cptOverflowY, global))
   return ok()
 
 proc parseComputedValues*(name: string; value: seq[CSSComponentValue];
diff --git a/src/css/layout.nim b/src/css/layout.nim
index 85d136c0..82f5b248 100644
--- a/src/css/layout.nim
+++ b/src/css/layout.nim
@@ -1191,7 +1191,7 @@ proc resolveBlockSizes(lctx: LayoutContext; space: AvailableSpace;
   if computed{"display"} == DisplayTableWrapper:
     sizes.space.w = fitContent(sizes.space.w)
   # height is max-content normally, but fit-content for clip.
-  sizes.space.h = if computed{"overflow"} != OverflowClip:
+  sizes.space.h = if computed{"overflow-y"} != OverflowClip:
     maxContent()
   else:
     fitContent(sizes.space.h)
@@ -1228,8 +1228,22 @@ proc applySize(box: BlockBox; sizes: ResolvedSizes;
   box.state.size[dim] = minClamp(box.state.size[dim], sizes.bounds.a[dim])
 
 proc clampIntr(box: BlockBox; sizes: ResolvedSizes) =
-  box.state.intr.w = min(box.state.intr.w, sizes.bounds.minClamp[dtHorizontal])
-  box.state.intr.h = min(box.state.intr.h, sizes.bounds.minClamp[dtVertical])
+  # We do not have a scroll bar, so do the next best thing: expand the
+  # box to the size its contents want.
+  #TODO this is far from perfect. For one, intrinsic minimum size isn't
+  # guaranteed to equal the desired scroll size. Also, it's possible
+  # that a parent box clamps the height of this box; in that case,
+  # the parent box's width/height should be clamped to the inner scroll
+  # width/height instead.
+  # Anyway, it is a pretty good approximation for now.
+  if box.computed{"overflow-x"} notin OverflowScrollLike:
+    box.state.intr.w = min(box.state.intr.w, sizes.bounds.minClamp[dtHorizontal])
+  else:
+    box.state.size.w = max(box.state.size.w, box.state.intr.w)
+  if box.computed{"overflow-y"} notin OverflowScrollLike:
+    box.state.intr.h = min(box.state.intr.h, sizes.bounds.minClamp[dtVertical])
+  else:
+    box.state.size.h = max(box.state.size.h, box.state.intr.h)
 
 proc applyWidth(box: BlockBox; sizes: ResolvedSizes;
     maxChildWidth: LayoutUnit; space: AvailableSpace) =
@@ -2625,7 +2639,7 @@ func establishesBFC(computed: CSSValues): bool =
   return computed{"float"} != FloatNone or
     computed{"display"} in {DisplayFlowRoot, DisplayTable, DisplayTableWrapper,
       DisplayFlex} or
-    computed{"overflow"} notin {OverflowVisible, OverflowClip}
+    computed{"overflow-x"} notin {OverflowVisible, OverflowClip}
     #TODO contain, grid, multicol, column-span
 
 # Layout and place all children in the block box.
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))