diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/css/box.nim | 71 | ||||
-rw-r--r-- | src/css/csstree.nim | 280 | ||||
-rw-r--r-- | src/css/cssvalues.nim | 18 | ||||
-rw-r--r-- | src/css/layout.nim | 409 | ||||
-rw-r--r-- | src/css/render.nim | 75 | ||||
-rw-r--r-- | src/html/chadombuilder.nim | 1 | ||||
-rw-r--r-- | src/html/dom.nim | 46 | ||||
-rw-r--r-- | src/server/buffer.nim | 14 | ||||
-rw-r--r-- | src/types/refstring.nim | 26 |
9 files changed, 431 insertions, 509 deletions
diff --git a/src/css/box.nim b/src/css/box.nim index dca3ade2..56943f63 100644 --- a/src/css/box.nim +++ b/src/css/box.nim @@ -2,6 +2,7 @@ import css/cssvalues import css/lunit import html/dom import types/bitmap +import types/refstring type DimensionType* = enum @@ -15,10 +16,6 @@ type offset*: Offset size*: Size - InlineImage* = ref object - state*: InlineImageState - bmp*: NetworkBitmap - TextRun* = ref object offset*: Offset str*: string @@ -44,10 +41,6 @@ type InlineBoxState* = object startOffset*: Offset # offset of the first word, for position: absolute areas*: seq[Area] # background that should be painted by box - runs: seq[TextRun] - - InlineBoxType* = enum - ibtParent, ibtText, ibtNewline, ibtBitmap, ibtBox Span* = object start*: LUnit @@ -88,48 +81,27 @@ type render*: BoxRenderState # render output computed*: CSSValues element*: Element + children*: seq[CSSBox] BlockBox* = ref object of CSSBox sizes*: ResolvedSizes # tree builder output -> layout input state*: BoxLayoutState # layout output -> render input - children*: seq[CSSBox] InlineBox* = ref object of CSSBox state*: InlineBoxState - case t*: InlineBoxType - of ibtParent: - children*: seq[CSSBox] - of ibtText: - text*: CharacterData # note: this has no parent. - of ibtNewline: - discard - of ibtBitmap: - image*: InlineImage - of ibtBox: - box*: BlockBox - -iterator children*(box: CSSBox): lent CSSBox {.inline.} = - if box of BlockBox: - let box = BlockBox(box) - for child in box.children: - yield child - else: - let ibox = InlineBox(box) - case ibox.t - of ibtParent: - for child in ibox.children: - yield child - of ibtBox: - yield CSSBox(ibox.box) - else: - discard -# We store runs in state as a private field, so that we can both check -# if the box type is correct and reset them on relayout by zeroing out -# state. -template runs*(ibox: InlineBox): seq[TextRun] = - assert ibox.t == ibtText - ibox.state.runs + InlineTextBox* = ref object of InlineBox + runs*: seq[TextRun] # state + text*: RefString + + InlineNewLineBox* = ref object of InlineBox + + InlineImageBox* = ref object of InlineBox + imgstate*: InlineImageState + bmp*: NetworkBitmap + + InlineBlockBox* = ref object of InlineBox + box*: BlockBox func offset*(x, y: LUnit): Offset = return [dtHorizontal: x, dtVertical: y] @@ -205,3 +177,18 @@ func topLeft*(s: RelativeRect): Offset = proc `+=`*(span: var Span; u: LUnit) = span.start += u span.send += u + +when defined(debug): + proc computedTree*(box: CSSBox): string = + result = "<" + if box.computed{"display"} != DisplayInline: + result &= "div" + else: + result &= "span" + let computed = box.computed.copyProperties() + if computed{"display"} == DisplayBlock: + computed{"display"} = DisplayInline + result &= " style='" & $computed.serializeEmpty() & "'>\n" + for it in box.children: + result &= it.computedTree() + result &= "\n</div>" diff --git a/src/css/csstree.nim b/src/css/csstree.nim index d4702cea..95afc237 100644 --- a/src/css/csstree.nim +++ b/src/css/csstree.nim @@ -1,5 +1,12 @@ # Tree building. # +#TODO: this is currently a separate pass from layout, meaning at least +# two tree traversals are required. Ideally, these should be collapsed +# into a single pass, reusing parts of previous layout passes when +# possible. +# +# --- +# # This wouldn't be nearly as complex as it is if not for CSS's asinine # anonymous table box generation rules. In particular: # * Runs of misparented boxes inside a table/table row/table row group @@ -19,6 +26,7 @@ # Whatever your reason may be for looking at this: good luck. import chame/tags +import css/box import css/cascade import css/cssvalues import css/selectorparser @@ -26,32 +34,34 @@ import html/catom import html/dom import types/bitmap import types/color +import types/refstring import utils/twtstr type - StyledType* = enum + StyledNodeType = enum stElement, stText, stImage, stBr # Abstraction over the DOM to pretend that elements, text, replaced # and pseudo-elements are derived from the same type. - StyledNode* = object - element*: Element - computed*: CSSValues - pseudo*: PseudoElement + StyledNode = object + element: Element + computed: CSSValues + pseudo: PseudoElement skipChildren: bool - case t*: StyledType + case t: StyledNodeType of stText: - text*: CharacterData + text: RefString of stElement: anonChildren: seq[StyledNode] of stImage: - bmp*: NetworkBitmap + bmp: NetworkBitmap of stBr: # <br> element discard - TreeContext* = object + TreeContext = object quoteLevel: int listItemCounter: int + rootProperties: CSSValues TreeFrame = object parent: Element @@ -64,6 +74,9 @@ type anonInlineComputed: CSSValues pctx: ptr TreeContext +# Forward declarations +proc build(ctx: var TreeContext; cached: CSSBox; styledNode: StyledNode): CSSBox + template ctx(frame: TreeFrame): var TreeContext = frame.pctx[] @@ -71,7 +84,7 @@ when defined(debug): func `$`*(node: StyledNode): string = case node.t of stText: - return node.text.data + return node.text of stElement: if node.pseudo != peNone: return $node.element.tagType & "::" & $node.pseudo @@ -81,16 +94,6 @@ when defined(debug): of stBr: return "#br" -# Root -proc initStyledElement*(element: Element): StyledNode = - if element.computed == nil: - element.applyStyle() - result = StyledNode( - t: stElement, - element: element, - computed: element.computed - ) - func inheritFor(frame: TreeFrame; display: CSSDisplay): CSSValues = result = frame.computed.inheritProperties() result{"display"} = display @@ -107,13 +110,13 @@ proc getAnonInlineComputed(frame: var TreeFrame): CSSValues = frame.anonInlineComputed = frame.computed.inheritProperties() return frame.anonInlineComputed -proc displayed(frame: TreeFrame; text: CharacterData): bool = - if text.data.len == 0: +proc displayed(frame: TreeFrame; text: RefString): bool = + if text.len == 0: return false return frame.computed{"display"} == DisplayInline or frame.lastChildWasInline or frame.computed{"white-space"} in WhiteSpacePreserve or - not text.data.onlyWhitespace() + not text.onlyWhitespace() #TODO implement table columns const DisplayNoneLike = { @@ -128,18 +131,23 @@ proc displayed(frame: TreeFrame; pseudo: PseudoElement): bool = proc displayed(frame: TreeFrame; element: Element): bool = return element.computed{"display"} notin DisplayNoneLike +proc initStyledAnon(element: Element; computed: CSSValues; + children: sink seq[StyledNode] = @[]): StyledNode = + result = StyledNode( + t: stElement, + element: element, + anonChildren: children, + computed: computed, + skipChildren: true + ) + proc getInternalTableParent(frame: var TreeFrame; display: CSSDisplay): var seq[StyledNode] = if frame.anonTableDisplay != display: if frame.anonComputed == nil: frame.anonComputed = frame.inheritFor(display) frame.anonTableDisplay = display - frame.children.add(StyledNode( - t: stElement, - element: frame.parent, - computed: frame.anonComputed, - skipChildren: true - )) + frame.children.add(initStyledAnon(frame.parent, frame.anonComputed)) return frame.children[^1].anonChildren # Add an anonymous table to children, and return based on display either @@ -156,16 +164,10 @@ proc addAnonTable(frame: var TreeFrame; parentDisplay, display: CSSDisplay): DisplayTable let (outer, inner) = frame.inheritFor(anonDisplay).splitTable() frame.anonComputed = outer - frame.children.add(StyledNode( - t: stElement, - computed: outer, - skipChildren: true, - anonChildren: @[StyledNode( - t: stElement, - computed: inner, - skipChildren: true - )] - )) + frame.children.add(initStyledAnon(frame.parent, outer, @[initStyledAnon( + frame.parent, + inner + )])) if display == DisplayTableCaption: frame.anonComputed = frame.children[^1].computed return frame.children[^1].anonChildren @@ -176,11 +178,9 @@ proc addAnonTable(frame: var TreeFrame; parentDisplay, display: CSSDisplay): if frame.anonComputed{"display"} == DisplayTableRow: return frame.children[^1].anonChildren[0].anonChildren[^1].anonChildren frame.anonComputed = frame.inheritFor(DisplayTableRow) - frame.children[^1].anonChildren[0].anonChildren.add(StyledNode( - t: stElement, - element: frame.parent, - computed: frame.anonComputed, - skipChildren: true + frame.children[^1].anonChildren[0].anonChildren.add(initStyledAnon( + frame.parent, + frame.anonComputed )) return frame.children[^1].anonChildren[0].anonChildren[^1].anonChildren @@ -192,12 +192,7 @@ proc getParent(frame: var TreeFrame; computed: CSSValues; display: CSSDisplay): if display in DisplayOuterInline: if frame.anonComputed == nil: frame.anonComputed = frame.inheritFor(DisplayBlock) - frame.children.add(StyledNode( - t: stElement, - element: frame.parent, - computed: frame.anonComputed, - skipChildren: true - )) + frame.children.add(initStyledAnon(frame.parent, frame.anonComputed)) return frame.children[^1].anonChildren of DisplayTableRow: if display != DisplayTableCell: @@ -230,33 +225,21 @@ proc addListItem(frame: var TreeFrame; node: sink StyledNode) = # Generate a marker box. inc frame.ctx.listItemCounter let computed = node.computed.inheritProperties() - computed{"display"} = DisplayBlock computed{"white-space"} = WhitespacePre - let t = computed{"list-style-type"} + let counter = frame.ctx.listItemCounter let markerText = StyledNode( t: stText, element: node.element, - text: newCharacterData(t.listMarker(frame.ctx.listItemCounter)), - computed: computed.inheritProperties() + text: newRefString(computed{"list-style-type"}.listMarker(counter)), + computed: computed ) case node.computed{"list-style-position"} of ListStylePositionOutside: - # Generate a separate box for the content and marker. - node.anonChildren.add(StyledNode( - t: stElement, - element: node.element, - computed: computed, - skipChildren: true, - anonChildren: @[markerText] - )) + # Generate separate boxes for the content and marker. let computed = node.computed.inheritProperties() computed{"display"} = DisplayBlock - node.anonChildren.add(StyledNode( - t: stElement, - element: node.element, - computed: computed, - skipChildren: true - )) + node.anonChildren.add(initStyledAnon(node.element, computed, @[markerText])) + node.anonChildren.add(initStyledAnon(node.element, computed)) of ListStylePositionInside: node.anonChildren.add(markerText) frame.getParent(node.computed, node.computed{"display"}).add(node) @@ -265,12 +248,7 @@ proc addTable(frame: var TreeFrame; node: sink StyledNode) = var node = node let (outer, inner) = node.computed.splitTable() node.computed = outer - node.anonChildren.add(StyledNode( - t: stElement, - element: node.element, - computed: inner, - skipChildren: true - )) + node.anonChildren.add(initStyledAnon(node.element, inner)) frame.getParent(node.computed, node.computed{"display"}).add(node) proc add(frame: var TreeFrame; node: sink StyledNode) = @@ -293,15 +271,9 @@ proc add(frame: var TreeFrame; node: sink StyledNode) = if display == DisplayTableCaption: frame.captionSeen = true -proc addAnon(frame: var TreeFrame; children: sink seq[StyledNode]; - computed: CSSValues) = - frame.add(StyledNode( - t: stElement, - element: frame.parent, - anonChildren: children, - computed: computed, - skipChildren: true - )) +proc addAnon(frame: var TreeFrame; computed: CSSValues; + children: sink seq[StyledNode]) = + frame.add(initStyledAnon(frame.parent, computed, children)) proc addElement(frame: var TreeFrame; element: Element) = if element.computed == nil: @@ -322,7 +294,7 @@ proc addPseudo(frame: var TreeFrame; pseudo: PseudoElement) = computed: frame.parent.computedMap[pseudo] )) -proc addText(frame: var TreeFrame; text: CharacterData) = +proc addText(frame: var TreeFrame; text: RefString) = if frame.displayed(text): frame.add(StyledNode( t: stText, @@ -333,7 +305,7 @@ proc addText(frame: var TreeFrame; text: CharacterData) = proc addText(frame: var TreeFrame; s: sink string) = #TODO should probably cache these... - frame.addText(newCharacterData(s)) + frame.addText(newRefString(s)) proc addImage(frame: var TreeFrame; bmp: NetworkBitmap) = if bmp != nil and bmp.cacheId != -1: @@ -362,19 +334,19 @@ proc addElementChildren(frame: var TreeFrame) = #TODO collapse subsequent text nodes into one StyledNode # (it isn't possible in HTML, only with JS DOM manipulation) let text = Text(it) - frame.addText(text) + frame.addText(text.data) proc addOptionChildren(frame: var TreeFrame; option: HTMLOptionElement) = if option.select != nil and option.select.attrb(satMultiple): frame.addText("[") - let cdata = newCharacterData(if option.selected: "*" else: " ") + let cdata = newRefString(if option.selected: "*" else: " ") let computed = option.computed.inheritProperties() computed{"color"} = cssColor(ANSIColor(1)) # red computed{"white-space"} = WhitespacePre block anon: var aframe = frame.ctx.initTreeFrame(option, computed) aframe.addText(cdata) - frame.addAnon(move(aframe.children), computed) + frame.addAnon(computed, move(aframe.children)) frame.addText("]") frame.addElementChildren() @@ -402,83 +374,103 @@ proc addChildren(frame: var TreeFrame) = else: frame.addElementChildren() -proc addContent(frame: var TreeFrame; content: CSSContent; ctx: var TreeContext; - computed: CSSValues) = +proc addContent(frame: var TreeFrame; content: CSSContent) = case content.t of ContentString: frame.addText(content.s) of ContentOpenQuote: let quotes = frame.computed{"quotes"} if quotes == nil: - frame.addText(quoteStart(ctx.quoteLevel)) + frame.addText(quoteStart(frame.ctx.quoteLevel)) elif quotes.qs.len > 0: - frame.addText(quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].s) + frame.addText(quotes.qs[min(frame.ctx.quoteLevel, quotes.qs.high)].s) else: return - inc ctx.quoteLevel + inc frame.ctx.quoteLevel of ContentCloseQuote: - if ctx.quoteLevel > 0: - dec ctx.quoteLevel - let quotes = computed{"quotes"} + if frame.ctx.quoteLevel > 0: + dec frame.ctx.quoteLevel + let quotes = frame.computed{"quotes"} if quotes == nil: - frame.addText(quoteEnd(ctx.quoteLevel)) + frame.addText(quoteEnd(frame.ctx.quoteLevel)) elif quotes.qs.len > 0: - frame.addText(quotes.qs[min(ctx.quoteLevel, quotes.qs.high)].e) + frame.addText(quotes.qs[min(frame.ctx.quoteLevel, quotes.qs.high)].e) of ContentNoOpenQuote: - inc ctx.quoteLevel + inc frame.ctx.quoteLevel of ContentNoCloseQuote: - if ctx.quoteLevel > 0: - dec ctx.quoteLevel + if frame.ctx.quoteLevel > 0: + dec frame.ctx.quoteLevel -proc build(frame: var TreeFrame; styledNode: StyledNode; - ctx: var TreeContext) = +proc buildChildren(frame: var TreeFrame; styledNode: StyledNode) = for child in styledNode.anonChildren: frame.add(child) - if styledNode.skipChildren: - return - let parent = styledNode.element - if styledNode.pseudo == peNone: - frame.addPseudo(peBefore) - frame.addChildren() - frame.addPseudo(peAfter) + if not styledNode.skipChildren: + if styledNode.pseudo == peNone: + frame.addPseudo(peBefore) + frame.addChildren() + frame.addPseudo(peAfter) + else: + for content in frame.computed{"content"}: + frame.addContent(content) + +proc buildBox(ctx: var TreeContext; frame: TreeFrame; cached: CSSBox): CSSBox = + var bbox: BlockBox = nil + let display = frame.computed{"display"} + let box = if display == DisplayInline: + InlineBox(computed: frame.computed, element: frame.parent) else: - let computed = parent.computedMap[styledNode.pseudo].inheritProperties() - for content in parent.computedMap[styledNode.pseudo]{"content"}: - frame.addContent(content, ctx, computed) - -iterator children*(styledNode: StyledNode; ctx: var TreeContext): StyledNode - {.inline.} = - if styledNode.t == stElement: + assert display notin DisplayNoneLike + bbox = BlockBox(computed: frame.computed, element: frame.parent) + bbox + for child in frame.children: + box.children.add(ctx.build(nil, child)) + if display in DisplayInlineBlockLike: + return InlineBlockBox( + computed: ctx.rootProperties, + element: frame.parent, + box: bbox + ) + return box + +proc build(ctx: var TreeContext; cached: CSSBox; styledNode: StyledNode): + CSSBox = + case styledNode.t + of stElement: for reset in styledNode.computed{"counter-reset"}: if reset.name == "list-item": ctx.listItemCounter = reset.num let listItemCounter = ctx.listItemCounter - let parent = styledNode.element - var frame = ctx.initTreeFrame(parent, styledNode.computed) - frame.build(styledNode, ctx) - for child in frame.children: - yield child + var frame = ctx.initTreeFrame(styledNode.element, styledNode.computed) + frame.buildChildren(styledNode) + let box = ctx.buildBox(frame, cached) ctx.listItemCounter = listItemCounter + return box + of stText: + return InlineTextBox( + computed: styledNode.computed, + element: styledNode.element, + text: styledNode.text + ) + of stBr: + return InlineNewLineBox( + computed: styledNode.computed, + element: styledNode.element + ) + of stImage: + return InlineImageBox( + computed: styledNode.computed, + element: styledNode.element, + bmp: styledNode.bmp + ) -when defined(debug): - proc computedTree*(styledNode: StyledNode; ctx: var TreeContext): string = - result = "" - if styledNode.t != stElement: - result &= $styledNode - else: - result &= "<" - if styledNode.computed{"display"} != DisplayInline: - result &= "div" - else: - result &= "span" - let computed = styledNode.computed.copyProperties() - if computed{"display"} == DisplayBlock: - computed{"display"} = DisplayInline - result &= " style='" & $computed.serializeEmpty() & "'>\n" - for it in styledNode.children(ctx): - result &= it.computedTree(ctx) - result &= "\n</div>" - - proc computedTree*(styledNode: StyledNode): string = - var ctx = TreeContext() - return styledNode.computedTree(ctx) +# Root +proc buildTree*(element: Element; cached: CSSBox): BlockBox = + if element.computed == nil: + element.applyStyle() + let styledNode = StyledNode( + t: stElement, + element: element, + computed: element.computed + ) + var ctx = TreeContext(rootProperties: rootProperties()) + return BlockBox(ctx.build(cached, styledNode)) diff --git a/src/css/cssvalues.nim b/src/css/cssvalues.nim index a183d6fb..15e81303 100644 --- a/src/css/cssvalues.nim +++ b/src/css/cssvalues.nim @@ -12,6 +12,7 @@ import html/catom import types/bitmap import types/color import types/opt +import types/refstring import types/winattrs import utils/twtstr @@ -354,11 +355,11 @@ type CSSContent* = object t*: CSSContentType - s*: string + s*: RefString # nil -> auto CSSQuotes* = ref object - qs*: seq[tuple[s, e: string]] + qs*: seq[tuple[s, e: RefString]] CSSCounterReset* = object name*: string @@ -533,9 +534,10 @@ const InheritedProperties = { const OverflowScrollLike* = {OverflowScroll, OverflowAuto, OverflowOverlay} const OverflowHiddenLike* = {OverflowHidden, OverflowClip} const FlexReverse* = {FlexDirectionRowReverse, FlexDirectionColumnReverse} -const DisplayOuterInline* = { - DisplayInline, DisplayInlineTable, DisplayInlineBlock, DisplayInlineFlex +const DisplayInlineBlockLike* = { + DisplayInlineTable, DisplayInlineBlock, DisplayInlineFlex } +const DisplayOuterInline* = DisplayInlineBlockLike + {DisplayInline} const DisplayInnerFlex* = {DisplayFlex, DisplayInlineFlex} const RowGroupBox* = { # Note: caption is not included here @@ -596,7 +598,7 @@ func `$`*(bmp: NetworkBitmap): string = func `$`*(content: CSSContent): string = if content.s != "": - return "url(" & content.s & ")" + return content.s return "none" func `$`(quotes: CSSQuotes): string = @@ -604,7 +606,7 @@ func `$`(quotes: CSSQuotes): string = return "auto" result = "" for (s, e) in quotes.qs: - result &= "'" & s.cssEscape() & "' '" & e.cssEscape() & "'" + result &= "'" & ($s).cssEscape() & "' '" & ($e).cssEscape() & "'" func `$`(counterreset: seq[CSSCounterReset]): string = result = "" @@ -1257,7 +1259,7 @@ func parseQuotes(cvals: openArray[CSSComponentValue]): Opt[CSSQuotes] = if tok.t != cttString: return err() if otok != nil: - res.qs.add((otok.value, tok.value)) + res.qs.add((newRefString(otok.value), newRefString(tok.value))) otok = nil else: otok = tok @@ -1286,7 +1288,7 @@ func cssContent(cvals: openArray[CSSComponentValue]): seq[CSSContent] = elif tok.value.equalsIgnoreCase("no-close-quote"): result.add(CSSContent(t: ContentNoCloseQuote)) of cttString: - result.add(CSSContent(t: ContentString, s: tok.value)) + result.add(CSSContent(t: ContentString, s: newRefString(tok.value))) else: return func parseFontWeight(cval: CSSComponentValue): Opt[int32] = diff --git a/src/css/layout.nim b/src/css/layout.nim index 0153b7e8..ba74b2bd 100644 --- a/src/css/layout.nim +++ b/src/css/layout.nim @@ -2,7 +2,6 @@ import std/algorithm import std/math import css/box -import css/csstree import css/cssvalues import css/lunit import html/dom @@ -46,7 +45,6 @@ type attrsp: ptr WindowAttributes cellSize: Size # size(w = attrsp.ppc, h = attrsp.ppl) positioned: seq[PositionedItem] - myRootProperties: CSSValues luctx: LUContext const DefaultSpan = Span(start: 0, send: LUnit.high) @@ -122,7 +120,6 @@ func establishesBFC(computed: CSSValues): bool = computed{"overflow-x"} notin {OverflowVisible, OverflowClip} #TODO contain, grid, multicol, column-span -# 2nd pass: layout func canpx(l: CSSLength; sc: SizeConstraint): bool = return l.u != clAuto and (l.u != clPerc or sc.t == scStretch) @@ -176,7 +173,7 @@ func minClamp(x: LUnit; span: Span): LUnit = # Flow is rooted in any block box that establishes a Block Formatting # Context (BFC)[1]. State associated with these is represented by the # BlockContext object. -# Then, flow includes further child "boxes"[1] of the following types: +# Then, flow includes further child "boxes"[2] of the following types: # # * Inline. These may contain further inline boxes, text, images, # or block boxes (!). @@ -236,26 +233,6 @@ func minClamp(x: LUnit; span: Span): LUnit = # * 3 # # and blocks that come after inlines simply flush the current line box. -# Note however, that the following fragment: -# -# <div id=a><span id=b>1</span><div id=c>2</div><span id=d>3</span></div> -# -# still produces this tree: -# -# * div#a -# * anonymous block -# * span#b -# * 1 -# * div#c -# * anonymous inline -# * 2 -# * anonymous block -# * span#d -# * 3 -# -# This is an artifact of the previous implementation, which assumed that -# inlines and blocks cannot be mixed together. I'm not yet sure if this -# should be changed. # # [3]: The spec itself does not even mention this case, but there is a # resolution that agrees with our new implementation: @@ -391,7 +368,7 @@ type # Inline context state: lbstate: LineBoxState whitespacenum: int - whitespaceBox: InlineBox + whitespaceBox: InlineTextBox word: InlineAtomState wrappos: int # position of last wrapping opportunity, or -1 lastTextBox: InlineBox @@ -554,9 +531,10 @@ func computeShift(fstate: FlowState; istate: InlineState): LUnit = if fstate.lbstate.iastates.len == 0: return 0 let ibox = fstate.lbstate.iastates[^1].ibox - if ibox.t == ibtText and ibox.runs.len > 0 and - ibox.runs[^1].str[^1] == ' ': - return 0 + if ibox of InlineTextBox: + let ibox = InlineTextBox(ibox) + if ibox.runs.len > 0 and ibox.runs[^1].str[^1] == ' ': + return 0 return fstate.cellWidth * fstate.whitespacenum proc newWord(fstate: var FlowState; ibox: InlineBox) = @@ -645,13 +623,15 @@ proc alignLine(fstate: var FlowState) = # init new box currentBox = box currentAreaOffsetX = iastate.offset.x - case iastate.ibox.t - of ibtBox: - iastate.ibox.box.state.offset += iastate.offset - of ibtBitmap: - iastate.ibox.image.state.offset += iastate.offset - of ibtText: + if iastate.ibox of InlineTextBox: iastate.run.offset = iastate.offset + elif iastate.ibox of InlineBlockBox: + let ibox = InlineBlockBox(iastate.ibox) + # Add the offset to avoid destroying margins (etc.) of the block. + ibox.box.state.offset += iastate.offset + elif iastate.ibox of InlineImageBox: + let ibox = InlineImageBox(iastate.ibox) + ibox.imgstate.offset = iastate.offset else: assert false if currentBox != nil: @@ -678,8 +658,9 @@ proc alignLine(fstate: var FlowState) = proc putAtom(lbstate: var LineBoxState; iastate: InlineAtomState) = lbstate.iastates.add(iastate) - if iastate.ibox.t == ibtText: - iastate.ibox.runs.add(iastate.run) + if iastate.ibox of InlineTextBox: + let ibox = InlineTextBox(iastate.ibox) + ibox.runs.add(iastate.run) proc addSpacing(fstate: var FlowState; width: LUnit; hang = false) = let ibox = fstate.whitespaceBox @@ -866,18 +847,21 @@ proc addAtom(fstate: var FlowState; istate: var InlineState; fstate.initLine() # Recompute on newline shift = fstate.computeShift(istate) - if iastate.size.w > 0 and iastate.size.h > 0 or iastate.ibox.t == ibtBox: + if iastate.size.w > 0 and iastate.size.h > 0 or + iastate.ibox of InlineBlockBox: if shift > 0: fstate.addSpacing(shift) if iastate.run != nil and fstate.lbstate.iastates.len > 0 and - istate.ibox.runs.len > 0: - let oiastate = addr fstate.lbstate.iastates[^1] - let orun = oiastate.run - if orun != nil and orun == istate.ibox.runs[^1]: - orun.str &= iastate.run.str - oiastate.size.w += iastate.size.w - fstate.lbstate.size.w += iastate.size.w - return + istate.ibox of InlineTextBox: + let ibox = InlineTextBox(istate.ibox) + if ibox.runs.len > 0: + let oiastate = addr fstate.lbstate.iastates[^1] + let orun = oiastate.run + if orun != nil and orun == ibox.runs[^1]: + orun.str &= iastate.run.str + oiastate.size.w += iastate.size.w + fstate.lbstate.size.w += iastate.size.w + return fstate.lbstate.putAtom(iastate) fstate.lbstate.iastates[^1].offset.x += fstate.lbstate.size.w fstate.lbstate.size.w += iastate.size.w @@ -961,45 +945,46 @@ proc checkWrap(fstate: var FlowState; state: var InlineState; u: uint32; fstate.finishLine(state, wrap = true) fstate.whitespacenum = 0 -proc processWhitespace(fstate: var FlowState; state: var InlineState; +proc processWhitespace(fstate: var FlowState; istate: var InlineState; c: char) = - discard fstate.addWord(state) - case state.ibox.computed{"white-space"} + let ibox = InlineTextBox(istate.ibox) + discard fstate.addWord(istate) + case ibox.computed{"white-space"} of WhitespaceNormal, WhitespaceNowrap: if fstate.whitespacenum < 1 and fstate.lbstate.iastates.len > 0: fstate.whitespacenum = 1 - fstate.whitespaceBox = state.ibox + fstate.whitespaceBox = ibox fstate.whitespaceIsLF = c == '\n' if c != '\n': fstate.whitespaceIsLF = false of WhitespacePreLine: if c == '\n': - fstate.finishLine(state, wrap = false, force = true) + fstate.finishLine(istate, wrap = false, force = true) elif fstate.whitespacenum < 1: fstate.whitespaceIsLF = false fstate.whitespacenum = 1 - fstate.whitespaceBox = state.ibox + fstate.whitespaceBox = ibox of WhitespacePre, WhitespacePreWrap: fstate.whitespaceIsLF = false if c == '\n': - fstate.finishLine(state, wrap = false, force = true) + fstate.finishLine(istate, wrap = false, force = true) elif c == '\t': let realWidth = fstate.lbstate.charwidth + fstate.whitespacenum # We must flush first, because addWord would otherwise try to wrap the # line. (I think.) - fstate.flushWhitespace(state) + fstate.flushWhitespace(istate) let w = ((realWidth + 8) and not 7) - realWidth fstate.word.run.str.addUTF8(tabPUAPoint(w)) fstate.word.size.w += w * fstate.cellWidth fstate.lbstate.charwidth += w # Ditto here - we don't want the tab stop to get merged into the next # word. - discard fstate.addWord(state) + discard fstate.addWord(istate) else: inc fstate.whitespacenum - fstate.whitespaceBox = state.ibox + fstate.whitespaceBox = ibox # set the "last word's last rune width" to the previous rune width - state.lastrw = state.prevrw + istate.lastrw = istate.prevrw proc layoutTextLoop(fstate: var FlowState; state: var InlineState; str: string) = @@ -1717,103 +1702,102 @@ proc layoutOuterBlock(fstate: var FlowState; child: BlockBox; newLine: newLine )) -proc addInlineBlock(fstate: var FlowState; istate: var InlineState; - box: BlockBox) = - let lctx = fstate.lctx - var sizes = lctx.resolveFloatSizes(fstate.space, box.computed) - lctx.roundSmallMarginsAndPadding(sizes) - lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes) - # Apply the block box's properties to the atom itself. - let iastate = InlineAtomState( - ibox: istate.ibox, - baseline: box.state.baseline + sizes.margin.top, - vertalign: box.computed{"vertical-align"}, - size: size( - w = box.outerSize(dtHorizontal, sizes), - h = box.outerSize(dtVertical, sizes) + box.state.marginBottom - ) - ) - discard fstate.addAtom(istate, iastate) - fstate.intr.w = max(fstate.intr.w, box.state.intr.w) - fstate.lbstate.intrh = max(fstate.lbstate.intrh, iastate.size.h) - fstate.lbstate.charwidth = 0 - fstate.whitespacenum = 0 - -proc addBox(fstate: var FlowState; state: var InlineState; box: BlockBox) = - # Absolute is a bit of a special case in inline. - if box.computed{"position"} notin PositionAbsoluteFixed and - box.computed{"display"} in DisplayOuterInline: - fstate.addInlineBlock(state, box) - else: - assert box.computed{"position"} in PositionAbsoluteFixed - var textAlign = state.ibox.computed{"text-align"} +proc layoutInlineBlock(fstate: var FlowState; ibox: InlineBlockBox) = + let box = ibox.box + if box.computed{"position"} in PositionAbsoluteFixed: + # Absolute is a bit of a special case in inline: while the spec + # *says* it should blockify, absolutely positioned inline-blocks are + # placed in a different place than absolutely positioned blocks (and + # websites depend on this). + var textAlign = ibox.computed{"text-align"} if not fstate.space.w.isDefinite(): # Aligning min-content or max-content is nonsensical. textAlign = TextAlignLeft fstate.layoutOuterBlock(box, textAlign) + else: + # A real inline block. + let lctx = fstate.lctx + var sizes = lctx.resolveFloatSizes(fstate.space, box.computed) + lctx.roundSmallMarginsAndPadding(sizes) + lctx.layoutRootBlock(box, sizes.margin.topLeft, sizes) + # Apply the block box's properties to the atom itself. + let iastate = InlineAtomState( + ibox: ibox, + baseline: box.state.baseline + sizes.margin.top, + vertalign: box.computed{"vertical-align"}, + size: size( + w = box.outerSize(dtHorizontal, sizes), + h = box.outerSize(dtVertical, sizes) + box.state.marginBottom + ) + ) + var istate = InlineState(ibox: ibox) + discard fstate.addAtom(istate, iastate) + fstate.intr.w = max(fstate.intr.w, box.state.intr.w) + fstate.lbstate.intrh = max(fstate.lbstate.intrh, iastate.size.h) + fstate.lbstate.charwidth = 0 + fstate.whitespacenum = 0 -proc addImage(fstate: var FlowState; state: var InlineState; - image: InlineImage; padding: LUnit) = - #TODO add state to image - image.state = InlineImageState( - size: size(w = image.bmp.width, h = image.bmp.height) +proc layoutImage(fstate: var FlowState; ibox: InlineImageBox; padding: LUnit) = + ibox.imgstate = InlineImageState( + size: size(w = ibox.bmp.width, h = ibox.bmp.height) ) #TODO this is hopelessly broken. # The core problem is that we generate an inner and an outer box for # images, and achieving an acceptable image sizing algorithm with this - # setup is practicaully impossible. + # setup is practically impossible. # Accordingly, a correct solution would either handle block-level # images separately, or at least resolve the outer box's sizes with # the knowledge that it is an image. - let computed = state.ibox.computed + let computed = ibox.computed let hasWidth = computed{"width"}.canpx(fstate.space.w) let hasHeight = computed{"height"}.canpx(fstate.space.h) - let osize = image.state.size + let osize = ibox.imgstate.size if hasWidth: - image.state.size.w = computed{"width"}.spx(fstate.space.w, computed, + ibox.imgstate.size.w = computed{"width"}.spx(fstate.space.w, computed, padding) if hasHeight: - image.state.size.h = computed{"height"}.spx(fstate.space.h, computed, + ibox.imgstate.size.h = computed{"height"}.spx(fstate.space.h, computed, padding) if computed{"max-width"}.canpx(fstate.space.w): let w = computed{"max-width"}.spx(fstate.space.w, computed, padding) - image.state.size.w = min(image.state.size.w, w) + ibox.imgstate.size.w = min(ibox.imgstate.size.w, w) let hasMinWidth = computed{"min-width"}.canpx(fstate.space.w) if hasMinWidth: let w = computed{"min-width"}.spx(fstate.space.w, computed, padding) - image.state.size.w = max(image.state.size.w, w) + ibox.imgstate.size.w = max(ibox.imgstate.size.w, w) if computed{"max-height"}.canpx(fstate.space.h): let h = computed{"max-height"}.spx(fstate.space.h, computed, padding) - image.state.size.h = min(image.state.size.h, h) + ibox.imgstate.size.h = min(ibox.imgstate.size.h, h) let hasMinHeight = computed{"min-height"}.canpx(fstate.space.h) if hasMinHeight: let h = computed{"min-height"}.spx(fstate.space.h, computed, padding) - image.state.size.h = max(image.state.size.h, h) + ibox.imgstate.size.h = max(ibox.imgstate.size.h, h) if not hasWidth and fstate.space.w.isDefinite(): - image.state.size.w = min(fstate.space.w.u, image.state.size.w) + ibox.imgstate.size.w = min(fstate.space.w.u, ibox.imgstate.size.w) if not hasHeight and fstate.space.h.isDefinite(): - image.state.size.h = min(fstate.space.h.u, image.state.size.h) + ibox.imgstate.size.h = min(fstate.space.h.u, ibox.imgstate.size.h) if not hasHeight and not hasWidth: if osize.w >= osize.h or not fstate.space.h.isDefinite() and fstate.space.w.isDefinite(): if osize.w > 0: - image.state.size.h = osize.h div osize.w * image.state.size.w + ibox.imgstate.size.h = osize.h div osize.w * ibox.imgstate.size.w else: if osize.h > 0: - image.state.size.w = osize.w div osize.h * image.state.size.h + ibox.imgstate.size.w = osize.w div osize.h * ibox.imgstate.size.h elif not hasHeight and osize.w != 0: - image.state.size.h = osize.h div osize.w * image.state.size.w + ibox.imgstate.size.h = osize.h div osize.w * ibox.imgstate.size.w elif not hasWidth and osize.h != 0: - image.state.size.w = osize.w div osize.h * image.state.size.h + ibox.imgstate.size.w = osize.w div osize.h * ibox.imgstate.size.h let iastate = InlineAtomState( - ibox: state.ibox, - vertalign: state.ibox.computed{"vertical-align"}, - baseline: image.state.size.h, - size: image.state.size + ibox: ibox, + vertalign: ibox.computed{"vertical-align"}, + baseline: ibox.imgstate.size.h, + size: ibox.imgstate.size ) - discard fstate.addAtom(state, iastate) + var istate = InlineState(ibox: ibox) + discard fstate.addAtom(istate, iastate) fstate.lbstate.charwidth = 0 - if image.state.size.h > 0: + if ibox.imgstate.size.h > 0: # Setting the atom size as intr.w might result in a circular dependency # between table cell sizing and image sizing when we don't have a definite # parent size yet. e.g. <img width=100% ...> with an indefinite containing @@ -1824,90 +1808,96 @@ proc addImage(fstate: var FlowState; state: var InlineState; # So check if any dimension is fixed, and if yes, report the intrinsic # minimum dimension as that or the atom size (whichever is greater). if computed{"width"}.u != clPerc or computed{"min-width"}.u != clPerc: - fstate.intr.w = max(fstate.intr.w, image.state.size.w) + fstate.intr.w = max(fstate.intr.w, ibox.imgstate.size.w) if computed{"height"}.u != clPerc or computed{"min-height"}.u != clPerc: - fstate.lbstate.intrh = max(fstate.lbstate.intrh, image.state.size.h) + fstate.lbstate.intrh = max(fstate.lbstate.intrh, ibox.imgstate.size.h) proc layoutInline(fstate: var FlowState; ibox: InlineBox) = let lctx = fstate.lctx let computed = ibox.computed - var padding = Span() - if ibox.t == ibtParent: + ibox.state = InlineBoxState( + startOffset: offset( + x = fstate.lbstate.widthAfterWhitespace, + y = fstate.offset.y + ) + ) + let padding = Span( + start: computed{"padding-left"}.px(fstate.space.w), + send: computed{"padding-right"}.px(fstate.space.w) + ) + if ibox of InlineTextBox: + let ibox = InlineTextBox(ibox) + ibox.runs.setLen(0) + var istate = InlineState(ibox: ibox) + fstate.layoutText(istate, ibox.text) + fstate.lastTextBox = ibox + elif ibox of InlineNewLineBox: + let ibox = InlineNewLineBox(ibox) + var istate = InlineState(ibox: ibox) + fstate.finishLine(istate, wrap = false, force = true, + ibox.computed{"clear"}) + fstate.lastTextBox = ibox + elif ibox of InlineBlockBox: + let ibox = InlineBlockBox(ibox) + fstate.layoutInlineBlock(ibox) + fstate.lastTextBox = ibox + elif ibox of InlineImageBox: + let ibox = InlineImageBox(ibox) + fstate.layoutImage(ibox, padding.sum()) + fstate.lastTextBox = ibox + else: let w = computed{"margin-left"}.px(fstate.space.w) if w != 0: fstate.initLine() fstate.lbstate.size.w += w fstate.lbstate.widthAfterWhitespace += w - padding = Span( - start: computed{"padding-left"}.px(fstate.space.w), - send: computed{"padding-right"}.px(fstate.space.w) - ) - ibox.state = InlineBoxState() - if padding.start != 0: - ibox.state.areas.add(Area( - offset: offset(x = fstate.lbstate.widthAfterWhitespace, y = 0), - size: size(w = padding.start, h = fstate.cellHeight) - )) - fstate.lbstate.paddingTodo.add((ibox, 0)) - ibox.state.startOffset = offset( - x = fstate.lbstate.widthAfterWhitespace, - y = fstate.offset.y - ) - if padding.start != 0: - fstate.initLine() - fstate.lbstate.size.w += padding.start - var state = InlineState(ibox: ibox) - if ibox.t == ibtParent and computed{"position"} != PositionStatic: - lctx.pushPositioned() - case ibox.t - of ibtNewline: - fstate.finishLine(state, wrap = false, force = true, - ibox.computed{"clear"}) - of ibtBox: fstate.addBox(state, ibox.box) - of ibtBitmap: fstate.addImage(state, ibox.image, padding.sum()) - of ibtText: - fstate.layoutText(state, ibox.text.data) - of ibtParent: + ibox.state.startOffset.x += w + if padding.start != 0: + ibox.state.areas.add(Area( + offset: offset(x = fstate.lbstate.widthAfterWhitespace, y = 0), + size: size(w = padding.start, h = fstate.cellHeight) + )) + fstate.lbstate.paddingTodo.add((ibox, 0)) + fstate.initLine() + fstate.lbstate.size.w += padding.start + if computed{"position"} != PositionStatic: + lctx.pushPositioned() for child in ibox.children: if child of InlineBox: fstate.layoutInline(InlineBox(child)) else: # It seems -moz-center uses the inline parent too... which is - # nonsense if you consider the CSS 2 anonymous box generation rules, - # but whatever. - var textAlign = state.ibox.computed{"text-align"} + # nonsense if you consider the CSS 2 anonymous box generation + # rules, but whatever. + var textAlign = ibox.computed{"text-align"} if not fstate.space.w.isDefinite(): # Aligning min-content or max-content is nonsensical. textAlign = TextAlignLeft fstate.layoutOuterBlock(BlockBox(child), textAlign) - if padding.send != 0: - ibox.state.areas.add(Area( - offset: offset(x = fstate.lbstate.size.w, y = 0), - size: size(w = padding.send, h = fstate.cellHeight) - )) - fstate.lbstate.paddingTodo.add((ibox, ibox.state.areas.high)) - if ibox.t == ibtParent: if padding.send != 0: + ibox.state.areas.add(Area( + offset: offset(x = fstate.lbstate.size.w, y = 0), + size: size(w = padding.send, h = fstate.cellHeight) + )) + fstate.lbstate.paddingTodo.add((ibox, ibox.state.areas.high)) fstate.initLine() fstate.lbstate.size.w += padding.send let marginRight = computed{"margin-right"}.px(fstate.space.w) if marginRight != 0: fstate.initLine() fstate.lbstate.size.w += marginRight - if ibox.t != ibtParent: - fstate.lastTextBox = ibox - if ibox.t == ibtParent and computed{"position"} != PositionStatic: - # This is UB in CSS 2.1, I can't find a newer spec about it, - # 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. - # Well, it seems good enough. - lctx.popPositioned(size( - w = 0, - h = fstate.offset.y + fstate.cellHeight - ibox.state.startOffset.y - )) + if computed{"position"} != PositionStatic: + # This is UB in CSS 2.1, I can't find a newer spec about it, + # 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. + # Well, it seems good enough. + lctx.popPositioned(size( + w = 0, + h = fstate.offset.y + fstate.cellHeight - ibox.state.startOffset.y + )) proc layoutFlow0(fstate: var FlowState; sizes: ResolvedSizes; box: BlockBox) = fstate.lbstate = fstate.initLineBoxState() @@ -2863,82 +2853,7 @@ proc layoutRootBlock(lctx: LayoutContext; box: BlockBox; offset: Offset; box.state.intr.h = max(box.state.intr.h, bctx.maxFloatHeight) box.state.marginBottom = marginBottom -# 1st pass: build tree -#TODO this tree traversal is largely redundant, and should be moved -# to csstree. (Then it should be collapsed into the second pass by -# lazily generating child boxes, taking boxes from the previous pass -# when possible.) - -proc build(ctx: var TreeContext; styledNode: StyledNode; - children: var seq[CSSBox]) - -proc buildInline(ctx: var TreeContext; styledNode: StyledNode): InlineBox = - let ibox = InlineBox( - t: ibtParent, - computed: styledNode.computed, - element: styledNode.element - ) - ctx.build(styledNode, ibox.children) - return ibox - -proc buildBlock(ctx: var TreeContext; styledNode: StyledNode): BlockBox = - let box = BlockBox(computed: styledNode.computed, element: styledNode.element) - ctx.build(styledNode, box.children) - return box - -proc buildInlineBlock(ctx: var TreeContext; styledNode: StyledNode): InlineBox = - return InlineBox( - t: ibtBox, - computed: styledNode.computed.inheritProperties(), - element: styledNode.element, - box: ctx.buildBlock(styledNode) - ) - -proc buildFromElem(ctx: var TreeContext; styledNode: StyledNode): CSSBox = - return case styledNode.computed{"display"} - of DisplayBlock, DisplayFlowRoot, DisplayFlex, DisplayTable, - DisplayTableCaption, DisplayTableCell, RowGroupBox, DisplayTableRow, - DisplayTableWrapper, DisplayListItem: - ctx.buildBlock(styledNode) - of DisplayInlineBlock, DisplayInlineTable, DisplayInlineFlex: - ctx.buildInlineBlock(styledNode) - of DisplayInline: ctx.buildInline(styledNode) - of DisplayTableColumn, DisplayTableColumnGroup, #TODO - DisplayNone: - assert false - nil - -proc build(ctx: var TreeContext; styledNode: StyledNode; - children: var seq[CSSBox]) = - for child in styledNode.children(ctx): - case child.t - of stElement: - children.add(ctx.buildFromElem(child)) - of stText: - children.add(InlineBox( - t: ibtText, - computed: child.computed, - element: child.element, - text: child.text - )) - of stBr: - children.add(InlineBox( - t: ibtNewline, - computed: child.computed, - element: child.element - )) - of stImage: - children.add(InlineBox( - t: ibtBitmap, - computed: child.computed, - element: child.element, - image: InlineImage(bmp: child.bmp) - )) - -proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox = - let box = BlockBox(computed: root.computed, element: root.element) - var ctx = TreeContext() - ctx.build(root, box.children) +proc layout*(box: BlockBox; attrsp: ptr WindowAttributes) = let space = availableSpace( w = stretch(attrsp[].widthPx), h = stretch(attrsp[].heightPx) @@ -2947,7 +2862,6 @@ proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox = attrsp: attrsp, cellSize: size(w = attrsp.ppc, h = attrsp.ppl), positioned: @[PositionedItem(), PositionedItem()], - myRootProperties: rootProperties(), luctx: LUContext() ) let sizes = lctx.resolveBlockSizes(space, box.computed) @@ -2965,4 +2879,3 @@ proc layout*(root: StyledNode; attrsp: ptr WindowAttributes): BlockBox = size.w = max(size.w, box.state.size.w) size.h = max(size.h, box.state.size.h) lctx.popPositioned(size) - return box diff --git a/src/css/render.nim b/src/css/render.nim index 241167bb..445896ac 100644 --- a/src/css/render.nim +++ b/src/css/render.nim @@ -360,43 +360,40 @@ proc paintInlineBox(grid: var FlexibleGrid; state: var RenderState; alpha) proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState; - box: InlineBox; offset: Offset; bgcolor0: ARGBColor; + ibox: InlineBox; offset: Offset; bgcolor0: ARGBColor; pass2 = false) = - let position = box.computed{"position"} + let position = ibox.computed{"position"} #TODO stacking contexts - let bgcolor = box.computed{"background-color"} + let bgcolor = ibox.computed{"background-color"} var bgcolor0 = bgcolor0 if bgcolor.isCell: let bgcolor = bgcolor.cellColor() if bgcolor.t != ctNone: - grid.paintInlineBox(state, box, offset, bgcolor, 255) + grid.paintInlineBox(state, ibox, offset, bgcolor, 255) else: bgcolor0 = bgcolor0.blend(bgcolor.argb) if bgcolor0.a > 0: - grid.paintInlineBox(state, box, offset, + grid.paintInlineBox(state, ibox, offset, bgcolor0.rgb.cellColor(), bgcolor0.a) - let startOffset = offset + box.state.startOffset - box.render.offset = startOffset - case box.t - of ibtParent: - if position != PositionStatic: - state.absolutePos.add(startOffset) - for child in box.children: - if child of InlineBox: - grid.renderInlineBox(state, InlineBox(child), offset, bgcolor0) - else: - grid.renderBlockBox(state, BlockBox(child), offset) - if position != PositionStatic: - discard state.absolutePos.pop() - of ibtBox: - grid.renderBlockBox(state, box.box, offset) - of ibtBitmap: - if box.computed{"visibility"} != VisibilityVisible: + let startOffset = offset + ibox.state.startOffset + ibox.render.offset = startOffset + 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) + elif ibox of InlineBlockBox: + let ibox = InlineBlockBox(ibox) + grid.renderBlockBox(state, ibox.box, offset) + elif ibox of InlineImageBox: + let ibox = InlineImageBox(ibox) + if ibox.computed{"visibility"} != VisibilityVisible: return - let image = box.image - let offset = offset + image.state.offset - let x2p = offset.x + image.state.size.w - let y2p = offset.y + image.state.size.h + 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 @@ -407,7 +404,7 @@ proc renderInlineBox(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, - box.element, 0) + ibox.element, 0) 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 @@ -417,18 +414,22 @@ proc renderInlineBox(grid: var FlexibleGrid; state: var RenderState; y: y, offx: offx, offy: offy, - width: image.state.size.w.toInt, - height: image.state.size.h.toInt, - bmp: image.bmp + width: ibox.imgstate.size.w.toInt, + height: ibox.imgstate.size.h.toInt, + bmp: ibox.bmp )) - of ibtText: - let format = box.computed.toFormat() - for run in box.runs: - let offset = offset + run.offset - if box.computed{"visibility"} == VisibilityVisible: - grid.setText(state, run.str, offset, format, box.element) - of ibtNewline: + elif ibox of InlineNewLineBox: discard + else: + if position != PositionStatic: + state.absolutePos.add(startOffset) + for child in ibox.children: + if child of InlineBox: + grid.renderInlineBox(state, InlineBox(child), offset, bgcolor0) + else: + grid.renderBlockBox(state, BlockBox(child), offset) + if position != PositionStatic: + discard state.absolutePos.pop() proc renderBlockBox(grid: var FlexibleGrid; state: var RenderState; box: BlockBox; offset: Offset; pass2 = false) = diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim index b6eaab9b..91230d12 100644 --- a/src/html/chadombuilder.nim +++ b/src/html/chadombuilder.nim @@ -13,6 +13,7 @@ import monoucha/fromjs import monoucha/javascript import monoucha/jserror import types/opt +import types/refstring import types/url export htmlparser.ParseResult diff --git a/src/html/dom.nim b/src/html/dom.nim index e628da6e..28859e27 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -48,6 +48,7 @@ import types/color import types/opt import types/path import types/referrer +import types/refstring import types/url import types/winattrs import utils/strwidth @@ -244,7 +245,7 @@ type XMLDocument = ref object of Document CharacterData* = ref object of Node - data* {.jsgetset.}: string + data* {.jsgetset.}: RefString Text* = ref object of CharacterData @@ -309,7 +310,7 @@ type HTMLInputElement* = ref object of FormAssociatedElement inputType* {.jsget: "type".}: InputType - internalValue: CharacterData + internalValue: RefString internalChecked {.jsget: "checked".}: bool files* {.jsget.}: seq[WebFile] xcoord*: int @@ -2097,7 +2098,7 @@ func names(ctx: JSContext; map: NamedNodeMap): JSPropertyEnumList return list func length(characterData: CharacterData): uint32 {.jsfget.} = - return uint32(characterData.data.utf16Len) + return uint32(($characterData.data).utf16Len) func tagName(element: Element): string {.jsfget.} = result = element.prefix.toStr() @@ -2597,9 +2598,6 @@ func serializeFragment*(node: Node): string = result = "" result.serializeFragment(node) -func newCharacterData*(data: sink string = ""): CharacterData = - return CharacterData(data: data) - # Element proc hash(element: Element): Hash = return hash(cast[pointer](element)) @@ -2989,8 +2987,8 @@ func jsForm(this: HTMLInputElement): HTMLFormElement {.jsfget: "form".} = func value*(this: HTMLInputElement): lent string = if this.internalValue == nil: - this.internalValue = newCharacterData() - return this.internalValue.data + this.internalValue = newRefString("") + return this.internalValue func jsValue(ctx: JSContext; this: HTMLInputElement): JSValue {.jsfget: "value".} = @@ -2999,8 +2997,8 @@ func jsValue(ctx: JSContext; this: HTMLInputElement): JSValue proc `value=`*(this: HTMLInputElement; value: sink string) {.jsfset: "value".} = if this.internalValue == nil: - this.internalValue = CharacterData() - this.internalValue.data = value + this.internalValue = newRefString("") + this.internalValue.s = value this.invalidate() proc setType(this: HTMLInputElement; s: string) {.jsfset: "type".} = @@ -3021,33 +3019,33 @@ proc setChecked*(input: HTMLInputElement; b: bool) {.jsfset: "checked".} = input.invalidate() input.internalChecked = b -func inputString*(input: HTMLInputElement): CharacterData = +func inputString*(input: HTMLInputElement): RefString = case input.inputType of itCheckbox, itRadio: if input.checked: - return newCharacterData("*") - return newCharacterData(" ") + return newRefString("*") + return newRefString(" ") of itSearch, itText, itEmail, itURL, itTel: if input.value.len == 20: return input.internalValue - return CharacterData( - data: input.value.padToWidth(int(input.attrulgz(satSize).get(20))) + return newRefString( + input.value.padToWidth(int(input.attrulgz(satSize).get(20))) ) of itPassword: let n = int(input.attrulgz(satSize).get(20)) - return newCharacterData('*'.repeat(input.value.len).padToWidth(n)) + return newRefString('*'.repeat(input.value.len).padToWidth(n)) of itReset: if input.attrb(satValue): return input.internalValue - return newCharacterData("RESET") + return newRefString("RESET") of itSubmit, itButton: if input.attrb(satValue): return input.internalValue - return newCharacterData("SUBMIT") + return newRefString("SUBMIT") of itFile: #TODO multiple files? let s = if input.files.len > 0: input.files[0].name else: "" - return newCharacterData(s.padToWidth(int(input.attrulgz(satSize).get(20)))) + return newRefString(s.padToWidth(int(input.attrulgz(satSize).get(20)))) else: return input.internalValue @@ -3549,7 +3547,7 @@ func getSrc*(this: HTMLElement): tuple[src, contentType: string] = func newText*(document: Document; data: sink string): Text = return Text( internalDocument: document, - data: data, + data: newRefString(data), index: -1 ) @@ -3560,7 +3558,7 @@ func newText(ctx: JSContext; data: sink string = ""): Text {.jsctor.} = func newCDATASection(document: Document; data: string): CDATASection = return CDATASection( internalDocument: document, - data: data, + data: newRefString(data), index: -1 ) @@ -3569,7 +3567,7 @@ func newProcessingInstruction(document: Document; target: string; return ProcessingInstruction( internalDocument: document, target: target, - data: data, + data: newRefString(data), index: -1 ) @@ -3583,7 +3581,7 @@ func newDocumentFragment(ctx: JSContext): DocumentFragment {.jsctor.} = func newComment(document: Document; data: sink string): Comment = return Comment( internalDocument: document, - data: data, + data: newRefString(data), index: -1 ) @@ -5088,7 +5086,7 @@ proc setNodeValue(ctx: JSContext; node: Node; data: JSValue): Err[void] var res = "" if not JS_IsNull(data): ?ctx.fromJS(data, res) - CharacterData(node).data = move(res) + CharacterData(node).data = newRefString(move(res)) elif node of Attr: var res = "" if not JS_IsNull(data): diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 49ae89e3..fe3f2e93 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -47,6 +47,7 @@ import types/cell import types/color import types/formdata import types/opt +import types/refstring import types/url import types/winattrs import utils/strwidth @@ -767,14 +768,15 @@ proc resolveTask[T](buffer: Buffer; cmd: BufferCommand; res: T) = buffer.tasks[cmd] = 0 proc maybeReshape(buffer: Buffer) = - if buffer.document == nil or buffer.document.documentElement == nil: + let document = buffer.document + if document == nil or document.documentElement == nil: return # not parsed yet, nothing to render - if buffer.document.invalid: - let root = initStyledElement(buffer.document.documentElement) - buffer.rootBox = root.layout(addr buffer.attrs) + if document.invalid: + buffer.rootBox = document.documentElement.buildTree(buffer.rootBox) + buffer.rootBox.layout(addr buffer.attrs) buffer.lines.renderDocument(buffer.bgcolor, buffer.rootBox, addr buffer.attrs, buffer.images) - buffer.document.invalid = false + document.invalid = false if buffer.hasTask(bcOnReshape): buffer.resolveTask(bcOnReshape) else: @@ -794,7 +796,7 @@ proc processData0(buffer: Buffer; data: UnsafeSlice): bool = if data.len > 0: let lastChild = plaintext.lastChild if lastChild != nil and lastChild of Text: - Text(lastChild).data &= data + Text(lastChild).data.s &= data else: plaintext.insert(buffer.document.newText($data), nil) #TODO just invalidate document? diff --git a/src/types/refstring.nim b/src/types/refstring.nim new file mode 100644 index 00000000..55c98530 --- /dev/null +++ b/src/types/refstring.nim @@ -0,0 +1,26 @@ +import monoucha/fromjs +import monoucha/javascript +import monoucha/tojs +import types/opt + +type RefString* = ref object + s*: string + +proc newRefString*(s: sink string): RefString = + return RefString(s: s) + +converter `$`*(rs: RefString): lent string = + rs.s + +template `&=`*(rs: var RefString; ss: string) = + rs.s &= ss + +proc toJS*(ctx: JSContext; rs: RefString): JSValue = + return ctx.toJS($rs) + +proc fromJS*(ctx: JSContext; val: JSValue; rs: out RefString): Opt[void] = + rs = RefString() + if ctx.fromJS(val, rs.s).isNone: + rs = nil + return err() + return ok() |