diff options
-rw-r--r-- | src/css/cascade.nim | 5 | ||||
-rw-r--r-- | src/css/cssvalues.nim | 36 | ||||
-rw-r--r-- | src/css/layout.nim | 22 | ||||
-rw-r--r-- | src/css/render.nim | 244 | ||||
-rw-r--r-- | todo | 6 |
5 files changed, 201 insertions, 112 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)) diff --git a/todo b/todo index e57648ee..0a6cfbc5 100644 --- a/todo +++ b/todo @@ -66,12 +66,6 @@ layout engine: - table layout: include caption in width calculation - flexbox: align-self, align-items, justify-content, proper margin handling - details element -- overflow - * which way to round on overflow: hidden? (floor is probably - better) - * instead of scrollbars, maybe just clamp overflow: scroll or - auto to their intrinsic minimum height? it might work well - enough on most sites - partial layout, layout caching - iframe - z order |