diff options
author | bptato <nincsnevem662@gmail.com> | 2025-02-18 18:51:57 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2025-02-18 19:26:40 +0100 |
commit | 4f785b483c92ef9755258e49de4a40a14ae4faf6 (patch) | |
tree | 96fc33c571455ef0822428188ab286167c9b8c3e /src/css | |
parent | b823867d6d1a557cd38c06cc77116d3cde090a20 (diff) | |
download | chawan-4f785b483c92ef9755258e49de4a40a14ae4faf6.tar.gz |
layout: implement negative z-index
Ugly, but works. I think.
Diffstat (limited to 'src/css')
-rw-r--r-- | src/css/box.nim | 34 | ||||
-rw-r--r-- | src/css/cssvalues.nim | 27 | ||||
-rw-r--r-- | src/css/layout.nim | 107 | ||||
-rw-r--r-- | src/css/render.nim | 187 |
4 files changed, 228 insertions, 127 deletions
diff --git a/src/css/box.nim b/src/css/box.nim index 2400c9da..1b696b58 100644 --- a/src/css/box.nim +++ b/src/css/box.nim @@ -48,8 +48,21 @@ type RelativeRect* = array[DimensionType, Span] + StackItem* = ref object + box*: CSSBox + index*: int32 + children*: seq[StackItem] + + ClipBox* = object + start*: Offset + send*: Offset + BoxRenderState* = object + # Whether the following two variables have been initialized. + #TODO find a better name that doesn't conflict with box.positioned + positioned*: bool offset*: Offset + clipBox*: ClipBox # min-content: box width is longest word's width # max-content: box width is content width without wrapping @@ -81,6 +94,7 @@ type parent* {.cursor.}: CSSBox firstChild*: CSSBox next*: CSSBox + positioned*: bool # set if we participate in positioned layout render*: BoxRenderState # render output computed*: CSSValues element*: Element @@ -105,6 +119,9 @@ type InlineBlockBox* = ref object of InlineBox # InlineBlockBox always has one block child. + LayoutResult* = ref object + stack*: StackItem + func offset*(x, y: LUnit): Offset = return [dtHorizontal: x, dtVertical: y] @@ -180,12 +197,29 @@ proc `+=`*(span: var Span; u: LUnit) = span.start += u span.send += u +func `<`*(a, b: Offset): bool = + return a.x < b.x and a.y < b.y + iterator children*(box: CSSBox): CSSBox = var it {.cursor.} = box.firstChild while it != nil: yield it it = it.next +proc resetState(box: CSSBox) = + box.positioned = false + box.render = BoxRenderState() + +proc resetState*(ibox: InlineBox) = + CSSBox(ibox).resetState() + ibox.state = InlineBoxState() + +proc resetState*(box: BlockBox) = + CSSBox(box).resetState() + box.state = BoxLayoutState() + +const DefaultClipBox* = ClipBox(send: offset(LUnit.high, LUnit.high)) + when defined(debug): proc computedTree*(box: CSSBox): string = result = "<" diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim index f536cefd..51d3fd1e 100644 --- a/src/css/cssvalues.nim +++ b/src/css/cssvalues.nim @@ -166,6 +166,7 @@ type cvtFlexWrap = "flexWrap" cvtNumber = "number" cvtOverflow = "overflow" + cvtZIndex = "zIndex" CSSGlobalType* = enum cgtInitial = "initial" @@ -382,6 +383,10 @@ type a*: CSSLength b*: CSSLength + CSSZIndex* = object + `auto`*: bool + num*: int32 + CSSValueBit* {.union.} = object dummy*: uint8 bgcolorIsCanvas*: bool @@ -412,6 +417,7 @@ type length*: CSSLength number*: float32 verticalAlign*: CSSVerticalAlign + zIndex*: CSSZIndex CSSValue* = ref object case v*: CSSValueType @@ -527,7 +533,7 @@ const ValueTypes = [ cptTop: cvtLength, cptVerticalAlign: cvtVerticalAlign, cptWidth: cvtLength, - cptZIndex: cvtInteger, + cptZIndex: cvtZIndex, # pointers cptBackgroundImage: cvtImage, @@ -634,6 +640,11 @@ func `$`(counterreset: seq[CSSCounterSet]): string = result &= ' ' result &= $it.num +func `$`(zIndex: CSSZIndex): string = + if zIndex.auto: + return $auto + return $zIndex.num + func serialize(val: CSSValue): string = case val.v of cvtImage: return $val.image @@ -658,6 +669,7 @@ func serialize(val: CSSValueWord; t: CSSValueType): string = of cvtLength: return $val.length of cvtNumber: return $val.number of cvtVerticalAlign: return $val.verticalAlign + of cvtZIndex: return $val.zIndex else: assert false func serialize(val: CSSValueBit; t: CSSValueType): string = @@ -1494,6 +1506,14 @@ func parseInteger(cval: CSSComponentValue; range: Slice[int32]): Opt[int32] = return ok(int32(tok.nvalue)) return err() +func parseZIndex(cval: CSSComponentValue): Opt[CSSZIndex] = + if cval of CSSToken: + let tok = CSSToken(cval) + if tok.t == cttIdent and tok.value == "auto": + return ok(CSSZIndex(auto: true)) + return ok(CSSZIndex(num: ?parseInteger(cval, -65534i32 .. 65534i32))) + return err() + func parseNumber(cval: CSSComponentValue; range: Slice[float32]): Opt[float32] = if cval of CSSToken: let tok = CSSToken(cval) @@ -1590,8 +1610,8 @@ proc parseValue(cvals: openArray[CSSComponentValue]; t: CSSPropertyType; of cptFontWeight: set_word integer, ?parseFontWeight(cval) of cptChaColspan: set_word integer, ?parseInteger(cval, 1i32 .. 1000i32) of cptChaRowspan: set_word integer, ?parseInteger(cval, 0i32 .. 65534i32) - of cptZIndex: set_word integer, ?parseInteger(cval, -65534i32 .. 65534i32) else: assert false + of cvtZIndex: set_word zIndex, ?parseZIndex(cval) of cvtTextDecoration: set_bit textDecoration, ?cssTextDecoration(cvals) of cvtVerticalAlign: set_word verticalAlign, ?cssVerticalAlign(cval, attrs) of cvtTextAlign: set_bit textAlign, ?parseIdent[CSSTextAlign](cval) @@ -1675,6 +1695,7 @@ proc getDefaultWord(t: CSSPropertyType): CSSValueWord = of cvtInteger: return CSSValueWord(integer: getInitialInteger(t)) of cvtLength: return CSSValueWord(length: getInitialLength(t)) of cvtNumber: return CSSValueWord(number: getInitialNumber(t)) + of cvtZIndex: return CSSValueWord(zIndex: CSSZIndex(auto: true)) else: return CSSValueWord(dummy: 0) func lengthShorthand(cvals: openArray[CSSComponentValue]; @@ -1931,7 +1952,7 @@ func splitTable*(computed: CSSValues): tuple[outer, innner: CSSValues] = cptPaddingLeft, cptPaddingRight, cptPaddingTop, cptPaddingBottom, cptWidth, cptHeight, cptBoxSizing, # no clue why this isn't included in the standard - cptClear + cptClear, cptPosition } for t in CSSPropertyType: if t in props: diff --git a/src/css/layout.nim b/src/css/layout.nim index 6ea110a6..ddf4847c 100644 --- a/src/css/layout.nim +++ b/src/css/layout.nim @@ -27,7 +27,9 @@ type offset: Offset child: BlockBox - PositionedItem = seq[QueuedAbsolute] + PositionedItem = object + stack: StackItem # stacking context to append children to + queue: seq[QueuedAbsolute] LayoutContext = ref object attrsp: ptr WindowAttributes @@ -1389,8 +1391,16 @@ proc applyIntr(box: BlockBox; sizes: ResolvedSizes; intr: Size) = box.state.intr[dim] = max(intr[dim], sizes.bounds.mi[dim].start) box.state.size[dim] = max(box.state.size[dim], intr[dim]) -proc pushPositioned(lctx: LayoutContext) = - lctx.positioned.add(@[]) +proc pushPositioned(lctx: LayoutContext; box: CSSBox) = + let index = box.computed{"z-index"} + let stack = StackItem(box: box, index: index.num) + lctx.positioned[^1].stack.children.add(stack) + let nextStack = if index.auto: + lctx.positioned[^1].stack + else: + stack + box.positioned = true + lctx.positioned.add(PositionedItem(stack: nextStack)) # Offset the child by the offset of its actual parent to its nearest # positioned ancestor (`parent'). @@ -1409,7 +1419,7 @@ proc realignAbsolutePosition(child: BlockBox; parent: CSSBox; if dtVertical in dims: child.state.offset.y -= offset.y it = it.parent - if parent of InlineBox: + if parent != nil and parent of InlineBox: # The renderer does not adjust position for inline parents, so we # must do it here. let offset = InlineBox(parent).state.startOffset @@ -1418,10 +1428,11 @@ proc realignAbsolutePosition(child: BlockBox; parent: CSSBox; if dtVertical in dims: child.state.offset.y += offset.y -# size is the parent's size -proc popPositioned(lctx: LayoutContext; parent: CSSBox; size: Size; - skipStatic = true) = - for it in lctx.positioned.pop(): +# size is the parent's size. +# Note that parent may be nil. +proc popPositioned(lctx: LayoutContext; parent: CSSBox; size: Size) = + let item = lctx.positioned[^1] + for it in item.queue: let child = it.child var size = size #TODO this is very ugly. @@ -1461,13 +1472,18 @@ proc popPositioned(lctx: LayoutContext; parent: CSSBox; size: Size; child.state.offset.y += sizes.margin.top if dims != {}: child.realignAbsolutePosition(parent, dims) + discard lctx.positioned.pop() + let stack = item.stack + if stack.box == parent: + #TODO this sorts twice because of fixed... + stack.children.sort(proc(x, y: StackItem): int = cmp(x.index, y.index)) proc queueAbsolute(lctx: LayoutContext; box: BlockBox; offset: Offset) = case box.computed{"position"} of PositionAbsolute: - lctx.positioned[^1].add(QueuedAbsolute(child: box, offset: offset)) + lctx.positioned[^1].queue.add(QueuedAbsolute(child: box, offset: offset)) of PositionFixed: - lctx.positioned[0].add(QueuedAbsolute(child: box, offset: offset)) + lctx.positioned[0].queue.add(QueuedAbsolute(child: box, offset: offset)) else: assert false proc positionRelative(lctx: LayoutContext; space: AvailableSpace; @@ -1545,20 +1561,21 @@ proc positionFloat(bctx: var BlockContext; child: BlockBox; # its parent. # Returns the block's outer size. # Stores its resolved size data in `sizes'. -proc layoutBlockChild(fstate: var FlowState; child: BlockBox; +proc layoutBlockChild(fstate: var FlowState; box: BlockBox; sizes: out ResolvedSizes): Size = let lctx = fstate.lctx - sizes = lctx.resolveBlockSizes(fstate.space, child.computed) + sizes = lctx.resolveBlockSizes(fstate.space, box.computed) fstate.bctx.marginTodo.append(sizes.margin.top) - child.state = BoxLayoutState(offset: offset(x = sizes.margin.left, y = 0)) - child.state.offset += fstate.offset - fstate.bctx.layout(child, sizes, canClear = true) + box.resetState() + box.state.offset = fstate.offset + box.state.offset.x += sizes.margin.left + fstate.bctx.layout(box, sizes, canClear = true) fstate.bctx.marginTodo.append(sizes.margin.bottom) return size( - w = child.outerSize(dtHorizontal, sizes), + w = box.outerSize(dtHorizontal, sizes), # delta y is difference between old and new offsets (margin-top), # plus height. - h = child.state.offset.y - fstate.offset.y + child.state.size.h + h = box.state.offset.y - fstate.offset.y + box.state.size.h ) # Outer layout for block-level children that establish a BFC. @@ -1840,7 +1857,7 @@ proc layoutImage(fstate: var FlowState; ibox: InlineImageBox; padding: LUnit) = proc layoutInline(fstate: var FlowState; ibox: InlineBox) = let lctx = fstate.lctx let computed = ibox.computed - ibox.state = InlineBoxState() + ibox.resetState() let padding = Span( start: computed{"padding-left"}.px(fstate.space.w), send: computed{"padding-right"}.px(fstate.space.w) @@ -1885,7 +1902,7 @@ proc layoutInline(fstate: var FlowState; ibox: InlineBox) = fstate.initLine() fstate.lbstate.size.w += padding.start if computed{"position"} != PositionStatic: - lctx.pushPositioned() + lctx.pushPositioned(ibox) for child in ibox.children: if child of InlineBox: fstate.layoutInline(InlineBox(child)) @@ -1915,8 +1932,8 @@ proc layoutInline(fstate: var FlowState; ibox: InlineBox) = # and Gecko can't even layout it consistently (???) # # So I'm trying to follow Blink, though it's still not quite right, - # since it uses cellHeight instead of the actual line height for the - # last line. + # since this uses cellHeight instead of the actual line height + # for the last line. # Well, it seems good enough. lctx.popPositioned(ibox, size( w = 0, @@ -2030,7 +2047,7 @@ proc initReLayout(fstate: var FlowState; bctx: var BlockContext; box: BlockBox; proc layoutFlow(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes; canClear: bool) = if box.computed{"position"} != PositionStatic: - bctx.lctx.pushPositioned() + bctx.lctx.pushPositioned(box) if canClear and box.computed{"clear"} != ClearNone and box.computed{"position"} notin PositionAbsoluteFixed: bctx.flushMargins(box.state.offset.y) @@ -2093,7 +2110,8 @@ proc layoutListItem(bctx: var BlockContext; box: BlockBox; of ListStylePositionOutside: let marker = BlockBox(box.firstChild) let content = BlockBox(marker.next) - content.state = BoxLayoutState(offset: box.state.offset) + content.resetState() + content.state.offset = box.state.offset bctx.layoutFlow(content, sizes, canClear = true) let markerSizes = ResolvedSizes( space: availableSpace(w = fitContent(sizes.space.w), h = sizes.space.h), @@ -2104,6 +2122,7 @@ proc layoutListItem(bctx: var BlockContext; box: BlockBox; # instead marker.state.offset.x = -marker.state.size.w # take inner box min width etc. + box.resetState() box.state = content.state content.state.offset = offset(x = 0, y = 0) of ListStylePositionInside: @@ -2165,7 +2184,7 @@ proc layoutTableCell(lctx: LayoutContext; box: BlockBox; ) if sizes.space.w.isDefinite(): sizes.space.w.u -= sizes.padding[dtHorizontal].sum() - box.state = BoxLayoutState() + box.resetState() var bctx = BlockContext(lctx: lctx) bctx.layoutFlow(box, sizes, canClear = false) assert bctx.unpositionedFloats.len == 0 @@ -2317,7 +2336,7 @@ proc alignTableCell(cell: BlockBox; availableHeight, baseline: LUnit) = proc layoutTableRow(tctx: TableContext; ctx: RowContext; parent, row: BlockBox) = - row.state = BoxLayoutState() + row.resetState() var x: LUnit = 0 var n = 0 var baseline: LUnit = 0 @@ -2586,7 +2605,7 @@ proc layoutInnerTable(tctx: var TableContext; table, parent: BlockBox; # block-level wrapper box. proc layoutTable(bctx: BlockContext; box: BlockBox; sizes: ResolvedSizes) = let table = BlockBox(box.firstChild) - table.state = BoxLayoutState() + table.resetState() var tctx = TableContext(lctx: bctx.lctx, space: sizes.space) tctx.layoutInnerTable(table, box, sizes) box.state.size = table.state.size @@ -2633,6 +2652,7 @@ type firstBaseline: LUnit baseline: LUnit canWrap: bool + reverse: bool dim: DimensionType # main dimension firstBaselineSet: bool @@ -2765,6 +2785,11 @@ proc flushMain(fctx: var FlexContext; mctx: var FlexMainContext; fctx.firstBaselineSet = true fctx.firstBaseline = baseline fctx.baseline = baseline + if fctx.reverse: + for it in mctx.pending: + let child = it.child + child.state.offset[dim] = offset[dim] - child.state.offset[dim] - + child.state.size[dim] fctx.totalMaxSize[dim] = max(fctx.totalMaxSize[dim], offset[dim]) fctx.mains.add(mctx) fctx.intr[dim] = max(fctx.intr[dim], intr[dim]) @@ -2818,7 +2843,7 @@ proc layoutFlexIter(fctx: var FlexContext; mctx: var FlexMainContext; proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) = let lctx = bctx.lctx if box.computed{"position"} != PositionStatic: - lctx.pushPositioned() + lctx.pushPositioned(box) let flexDir = box.computed{"flex-direction"} let dim = if flexDir in FlexRow: dtHorizontal else: dtVertical let odim = dim.opposite() @@ -2827,6 +2852,7 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) = offset: sizes.padding.topLeft, redistSpace: sizes.space[dim], canWrap: box.computed{"flex-wrap"} != FlexWrapNowrap, + reverse: box.computed{"flex-direction"} in FlexReverse, dim: dim ) if fctx.redistSpace.t == scFitContent and sizes.bounds.a[dim].start > 0: @@ -2834,17 +2860,9 @@ proc layoutFlex(bctx: var BlockContext; box: BlockBox; sizes: ResolvedSizes) = if fctx.redistSpace.isDefinite: fctx.redistSpace.u = fctx.redistSpace.u.minClamp(sizes.bounds.a[dim]) var mctx = FlexMainContext() - if box.computed{"flex-direction"} notin FlexReverse: - for child in box.children: - let child = BlockBox(child) - fctx.layoutFlexIter(mctx, child, sizes) - else: - var children: seq[CSSBox] = @[] - for it in box.children: - children.add(it) - for i in countdown(children.high, 0): - let child = BlockBox(children[i]) - fctx.layoutFlexIter(mctx, child, sizes) + for child in box.children: + let child = BlockBox(child) + fctx.layoutFlexIter(mctx, child, sizes) if mctx.pending.len > 0: fctx.flushMain(mctx, sizes) var size = fctx.totalMaxSize @@ -2868,7 +2886,8 @@ proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset; return box.sizes = sizes var bctx = BlockContext(lctx: lctx) - box.state = BoxLayoutState(offset: offset) + box.resetState() + box.state.offset = offset bctx.layout(box, sizes, canClear = false) assert bctx.unpositionedFloats.len == 0 let marginBottom = bctx.marginTodo.sum() @@ -2882,15 +2901,16 @@ proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset; box.state.intr.h = max(box.state.intr.h, bctx.maxFloatHeight) box.state.marginBottom = marginBottom -proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) = +proc layout*(box: BlockBox; attrsp: ptr WindowAttributes): StackItem = let space = availableSpace( w = stretch(attrsp[].widthPx), h = stretch(attrsp[].heightPx) ) + let stack = StackItem(box: box) let lctx = LayoutContext( attrsp: attrsp, cellSize: size(w = attrsp.ppc, h = attrsp.ppl), - positioned: @[@[], @[]], + positioned: @[PositionedItem(stack: stack), PositionedItem(stack: stack)], luctx: LUContext() ) let sizes = lctx.resolveBlockSizes(space, box.computed) @@ -2898,7 +2918,7 @@ proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) = lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes) var size = size(w = attrsp[].widthPx, h = attrsp[].heightPx) # Last absolute layer. - lctx.popPositioned(box, size) + lctx.popPositioned(nil, size) # Fixed containing block. # The idea is to move fixed boxes to the real edges of the page, # so that they do not overlap with other boxes *and* we don't have @@ -2907,4 +2927,5 @@ proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) = # slow down the renderer to a crawl.) size.w = max(size.w, box.state.size.w) size.h = max(size.h, box.state.size.h) - lctx.popPositioned(box, size, skipStatic = false) + lctx.popPositioned(box, size) + return stack diff --git a/src/css/render.nim b/src/css/render.nim index 875cf5c0..565b7c9e 100644 --- a/src/css/render.nim +++ b/src/css/render.nim @@ -1,5 +1,3 @@ -import std/algorithm - import css/box import css/cssvalues import css/lunit @@ -38,21 +36,10 @@ type height*: int bmp*: NetworkBitmap - ClipBox = object - start: Offset - send: Offset - - StackItem = object - box: CSSBox - clipBox: ClipBox - index: int - RenderState = object - clipBoxes: seq[ClipBox] bgcolor: CellColor attrsp: ptr WindowAttributes images: seq[PosBitmap] - nstack: seq[StackItem] spaces: string # buffer filled with spaces for padding # Forward declarations @@ -62,9 +49,6 @@ proc renderBlock(grid: var FlexibleGrid; state: var RenderState; 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: @@ -242,21 +226,21 @@ proc setText1(line: var FlexibleLine; s: openArray[char]; x, targetX: int; line.setTextFormat(x, cx, targetX, hadStr, format, node) proc setText(grid: var FlexibleGrid; state: var RenderState; s: string; - offset: Offset; format: Format; node: Element) = - if offset.y notin state.clipBox.start.y ..< state.clipBox.send.y: + offset: Offset; format: Format; node: Element; clipBox: ClipBox) = + if offset.y notin clipBox.start.y ..< clipBox.send.y: return - if offset.x > state.clipBox.send.x: + if offset.x > clipBox.send.x: return 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) + let sx = max((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 - let ex = ((state.clipBox.send.x + state.attrs.ppc) div state.attrs.ppc).toInt + let ex = ((clipBox.send.x + state.attrs.ppc) div state.attrs.ppc).toInt var j = i var targetX = x while targetX < ex and j < s.len: @@ -270,8 +254,7 @@ proc setText(grid: var FlexibleGrid; state: var RenderState; s: string; proc paintBackground(grid: var FlexibleGrid; state: var RenderState; color: CellColor; startx, starty, endx, endy: int; node: Element; - alpha: uint8) = - let clipBox = addr state.clipBox + alpha: uint8; clipBox: ClipBox) = var startx = startx var starty = starty var endx = endx @@ -353,21 +336,20 @@ proc paintInlineBox(grid: var FlexibleGrid; state: var RenderState; 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, box.element, - alpha) + alpha, box.render.clipBox) proc renderInline(grid: var FlexibleGrid; state: var RenderState; ibox: InlineBox; offset: Offset; bgcolor0 = rgba(0, 0, 0, 0); pass2 = false) = - let position = ibox.computed{"position"} - #TODO handle negative z-index - let zindex = ibox.computed{"z-index"} - if position != PositionStatic and not pass2 and zindex >= 0: - state.nstack.add(StackItem( - box: ibox, - clipBox: state.clipBox, - index: zindex - )) - return + let clipBox = if ibox.parent != nil: + ibox.parent.render.clipBox + else: + DefaultClipBox + ibox.render = BoxRenderState( + offset: offset + ibox.state.startOffset, + clipBox: clipBox, + positioned: true + ) let bgcolor = ibox.computed{"background-color"} var bgcolor0 = bgcolor0 if bgcolor.isCell: @@ -377,16 +359,15 @@ proc renderInline(grid: var FlexibleGrid; state: var RenderState; else: bgcolor0 = bgcolor0.blend(bgcolor.argb) if bgcolor0.a > 0: - grid.paintInlineBox(state, ibox, offset, - bgcolor0.rgb.cellColor(), bgcolor0.a) - ibox.render.offset = offset + ibox.state.startOffset + grid.paintInlineBox(state, ibox, offset, bgcolor0.rgb.cellColor(), + bgcolor0.a) if ibox of InlineTextBox: let ibox = InlineTextBox(ibox) let format = ibox.computed.toFormat() for run in ibox.runs: let offset = offset + run.offset if ibox.computed{"visibility"} == VisibilityVisible: - grid.setText(state, run.str, offset, format, ibox.element) + grid.setText(state, run.str, offset, format, ibox.element, clipBox) elif ibox of InlineImageBox: let ibox = InlineImageBox(ibox) if ibox.computed{"visibility"} != VisibilityVisible: @@ -394,7 +375,6 @@ proc renderInline(grid: var FlexibleGrid; state: var RenderState; let offset = offset + ibox.imgstate.offset let x2p = offset.x + ibox.imgstate.size.w let y2p = offset.y + ibox.imgstate.size.h - let clipBox = addr state.clipBoxes[^1] #TODO implement proper image clipping if offset.x < clipBox.send.x and offset.y < clipBox.send.y and x2p >= clipBox.start.x and y2p >= clipBox.start.y: @@ -404,7 +384,7 @@ proc renderInline(grid: var FlexibleGrid; state: var RenderState; let y2 = y2p.toInt # add Element to background (but don't actually color it) grid.paintBackground(state, defaultColor, x1, y1, x2, y2, - ibox.element, 0) + ibox.element, 0, ibox.render.clipBox) let x = (offset.x div state.attrs.ppc).toInt let y = (offset.y div state.attrs.ppl).toInt let offx = (offset.x - x.toLUnit * state.attrs.ppc).toInt @@ -419,31 +399,26 @@ proc renderInline(grid: var FlexibleGrid; state: var RenderState; bmp: ibox.bmp )) else: # InlineNewLineBox does not have children, so we handle it here + # only check position here to avoid skipping leaves that use our + # computed values + if ibox.positioned and not pass2: + return for child in ibox.children: if child of InlineBox: grid.renderInline(state, InlineBox(child), offset, bgcolor0) else: grid.renderBlock(state, BlockBox(child), offset) -proc renderBlock(grid: var FlexibleGrid; state: var RenderState; - box: BlockBox; offset: Offset; pass2 = false) = - let position = box.computed{"position"} - #TODO handle negative z-index - let zindex = box.computed{"z-index"} - if position != PositionStatic and not pass2 and zindex >= 0: - state.nstack.add(StackItem( - box: box, - clipBox: state.clipBox, - index: zindex - )) +proc inheritClipBox(box: BlockBox; parent: CSSBox) = + if parent == nil: + box.render.clipBox = DefaultClipBox return - let offset = offset + box.state.offset - box.render.offset = offset + assert parent.render.positioned + var clipBox = parent.render.clipBox 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 != OverflowVisible or overflowY != OverflowVisible: + let offset = box.render.offset 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) @@ -453,7 +428,17 @@ proc renderBlock(grid: var FlexibleGrid; state: var RenderState; 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) + box.render.clipBox = clipBox + +proc renderBlock(grid: var FlexibleGrid; state: var RenderState; + box: BlockBox; offset: Offset; pass2 = false) = + if box.positioned and not pass2: + return + let offset = offset + box.state.offset + box.render.offset = offset + box.render.positioned = true + if not pass2: + box.inheritClipBox(box.parent) let opacity = box.computed{"opacity"} if box.computed{"visibility"} == VisibilityVisible and opacity != 0: #TODO maybe blend with the terminal background? @@ -471,7 +456,7 @@ proc renderBlock(grid: var FlexibleGrid; state: var RenderState; let iex = toInt(e.x) let iey = toInt(e.y) grid.paintBackground(state, bgcolor, ix, iy, iex, iey, box.element, - bgcolor0.a) + bgcolor0.a, box.render.clipBox) if box.computed{"background-image"} != nil: # ugly hack for background-image display... TODO actually display images const s = "[img]" @@ -481,55 +466,95 @@ proc renderBlock(grid: var FlexibleGrid; state: var RenderState; # text is larger than image; center it to minimize error offset.x -= w div 2 offset.x += box.state.size.w div 2 - grid.setText(state, s, offset, box.computed.toFormat(), box.element) + grid.setText(state, s, offset, box.computed.toFormat(), box.element, + box.render.clipBox) if opacity != 0: #TODO this isn't right... - if state.clipBox.start.x < state.clipBox.send.x and - state.clipBox.start.y < state.clipBox.send.y: + if box.render.clipBox.start < box.render.clipBox.send: for child in box.children: if child of InlineBox: grid.renderInline(state, InlineBox(child), offset) else: grid.renderBlock(state, BlockBox(child), offset) - if hasClipBox: - discard state.clipBoxes.pop() -func findBlockParent(box: CSSBox): BlockBox = +# This function exists to support another insanity-inducing CSS +# construct: negative z-index. +# The issue here is that their position depends on their parent, but the +# parent box is very often not positioned yet. So we brute-force our +# way out of the problem by resolving the parent box's position here. +# The algorithm itself is mildly confusing because we must skip +# InlineBox offsets in the process - this means that there may be inline +# boxes after this pass with an unresolved position which contain block +# boxes with a resolved position. +proc resolveBlockParent(box: CSSBox): BlockBox = var it {.cursor.} = box.parent while it != nil: if it of BlockBox: - return BlockBox(it) + break it = it.parent + var toPosition: seq[BlockBox] = @[] + let findPositioned = box.computed{"position"} in PositionAbsoluteFixed + var it2 {.cursor.} = it + var parent {.cursor.}: CSSBox = nil + while it2 != nil: + if it2 of BlockBox: + let it2 = BlockBox(it2) + if it2.render.positioned and (not findPositioned or it2.positioned): + break + toPosition.add(it2) + it2 = it2.parent + var offset = if it2 != nil: it2.render.offset else: offset(0, 0) + for i in countdown(toPosition.high, 0): + let it = toPosition[i] + offset += it.state.offset + it.render = BoxRenderState( + offset: offset, + clipBox: DefaultClipBox, + positioned: true + ) + it.inheritClipBox(parent) + parent = it + if box of BlockBox: + let box = BlockBox(box) + box.render.clipBox = DefaultClipBox + if findPositioned: + box.inheritClipBox(it2) + else: + box.inheritClipBox(it) + return BlockBox(it) proc renderPositioned(grid: var FlexibleGrid; state: var RenderState; box: CSSBox) = - let parent = box.findBlockParent() + let parent = box.resolveBlockParent() let offset = if parent != nil: parent.render.offset else: offset(0, 0) if box of BlockBox: grid.renderBlock(state, BlockBox(box), offset, pass2 = true) else: grid.renderInline(state, InlineBox(box), offset, pass2 = true) -proc render*(grid: var FlexibleGrid; bgcolor: var CellColor; rootBox: BlockBox; +proc renderStack(grid: var FlexibleGrid; state: var RenderState; + stack: StackItem) = + var i = 0 + # negative z-index + while i < stack.children.len: + let it = stack.children[i] + if it.index >= 0: + break + grid.renderStack(state, it) + inc i + grid.renderPositioned(state, stack.box) + # z-index >= 0 + for it in stack.children.toOpenArray(i, stack.children.high): + grid.renderStack(state, it) + +proc render*(grid: var FlexibleGrid; bgcolor: var CellColor; stack: StackItem; attrsp: ptr WindowAttributes; images: var seq[PosBitmap]) = grid.setLen(0) - if rootBox == nil: - # no HTML element when we run cascade; just clear all lines. - return var state = RenderState( - clipBoxes: @[ClipBox(send: offset(LUnit.high, LUnit.high))], attrsp: attrsp, bgcolor: defaultColor ) - var stack = @[StackItem(box: rootBox, clipBox: state.clipBox)] - while stack.len > 0: - for it in stack: - state.clipBoxes.add(it.clipBox) - grid.renderPositioned(state, it.box) - discard state.clipBoxes.pop() - stack = move(state.nstack) - stack.sort(proc(x, y: StackItem): int = cmp(x.index, y.index)) - state.nstack = @[] + grid.renderStack(state, stack) if grid.len == 0: grid.setLen(1) bgcolor = state.bgcolor - images = state.images + images = move(state.images) |