import strutils
import unicode
import buffer/cell
import css/cascade
import css/sheet
import css/stylednode
import css/values
import html/dom
import io/window
import layout/box
import layout/engine
import types/color
import utils/twtstr
func formatFromWord(computed: ComputedFormat): Format =
result.fgcolor = computed.color.cellColor()
if computed.bgcolor.a != 0:
result.bgcolor = computed.bgcolor.cellColor()
if computed.fontstyle in { FONT_STYLE_ITALIC, FONT_STYLE_OBLIQUE }:
result.italic = true
if computed.fontweight > 500:
result.bold = true
if TEXT_DECORATION_UNDERLINE in computed.textdecoration:
result.underline = true
if TEXT_DECORATION_OVERLINE in computed.textdecoration:
result.overline = true
if TEXT_DECORATION_LINE_THROUGH in computed.textdecoration:
result.strike = true
if TEXT_DECORATION_BLINK in computed.textdecoration:
result.blink = true
else: discard
proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat, x, y: int) {.inline.} =
var i = 0
var r: Rune
# make sure we have line y
while lines.len <= y:
lines.addLine()
var cx = 0 # first x of new string (before padding)
while cx < x and i < lines[y].str.len:
let pi = i
fastRuneAt(lines[y].str, i, r)
let w = r.twidth(cx)
# we must ensure x is max(cx, x), otherwise our assumption of cx <= x
# breaks down
if cx + w > x:
i = pi
break
cx += w
let ostr = lines[y].str.substr(i)
lines[y].str.setLen(i)
let padwidth = x - cx
if padwidth > 0:
lines[y].str &= ' '.repeat(padwidth)
lines[y].str &= linestr
let linestrwidth = linestr.twidth(x) - x
i = 0
var nx = x # last x of new string
while nx < x + linestrwidth and i < ostr.len:
fastRuneAt(ostr, i, r)
nx += r.twidth(nx)
if i < ostr.len:
lines[y].str &= ostr.substr(i)
# Negative x values make no sense from here on, as text with negative x
# coordinates can not be formatted.
let x = max(0, x)
if cx < 0:
cx = 0
if nx < 0:
nx = 0
# Skip unchanged formats before the new string
var fi = lines[y].findFormatN(cx) - 1
if padwidth > 0:
# Replace formats for padding
var padformat = newFormat()
if fi == -1:
# No formats
inc fi # insert after first format (meaning fi = 0)
lines[y].insertFormat(cx, fi, padformat)
else:
# First format's pos may be == cx here.
if lines[y].formats[fi].pos == cx:
padformat.bgcolor = lines[y].formats[fi].format.bgcolor
let node = lines[y].formats[fi].node
lines[y].formats.delete(fi)
lines[y].insertFormat(cx, fi, padformat, node)
else:
# First format < cx => split it up
assert lines[y].formats[fi].pos < cx
padformat.bgcolor = lines[y].formats[fi].format.bgcolor
let node = lines[y].formats[fi].node
inc fi # insert after first format
lines[y].insertFormat(cx, fi, padformat, node)
inc fi # skip last format
while fi < lines[y].formats.len and lines[y].formats[fi].pos < x:
# Other formats must be > cx => replace them
padformat.bgcolor = lines[y].formats[fi].format.bgcolor
let node = lines[y].formats[fi].node
let px = lines[y].formats[fi].pos
lines[y].formats.delete(fi)
lines[y].insertFormat(px, fi, padformat, node)
inc fi
dec fi # go back to previous format, so that pos <= x
assert lines[y].formats[fi].pos <= x
# Now for the text's formats:
var format = cformat.formatFromWord()
var lformat: Format
var lnode: StyledNode
if fi == -1:
# No formats => just insert a new format at 0
inc fi
lines[y].insertFormat(x, fi, format, cformat.node)
lformat = newFormat()
else:
# First format's pos may be == x here.
lformat = lines[y].formats[fi].format # save for later use
lnode = lines[y].formats[fi].node
if lines[y].formats[fi].pos == x:
# Replace.
if cformat.bgcolor.a == 0: #TODO alpha blending
# We must check if the old string's last x position is greater than
# the new string's first x position. If not, we cannot inherit
# its bgcolor (which is supposed to end before the new string started.)
if nx > cx:
format.bgcolor = lines[y].formats[fi].format.bgcolor
lines[y].formats.delete(fi)
lines[y].insertFormat(x, fi, format, cformat.node)
else:
# First format's pos < x => split it up.
assert lines[y].formats[fi].pos < x
if cformat.bgcolor.a == 0: #TODO alpha blending
if nx > cx: # see above
format.bgcolor = lines[y].formats[fi].format.bgcolor
inc fi # insert after first format
lines[y].insertFormat(x, fi, format, cformat.node)
inc fi # skip last format
while fi < lines[y].formats.len and lines[y].formats[fi].pos < nx:
# Other formats must be > x => replace them
if cformat.bgcolor.a == 0: #TODO alpha blending
format.bgcolor = lines[y].formats[fi].format.bgcolor
let px = lines[y].formats[fi].pos
lformat = lines[y].formats[fi].format # save for later use
lnode = lines[y].formats[fi].node
lines[y].formats.delete(fi)
lines[y].insertFormat(px, fi, format, cformat.node)
inc fi
if i < ostr.len and (fi >= lines[y].formats.len or lines[y].formats[fi].pos > nx):
# nx < ostr.width, but we have removed all formatting in the range of our
# string, and no formatting comes directly after it. So we insert the
# continuation of the last format we replaced after our string.
# (Default format when we haven't replaced anything.)
lines[y].insertFormat(nx, fi, lformat, lnode)
dec fi # go back to previous format, so that pos <= nx
assert lines[y].formats[fi].pos <= nx
# That's it!
proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: WindowAttributes) =
var r: Rune
var y = (y + word.offset.y) div window.ppl # y cell
if y < 0: return # y is outside the canvas, no need to draw
var x = (x + word.offset.x) div window.ppc # x cell
var i = 0
while x < 0 and i < word.str.len:
fastRuneAt(word.str, i, r)
x += r.twidth(x)
if x < 0: return # highest x is outside the canvas, no need to draw
let linestr = word.str.substr(i)
lines.setText(linestr, word.format, x, y)
proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, window: WindowAttributes) =
var y = (y + spacing.offset.y) div window.ppl # y cell
if y < 0: return # y is outside the canvas, no need to draw
var x = (x + spacing.offset.x) div window.ppc # x cell
let width = spacing.width div window.ppc # cell width
if x + width < 0: return # highest x is outside the canvas, no need to draw
var i = 0
if x < 0:
i -= x
x = 0
let linestr = ' '.repeat(width - i)
lines.setText(linestr, spacing.format, x, y)
proc paintBackground(lines: var FlexibleGrid, color: RGBAColor, startx, starty, endx, endy: int, node: StyledNode, window: WindowAttributes) =
let color = color.cellColor()
var starty = starty div window.ppl
var endy = endy div window.ppl
if starty > endy:
swap(starty, endy)
if endy <= 0: return # highest y is outside canvas, no need to paint
if starty < 0: starty = 0
if starty == endy: return # height is 0, no need to paint
var startx = startx div window.ppc
var endx = endx div window.ppc
if endy < 0: endy = 0
if startx > endx:
swap(startx, endx)
if endx <= 0: return # highest x is outside the canvas, no need to paint
if startx < 0: startx = 0
if startx == endx: return # width is 0, no need to paint
# make sure we have line y
while lines.len <= endy:
lines.addLine()
for y in starty..<endy:
# Make sure line.width() >= endx
let linewidth = lines[y].width()
if linewidth < endx:
lines[y].addFormat(linewidth, newFormat())
lines[y].str &= ' '.repeat(endx - linewidth)
# Process formatting around startx
if lines[y].formats.len == 0:
# No formats
lines[y].addFormat(startx, newFormat())
else:
let fi = lines[y].findFormatN(startx) - 1
if fi == -1:
# No format <= startx
lines[y].insertFormat(startx, 0, newFormat())
elif lines[y].formats[fi].pos == startx:
# Last format equals startx => next comes after, nothing to be done
discard
else:
# Last format lower than startx => separate format from startx
let copy = lines[y].formats[fi]
lines[y].formats[fi].pos = startx
lines[y].formats.insert(copy, fi)
# Process formatting around endx
assert lines[y].formats.len > 0
let fi = lines[y].findFormatN(endx) - 1
if fi == -1:
# Last format > endx -> nothing to be done
discard
else:
if lines[y].formats[fi].pos != endx:
let copy = lines[y].formats[fi]
if linewidth != endx:
lines[y].formats[fi].pos = endx
lines[y].formats.insert(copy, fi)
else:
lines[y].formats.delete(fi)
lines[y].formats.insert(copy, fi)
# Paint format backgrounds between startx and endx
for fi in 0..lines[y].formats.high:
if lines[y].formats[fi].pos >= endx:
break
if lines[y].formats[fi].pos >= startx:
lines[y].formats[fi].format.bgcolor = color
lines[y].formats[fi].node = node
func calculateErrorY(ctx: InlineContext, window: WindowAttributes): int =
if ctx.lines.len <= 1: return 0
var error = 0
for i in 0 ..< ctx.lines.len:
if i < ctx.lines.high:
let dy = ctx.lines[i + 1].offset.y - ctx.lines[i].offset.y
error += dy - (dy div window.ppl) * window.ppl
return error div (ctx.lines.len - 1)
proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: WindowAttributes)
proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, window: WindowAttributes) =
let x = x + ctx.offset.x
let y = y + ctx.offset.y
let erry = ctx.calculateErrorY(window)
var i = 0
for line in ctx.lines:
let x = x + line.offset.x
let y0 = y + line.offset.y
let y = y0 - erry * i
let r = y div window.ppl
while grid.len <= r:
grid.addLine()
for atom in line.atoms:
if atom of InlineBlockBox:
let iblock = InlineBlockBox(atom)
grid.renderBlockBox(iblock.innerbox, x + iblock.offset.x, y + iblock.offset.y, window)
elif atom of InlineWord:
let word = InlineWord(atom)
grid.setRowWord(word, x, y, window)
elif atom of InlineSpacing:
let spacing = InlineSpacing(atom)
grid.setSpacing(spacing, x, y, window)
inc i
proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: WindowAttributes) =
var stack = newSeqOfCap[(BlockBox, int, int)](100)
stack.add((box, x, y))
while stack.len > 0:
var (box, x, y) = stack.pop()
x += box.offset.x
y += box.offset.y
if box.computed{"background-color"}.a != 0: #TODO color blending
grid.paintBackground(box.computed{"background-color"}, x, y, x + box.width, y + box.height, box.node, window)
if box.computed{"background-image"}.t == CONTENT_IMAGE and box.computed{"background-image"}.s != "":
# ugly hack for background-image display... TODO actually display images
let s = "[img]"
let w = s.len * window.ppc
var x = x
if box.width < w:
# text is larger than image; center it to minimize error
x -= w div 2
x += box.width div 2
x = x div window.ppc
y = y div window.ppl
if y >= 0 and x + w >= 0:
grid.setText(s, ComputedFormat(node: box.node), x, y)
if box of ListItemBox:
let box = ListItemBox(box)
if box.marker != nil:
grid.renderInlineContext(box.marker, x - box.marker.width, y, window)
if box.inline != nil:
assert box.nested.len == 0
grid.renderInlineContext(box.inline, x, y, window)
else:
for i in countdown(box.nested.high, 0):
stack.add((box.nested[i], x, y))
const css = staticRead"res/ua.css"
let uastyle = css.parseStylesheet()
const quirk = css & staticRead"res/quirk.css"
let quirkstyle = quirk.parseStylesheet()
proc renderDocument*(document: Document, window: WindowAttributes, userstyle: CSSStylesheet, layout: var Viewport, previousStyled: StyledNode): (FlexibleGrid, StyledNode) =
var uastyle = uastyle
if document.mode == QUIRKS:
uastyle = quirkstyle
let styledNode = document.applyStylesheets(uastyle, userstyle, previousStyled)
result[1] = styledNode
layout.renderLayout(styledNode)
result[0].setLen(0)
for root in layout.root:
result[0].renderBlockBox(root, 0, 0, window)
if result[0].len == 0:
result[0].addLine()