about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-07-03 23:50:50 +0200
committerbptato <nincsnevem662@gmail.com>2023-07-04 00:04:26 +0200
commit1c6ce0892f5ca63682fa2c4dfe9d6a4e81558eb4 (patch)
tree87a17f65545ad16543ecfe8b3bec675f9c6b3c51
parentee8310c5490d3366559304e056bda164f7fae726 (diff)
downloadchawan-1c6ce0892f5ca63682fa2c4dfe9d6a4e81558eb4.tar.gz
Use LayoutUnit in layout
Reduces ugly rendering caused by rounding errors.
-rw-r--r--src/css/cascade.nim9
-rw-r--r--src/css/values.nim43
-rw-r--r--src/layout/box.nim104
-rw-r--r--src/layout/engine.nim136
-rw-r--r--src/layout/layoutunit.nim48
-rw-r--r--src/render/renderdocument.nim54
6 files changed, 242 insertions, 152 deletions
diff --git a/src/css/cascade.nim b/src/css/cascade.nim
index 2582763e..7996f96e 100644
--- a/src/css/cascade.nim
+++ b/src/css/cascade.nim
@@ -12,6 +12,7 @@ import css/stylednode
 import css/values
 import html/dom
 import html/tags
+import layout/layoutunit
 import types/color
 
 type
@@ -33,12 +34,12 @@ func applies(feature: MediaFeature, window: Window): bool =
   of FEATURE_PREFERS_COLOR_SCHEME:
     return feature.b
   of FEATURE_WIDTH:
-    let a = px(feature.lengthrange.a, window.attrs, 0)
-    let b = px(feature.lengthrange.b, window.attrs, 0)
+    let a = toInt(px(feature.lengthrange.a, window.attrs, 0))
+    let b = toInt(px(feature.lengthrange.b, window.attrs, 0))
     return window.attrs.ppc * window.attrs.width in a .. b
   of FEATURE_HEIGHT:
-    let a = px(feature.lengthrange.a, window.attrs, 0)
-    let b = px(feature.lengthrange.b, window.attrs, 0)
+    let a = toInt(px(feature.lengthrange.a, window.attrs, 0))
+    let b = toInt(px(feature.lengthrange.b, window.attrs, 0))
     return window.attrs.ppl * window.attrs.height in a .. b
 
 func applies(mq: MediaQuery, window: Window): bool =
diff --git a/src/css/values.nim b/src/css/values.nim
index 6d664a25..e988333d 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -8,6 +8,7 @@ import css/cssparser
 import css/selectorparser
 import img/bitmap
 import io/window
+import layout/layoutunit
 import types/color
 import utils/opt
 import utils/twtstr
@@ -387,37 +388,39 @@ macro `{}=`*(vals: CSSComputedValues, s: string, val: typed) =
 func inherited(t: CSSPropertyType): bool =
   return InheritedArray[t]
 
-func em_to_px(em: float64, window: WindowAttributes): int =
-  int(em * float64(window.ppl))
+func em_to_px(em: float64, window: WindowAttributes): LayoutUnit =
+  em * toLayoutUnit(window.ppl)
 
-func ch_to_px(ch: float64, window: WindowAttributes): int =
-  int(ch * float64(window.ppc))
+func ch_to_px(ch: float64, window: WindowAttributes): LayoutUnit =
+  ch * toLayoutUnit(window.ppc)
 
 # 水 width, we assume it's 2 chars
-func ic_to_px(ic: float64, window: WindowAttributes): int =
-  int(ic * float64(window.ppc) * 2)
+func ic_to_px(ic: float64, window: WindowAttributes): LayoutUnit =
+  ic * toLayoutUnit(window.ppc) * 2
 
 # x-letter height, we assume it's em/2
-func ex_to_px(ex: float64, window: WindowAttributes): int =
-  int(ex * float64(window.ppc) / 2)
+func ex_to_px(ex: float64, window: WindowAttributes): LayoutUnit =
+  ex * toLayoutUnit(window.ppc) / 2
 
-func px*(l: CSSLength, window: WindowAttributes, p: int): int {.inline.} =
+func px*(l: CSSLength, window: WindowAttributes, p: LayoutUnit): LayoutUnit {.inline.} =
   case l.unit
   of UNIT_EM, UNIT_REM: em_to_px(l.num, window)
   of UNIT_CH: ch_to_px(l.num, window)
   of UNIT_IC: ic_to_px(l.num, window)
   of UNIT_EX: ex_to_px(l.num, window)
-  of UNIT_PERC: int(p / 100 * l.num)
-  of UNIT_PX: int(l.num)
-  of UNIT_CM: int(l.num * 37.8)
-  of UNIT_MM: int(l.num * 3.78)
-  of UNIT_IN: int(l.num * 96)
-  of UNIT_PC: int(l.num * 96 / 6)
-  of UNIT_PT: int(l.num * 96 / 72)
-  of UNIT_VW: int(window.width_px / 100 * l.num)
-  of UNIT_VH: int(window.height_px / 100 * l.num)
-  of UNIT_VMIN: int(min(window.width_px, window.width_px) / 100 * l.num)
-  of UNIT_VMAX: int(max(window.width_px, window.width_px) / 100 * l.num)
+  of UNIT_PERC: p * l.num / 100
+  of UNIT_PX: toLayoutUnit(l.num)
+  of UNIT_CM: toLayoutUnit(l.num * 37.8)
+  of UNIT_MM: toLayoutUnit(l.num * 3.78)
+  of UNIT_IN: toLayoutUnit(l.num * 96)
+  of UNIT_PC: toLayoutUnit(l.num * 16)
+  of UNIT_PT: toLayoutUnit(l.num * 4 / 3)
+  of UNIT_VW: toLayoutUnit(float64(window.width_px) * l.num / 100)
+  of UNIT_VH: toLayoutUnit(float64(window.height_px) * l.num / 100)
+  of UNIT_VMIN:
+    toLayoutUnit(min(window.width_px, window.width_px) / 100 * l.num)
+  of UNIT_VMAX:
+    toLayoutUnit(max(window.width_px, window.width_px) / 100 * l.num)
 
 func listMarker*(t: CSSListStyleType, i: int): string =
   case t
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 1167ca7b..87d61829 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -3,12 +3,10 @@ import options
 import css/stylednode
 import css/values
 import io/window
+import layout/layoutunit
 import types/color
 
 type
-  #LayoutUnit* = distinct int32
-  LayoutUnit* = int
-
   Offset* = object
     x*: LayoutUnit
     y*: LayoutUnit
@@ -18,8 +16,8 @@ type
     height*: LayoutUnit
 
   Strut* = object
-    pos*: int
-    neg*: int
+    pos*: LayoutUnit
+    neg*: LayoutUnit
 
   Viewport* = ref object
     window*: WindowAttributes
@@ -58,12 +56,12 @@ type
 
   InlineAtom* = ref object of RootObj
     offset*: Offset
-    width*: int
-    height*: int
+    width*: LayoutUnit
+    height*: LayoutUnit
     vertalign*: CSSVerticalAlign
-    baseline*: int
-    top*: int
-    bottom*: int
+    baseline*: LayoutUnit
+    top*: LayoutUnit
+    bottom*: LayoutUnit
 
   ComputedFormat* = ref object
     fontstyle*: CSSFontStyle
@@ -86,25 +84,25 @@ type
   LineBox* = ref object
     atoms*: seq[InlineAtom]
     offset*: Offset
-    width*: int
-    height*: int
-    baseline*: int
-    lineheight*: int #line-height property
+    width*: LayoutUnit
+    height*: LayoutUnit
+    baseline*: LayoutUnit
+    lineheight*: LayoutUnit #line-height property
 
   InlineContext* = ref object
     offset*: Offset
-    height*: int
+    height*: LayoutUnit
     lines*: seq[LineBox]
     currentLine*: LineBox
-    width*: int
-    contentWidth*: int
-    contentHeight*: Option[int]
+    width*: LayoutUnit
+    contentWidth*: LayoutUnit
+    contentHeight*: Option[LayoutUnit]
     contentWidthInfinite*: bool
 
     charwidth*: int
     whitespacenum*: int
     # this is actually xminwidth.
-    minwidth*: int
+    minwidth*: LayoutUnit
     viewport*: Viewport
     shrink*: bool
     format*: ComputedFormat
@@ -118,25 +116,25 @@ type
     offset*: Offset
 
     # This is the padding width/height.
-    width*: int
-    height*: int
-    margin_top*: int
-    margin_bottom*: int
-    margin_left*: int
-    margin_right*: int
-    padding_top*: int
-    padding_bottom*: int
-    padding_left*: int
-    padding_right*: int
-    min_width*: Option[int]
-    max_width*: Option[int]
-    min_height*: Option[int]
-    max_height*: Option[int]
+    width*: LayoutUnit
+    height*: LayoutUnit
+    margin_top*: LayoutUnit
+    margin_bottom*: LayoutUnit
+    margin_left*: LayoutUnit
+    margin_right*: LayoutUnit
+    padding_top*: LayoutUnit
+    padding_bottom*: LayoutUnit
+    padding_left*: LayoutUnit
+    padding_right*: LayoutUnit
+    min_width*: Option[LayoutUnit]
+    max_width*: Option[LayoutUnit]
+    min_height*: Option[LayoutUnit]
+    max_height*: Option[LayoutUnit]
 
     # This is the (specified) content width/height. Actual dimensions may
     # differ (i.e. overflow)
-    contentWidth*: int
-    contentHeight*: Option[int]
+    contentWidth*: LayoutUnit
+    contentHeight*: Option[LayoutUnit]
     shrink*: bool
     # Whether to stretch content to infinity.
     contentWidthInfinite*: bool
@@ -148,7 +146,7 @@ type
     # very bad name. basically the minimum content width after the contents
     # have been positioned (usually the width of the shortest word.) used
     # in table cells.
-    xminwidth*: int
+    xminwidth*: LayoutUnit
 
   ListItemBox* = ref object of BlockBox
     marker*: InlineContext
@@ -166,14 +164,14 @@ type
   RowContext* = object
     cells*: seq[CellWrapper]
     reflow*: seq[bool]
-    width*: int
+    width*: LayoutUnit
     builder*: TableRowBoxBuilder
     ncols*: int
 
   ColumnContext* = object
-    minwidth*: int
-    maxwidth*: int
-    width*: int
+    minwidth*: LayoutUnit
+    maxwidth*: LayoutUnit
+    width*: LayoutUnit
     wspecified*: bool
     weight*: float64
 
@@ -182,33 +180,21 @@ type
     rows*: seq[RowContext]
     cols*: seq[ColumnContext]
     growing*: seq[CellWrapper]
-    maxwidth*: int
-    blockspacing*: int
-    inlinespacing*: int
+    maxwidth*: LayoutUnit
+    blockspacing*: LayoutUnit
+    inlinespacing*: LayoutUnit
     collapse*: bool
 
   InlineBlockBox* = ref object of InlineAtom
     innerbox*: BlockBox
-    margin_top*: int
-    margin_bottom*: int
+    margin_top*: LayoutUnit
+    margin_bottom*: LayoutUnit
 
-proc append*(a: var Strut, b: int) =
+proc append*(a: var Strut, b: LayoutUnit) =
   if b < 0:
     a.neg = min(b, a.neg)
   else:
     a.pos = max(b, a.pos)
 
-func sum*(a: Strut): int =
+func sum*(a: Strut): LayoutUnit =
   return a.pos + a.neg
-
-#proc `div`(a, b: LayoutUnit): LayoutUnit {.borrow.}
-#
-#func `+`*(a, b: LayoutUnit): LayoutUnit {.borrow.}
-#func `-`*(a, b: LayoutUnit): LayoutUnit {.borrow.}
-#func `*`*(a, b: LayoutUnit): LayoutUnit {.borrow.}
-#func `/`*(a, b: LayoutUnit): LayoutUnit = a div b
-#
-#proc `+=`*(a: var LayoutUnit, b: LayoutUnit) {.borrow.}
-#proc `-=`*(a: var LayoutUnit, b: LayoutUnit) {.borrow.}
-#proc `*=`*(a: var LayoutUnit, b: LayoutUnit) {.borrow.}
-#proc `/=`*(a: var LayoutUnit, b: LayoutUnit) = a = a div b
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index c3e6c3e0..7cc6731d 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -6,15 +6,18 @@ import css/stylednode
 import css/values
 import io/window
 import layout/box
+import layout/layoutunit
 import utils/twtstr
 
 # Build phase
-func px(l: CSSLength, viewport: Viewport, p = 0): int {.inline.} =
+func px(l: CSSLength, viewport: Viewport, p: LayoutUnit = 0):
+    LayoutUnit {.inline.} =
   return px(l, viewport.window, p)
 
-func px(l: CSSLength, viewport: Viewport, p: Option[int]): Option[int] {.inline.} =
+func px(l: CSSLength, viewport: Viewport, p: Option[LayoutUnit]):
+    Option[LayoutUnit] {.inline.} =
   if l.unit == UNIT_PERC and p.isNone:
-    return none(int)
+    return none(LayoutUnit)
   return some(px(l, viewport.window, p.get(0)))
 
 type InlineState = object
@@ -32,20 +35,21 @@ func whitespacepre(computed: CSSComputedValues): bool =
 func nowrap(computed: CSSComputedValues): bool =
   computed{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}
 
-func cellwidth(viewport: Viewport): int {.inline.} =
+func cellwidth(viewport: Viewport): LayoutUnit =
   viewport.window.ppc
 
-func cellwidth(ictx: InlineContext): int {.inline.} =
+func cellwidth(ictx: InlineContext): LayoutUnit =
   ictx.viewport.cellwidth
 
-func cellheight(viewport: Viewport): int {.inline.} =
+func cellheight(viewport: Viewport): LayoutUnit =
   viewport.window.ppl
 
-func cellheight(ictx: InlineContext): int {.inline.} =
+func cellheight(ictx: InlineContext): LayoutUnit =
   ictx.viewport.cellheight
 
 # Whitespace between words
-func computeShift(ictx: InlineContext, computed: CSSComputedValues): int =
+func computeShift(ictx: InlineContext, computed: CSSComputedValues):
+    LayoutUnit =
   if ictx.whitespacenum > 0:
     if ictx.currentLine.atoms.len > 0 or computed.whitespacepre:
       let spacing = computed{"word-spacing"}
@@ -102,7 +106,7 @@ proc horizontalAlignLine(ictx: InlineContext, line: LineBox, computed: CSSComput
       atom.offset.x += x
   of TEXT_ALIGN_JUSTIFY:
     if not computed.whitespacepre and not last:
-      var sumwidth = 0
+      var sumwidth: LayoutUnit = 0
       var spaces = 0
       for atom in line.atoms:
         if atom of InlineSpacing:
@@ -189,8 +193,8 @@ proc verticalAlignLine(ictx: InlineContext) =
 
   # Finally, find the inline block with the largest block margins, then apply
   # these to the line itself.
-  var margin_top = 0
-  var margin_bottom = 0
+  var margin_top: LayoutUnit = 0
+  var margin_bottom: LayoutUnit = 0
 
   for atom in line.atoms:
     if atom of InlineBlockBox:
@@ -204,7 +208,8 @@ proc verticalAlignLine(ictx: InlineContext) =
   line.height += margin_top
   line.height += margin_bottom
 
-proc addSpacing(line: LineBox, width, height: int, format: ComputedFormat, hang = false) =
+proc addSpacing(line: LineBox, width, height: LayoutUnit,
+    format: ComputedFormat, hang = false) =
   let spacing = InlineSpacing(width: width, height: height, baseline: height, format: format)
   spacing.offset.x = line.width
   if not hang:
@@ -243,7 +248,7 @@ proc finish(ictx: InlineContext, computed: CSSComputedValues) =
   for line in ictx.lines:
     ictx.horizontalAlignLine(line, computed, line == ictx.lines[^1])
 
-func minwidth(atom: InlineAtom): int =
+func minwidth(atom: InlineAtom): LayoutUnit =
   if atom of InlineBlockBox:
     return cast[InlineBlockBox](atom).innerbox.xminwidth
   return atom.width
@@ -391,7 +396,8 @@ proc layoutText(ictx: InlineContext, str: string, computed: CSSComputedValues, n
 func isOuterBlock(computed: CSSComputedValues): bool =
   return computed{"display"} in {DISPLAY_BLOCK, DISPLAY_TABLE}
 
-proc resolveContentWidth(box: BlockBox, widthpx, availableWidth: int, isauto = false) =
+proc resolveContentWidth(box: BlockBox, widthpx, availableWidth: LayoutUnit,
+    isauto = false) =
   if box.computed.isOuterBlock:
     let computed = box.computed
     let total = widthpx + box.margin_left + box.margin_right +
@@ -416,8 +422,8 @@ proc resolveContentWidth(box: BlockBox, widthpx, availableWidth: int, isauto = f
 # Resolve percentage-based dimensions.
 # availableWidth: width of the containing box
 # availableHeight: ditto, but with height.
-proc resolveDimensions(box: BlockBox, availableWidth: int,
-    availableHeight: Option[int]) =
+proc resolveDimensions(box: BlockBox, availableWidth: LayoutUnit,
+    availableHeight: Option[LayoutUnit]) =
   let viewport = box.viewport
   let computed = box.computed
 
@@ -471,7 +477,8 @@ proc resolveDimensions(box: BlockBox, availableWidth: int,
         min_height.get > box.contentHeight.get:
       box.contentHeight = min_height
 
-proc resolveTableCellDimensions(box: BlockBox, availableWidth: int, availableHeight: Option[int]) =
+proc resolveTableCellDimensions(box: BlockBox, availableWidth: LayoutUnit,
+    availableHeight: Option[LayoutUnit]) =
   let viewport = box.viewport
   let computed = box.computed
 
@@ -537,7 +544,7 @@ func isShrink(box: BlockBox, parent: BlockBox = nil, override = false): bool =
   else: discard
 
 proc newTableCellBox(viewport: Viewport, builder: BoxBuilder,
-    parentWidth: int, parentHeight = none(int), shrink = true,
+    parentWidth: LayoutUnit, parentHeight = none(LayoutUnit), shrink = true,
     contentWidthInfinite = false): BlockBox =
   let box = BlockBox(
     viewport: viewport,
@@ -550,7 +557,7 @@ proc newTableCellBox(viewport: Viewport, builder: BoxBuilder,
   return box
 
 proc newFlowRootBox(viewport: Viewport, builder: BoxBuilder,
-    parentWidth: int, parentHeight = none(int), shrink = true,
+    parentWidth: LayoutUnit, parentHeight = none(LayoutUnit), shrink = true,
     contentWidthInfinite = false): BlockBox =
   let box = BlockBox(
     viewport: viewport,
@@ -603,7 +610,8 @@ proc newListItem(parent: BlockBox, builder: ListItemBoxBuilder): ListItemBox =
   box.resolveDimensions(parentWidth, parentHeight)
   return box
 
-proc newInlineBlock(viewport: Viewport, builder: BoxBuilder, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
+proc newInlineBlock(viewport: Viewport, builder: BoxBuilder,
+    parentWidth: LayoutUnit, parentHeight = none(LayoutUnit)): InlineBlockBox =
   new(result)
   result.innerbox = newFlowRootBox(viewport, builder, parentWidth, parentHeight)
   result.vertalign = builder.computed{"vertical-align"}
@@ -661,7 +669,7 @@ proc buildBlockLayout(box: BlockBox, children: seq[BoxBuilder], node: StyledNode
     discard box.viewport.positioned.pop()
 
 #TODO this is horribly inefficient, and should be inherited like xminwidth
-func firstBaseline(box: BlockBox): int =
+func firstBaseline(box: BlockBox): LayoutUnit =
   if box.inline != nil:
     if box.inline.lines.len > 0:
       return box.offset.y + box.inline.lines[0].baseline
@@ -671,9 +679,9 @@ func firstBaseline(box: BlockBox): int =
   box.offset.y
 
 #TODO ditto
-func baseline(box: BlockBox): int =
+func baseline(box: BlockBox): LayoutUnit =
   if box.inline != nil:
-    var y = 0
+    var y: LayoutUnit = 0
     for line in box.inline.lines:
       if line == box.inline.lines[^1]:
         return box.offset.y + y + line.baseline
@@ -690,7 +698,8 @@ proc buildLayout(box: BlockBox, builder: BlockBoxBuilder) =
     box.buildBlockLayout(builder.children, builder.node)
 
 # parentWidth, parentHeight: width/height of the containing block.
-proc buildInlineBlock(builder: BlockBoxBuilder, parent: InlineContext, parentWidth: int, parentHeight = none(int)): InlineBlockBox =
+proc buildInlineBlock(builder: BlockBoxBuilder, parent: InlineContext,
+    parentWidth: LayoutUnit, parentHeight = none(LayoutUnit)): InlineBlockBox =
   result = newInlineBlock(parent.viewport, builder, parentWidth)
 
   case builder.computed{"display"}
@@ -824,7 +833,8 @@ proc positionRelative(parent, box: BlockBox) =
   elif not top.auto:
     box.offset.y -= parent.height - bottom.px(parent.viewport) - box.height
 
-proc applyChildPosition(parent, child: BlockBox, spec: bool, x, y: var int, margin_todo: var Strut) =
+proc applyChildPosition(parent, child: BlockBox, spec: bool,
+    x, y: var LayoutUnit, margin_todo: var Strut) =
   if child.computed{"position"} == POSITION_ABSOLUTE: #TODO sticky, fixed
     if child.computed{"left"}.auto and child.computed{"right"}.auto:
       child.offset.x = x
@@ -845,7 +855,7 @@ proc applyChildPosition(parent, child: BlockBox, spec: bool, x, y: var int, marg
     margin_todo = Strut()
     margin_todo.append(child.margin_bottom)
 
-proc postAlignChild(box, child: BlockBox, width: int, spec: bool) =
+proc postAlignChild(box, child: BlockBox, width: LayoutUnit, spec: bool) =
   case box.computed{"text-align"}
   of TEXT_ALIGN_CHA_CENTER:
     child.offset.x += max(width div 2 - child.width div 2, 0)
@@ -856,8 +866,8 @@ proc postAlignChild(box, child: BlockBox, width: int, spec: bool) =
     child.offset.x += child.margin_left
 
 proc positionBlocks(box: BlockBox) =
-  var y = 0
-  var x = 0
+  var y: LayoutUnit = 0
+  var x: LayoutUnit = 0
   var margin_todo: Strut
 
   # If content width has been specified, use it.
@@ -928,18 +938,22 @@ proc positionBlocks(box: BlockBox) =
   box.width += box.padding_left
   box.width += box.padding_right
 
-proc buildTableCaption(viewport: Viewport, builder: TableCaptionBoxBuilder, maxwidth: int, maxheight: Option[int], shrink = false): BlockBox =
+proc buildTableCaption(viewport: Viewport, builder: TableCaptionBoxBuilder,
+    maxwidth: LayoutUnit, maxheight: Option[LayoutUnit], shrink = false):
+    BlockBox =
   result = viewport.newFlowRootBox(builder, maxwidth, maxheight, shrink)
   result.buildLayout(builder)
 
 proc buildTableCell(viewport: Viewport, builder: TableCellBoxBuilder,
-    parentWidth: int, parentHeight: Option[int], shrink: bool,
+    parentWidth: LayoutUnit, parentHeight: Option[LayoutUnit], shrink: bool,
     contentWidthInfinite = false): BlockBox =
-  result = viewport.newTableCellBox(builder, parentWidth, parentHeight,
+  let tableCell = viewport.newTableCellBox(builder, parentWidth, parentHeight,
     shrink, contentWidthInfinite)
-  result.buildLayout(builder)
+  tableCell.buildLayout(builder)
+  return tableCell
 
-proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: BlockBox, i: int): RowContext =
+proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder,
+    parent: BlockBox, i: int): RowContext =
   var ctx = RowContext(builder: box, cells: newSeq[CellWrapper](box.children.len))
   var n = 0
   var i = 0
@@ -952,7 +966,14 @@ proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: B
     let spec = (not computedWidth.auto) and computedWidth.unit != UNIT_PERC
     let box = parent.viewport.buildTableCell(cellbuilder, parent.contentWidth,
         parent.contentHeight, not spec, not spec)
-    let wrapper = CellWrapper(box: box, builder: cellbuilder, colspan: colspan, rowspan: rowspan, rowi: i, coli: n)
+    let wrapper = CellWrapper(
+      box: box,
+      builder: cellbuilder,
+      colspan: colspan,
+      rowspan: rowspan,
+      rowi: i,
+      coli: n
+    )
     ctx.cells[i] = wrapper
     if rowspan != 1:
       pctx.growing.add(wrapper)
@@ -994,22 +1015,27 @@ proc preBuildTableRow(pctx: var TableContext, box: TableRowBoxBuilder, parent: B
   ctx.ncols = n
   return ctx
 
-proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, builder: TableRowBoxBuilder): BlockBox =
-  var x = 0
+proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox,
+    builder: TableRowBoxBuilder): BlockBox =
+  var x: LayoutUnit = 0
   var n = 0
   let row = newBlockBox(parent, builder)
-  var baseline = 0
+  var baseline: LayoutUnit = 0
   for cellw in ctx.cells:
     var cell = cellw.box
-    var w = 0
+    var w: LayoutUnit = 0
     for i in n ..< n + cellw.colspan:
       w += pctx.cols[i].width
     if cellw.reflow:
       #TODO TODO TODO this is a hack, and it doesn't even work properly
       let ocomputed = cellw.builder.computed
       cellw.builder.computed = ocomputed.copyProperties()
-      cellw.builder.computed{"width"} = CSSLength(num: float64(w), unit: UNIT_PX)
-      cell = parent.viewport.buildTableCell(cellw.builder, w, none(int), parent.shrink)
+      cellw.builder.computed{"width"} = CSSLength(
+        num: toFloat64(w),
+        unit: UNIT_PX
+      )
+      cell = parent.viewport.buildTableCell(cellw.builder, w, none(LayoutUnit),
+        parent.shrink)
       cellw.builder.computed = ocomputed
       w = max(w, cell.width)
     x += pctx.inlinespacing
@@ -1017,7 +1043,10 @@ proc buildTableRow(pctx: TableContext, ctx: RowContext, parent: BlockBox, builde
     x += pctx.inlinespacing
     x += w
     n += cellw.colspan
-    if cell.computed{"vertical-align"}.keyword notin {VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM}: # baseline
+    const HasNoBaseline = {
+      VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM
+    }
+    if cell.computed{"vertical-align"}.keyword notin HasNoBaseline: # baseline
       baseline = max(cell.firstBaseline, baseline)
     row.nested.add(cell)
     row.height = max(row.height, cell.height)
@@ -1071,7 +1100,8 @@ iterator rows(builder: TableBoxBuilder): BoxBuilder {.inline.} =
   for child in footer:
     yield child
 
-proc calcUnspecifiedColIndices(ctx: var TableContext, W: var int, weight: var float64): seq[int] =
+proc calcUnspecifiedColIndices(ctx: var TableContext, W: var LayoutUnit,
+    weight: var float64): seq[int] =
   var avail = newSeqUninitialized[int](ctx.cols.len)
   var i = 0
   var j = 0
@@ -1080,9 +1110,9 @@ proc calcUnspecifiedColIndices(ctx: var TableContext, W: var int, weight: var fl
       avail[j] = i
       let colw = ctx.cols[i].width
       let w = if colw < W:
-        float64(colw)
+        toFloat64(colw)
       else:
-        float64(W) * (ln(float64(colw) / float64(W)) + 1)
+        toFloat64(W) * (ln(toFloat64(colw) / toFloat64(W)) + 1)
       ctx.cols[i].weight = w
       weight += w
       inc j
@@ -1132,11 +1162,11 @@ proc buildTableLayout(table: BlockBox, builder: TableBoxBuilder) =
         W = 0
       redo = false
       # divide delta width by sum of sqrt(width) for all elem in avail
-      let unit = float64(W) / weight
+      let unit = toFloat64(W) / weight
       weight = 0
       for i in countdown(avail.high, 0):
         let j = avail[i]
-        let x = int(unit * ctx.cols[j].weight)
+        let x = unit * ctx.cols[j].weight
         let mw = ctx.cols[j].minwidth
         ctx.cols[j].width = x
         if mw > x:
@@ -1160,7 +1190,7 @@ proc buildTableLayout(table: BlockBox, builder: TableBoxBuilder) =
         if n < row.reflow.len and row.reflow[n]:
           reflow[n] = true
         dec n
-  var y = 0
+  var y: LayoutUnit = 0
   for roww in ctx.rows:
     if roww.builder.computed{"visibility"} == VISIBILITY_COLLAPSE:
       continue
@@ -1178,27 +1208,31 @@ proc buildTableLayout(table: BlockBox, builder: TableBoxBuilder) =
   if ctx.caption != nil:
     case ctx.caption.computed{"caption-side"}
     of CAPTION_SIDE_TOP, CAPTION_SIDE_BLOCK_START:
-      let caption = table.viewport.buildTableCaption(ctx.caption, table.width, none(int), false)
+      let caption = table.viewport.buildTableCaption(ctx.caption, table.width,
+        none(LayoutUnit), false)
       for r in table.nested:
         r.offset.y += caption.height
       table.nested.insert(caption, 0)
       table.height += caption.height
       table.width = max(table.width, caption.width)
     of CAPTION_SIDE_BOTTOM, CAPTION_SIDE_BLOCK_END:
-      let caption = table.viewport.buildTableCaption(ctx.caption, table.width, none(int), false)
+      let caption = table.viewport.buildTableCaption(ctx.caption, table.width,
+        none(LayoutUnit), false)
       caption.offset.y += table.width
       table.nested.add(caption)
       table.height += caption.height
       table.width = max(table.width, caption.width)
     of CAPTION_SIDE_LEFT, CAPTION_SIDE_INLINE_START:
-      let caption = table.viewport.buildTableCaption(ctx.caption, table.contentWidth, some(table.height), true)
+      let caption = table.viewport.buildTableCaption(ctx.caption,
+        table.contentWidth, some(table.height), true)
       for r in table.nested:
         r.offset.x += caption.width
       table.nested.insert(caption, 0)
       table.width += caption.width
       table.height = max(table.height, caption.height)
     of CAPTION_SIDE_RIGHT, CAPTION_SIDE_INLINE_END:
-      let caption = table.viewport.buildTableCaption(ctx.caption, table.contentWidth, some(table.height), true)
+      let caption = table.viewport.buildTableCaption(ctx.caption,
+        table.contentWidth, some(table.height), true)
       caption.offset.x += table.width
       table.nested.add(caption)
       table.width += caption.width
diff --git a/src/layout/layoutunit.nim b/src/layout/layoutunit.nim
new file mode 100644
index 00000000..1038e5ed
--- /dev/null
+++ b/src/layout/layoutunit.nim
@@ -0,0 +1,48 @@
+# 32-bit fixed-point number, with 6 bits of precision.
+
+type LayoutUnit* = distinct int32
+
+func toInt*(a: LayoutUnit): int =
+  return int32(a) shr 6
+
+converter toLayoutUnit*(a: int32): LayoutUnit =
+  return LayoutUnit(a shl 6)
+
+converter toLayoutUnit*(a: int64): LayoutUnit =
+  return toLayoutUnit(cast[int32](a))
+
+converter toLayoutUnit*(a: int): LayoutUnit =
+  return toLayoutUnit(cast[int32](a))
+
+converter toLayoutUnit*(a: float64): LayoutUnit =
+  return LayoutUnit(int32(a * 64))
+
+func toFloat64*(a: LayoutUnit): float64 =
+  return float64(int32(a)) / 64
+
+func `$`*(a: LayoutUnit): string =
+  $toFloat64(a)
+
+func `==`*(a, b: LayoutUnit): bool {.borrow.}
+func `<`*(a, b: LayoutUnit): bool {.borrow.}
+func `<=`*(a, b: LayoutUnit): bool {.borrow.}
+func `+`*(a, b: LayoutUnit): LayoutUnit {.borrow.}
+func `+=`*(a: var LayoutUnit, b: LayoutUnit) {.borrow.}
+func `-`*(a, b: LayoutUnit): LayoutUnit {.borrow.}
+func `-=`*(a: var LayoutUnit, b: LayoutUnit) {.borrow.}
+func `*`*(a, b: LayoutUnit): LayoutUnit {.inline.} =
+  LayoutUnit((int32(a) * int32(b)) shr 6)
+func `*=`*(a: var LayoutUnit, b: LayoutUnit) {.inline.} =
+  a = a * b
+func `/`*(a, b: LayoutUnit): LayoutUnit {.inline.} =
+  let a64 = int64(a)
+  let b64 = int64(b)
+  LayoutUnit(cast[int32](((a64 shl 12) div b64) shr 6))
+func `/=`*(a: var LayoutUnit, b: LayoutUnit) {.inline.} =
+  a = a / b
+func `div`*(a, b: LayoutUnit): LayoutUnit {.inline.} =
+  a / b
+
+func min*(a, b: LayoutUnit): LayoutUnit {.borrow.}
+func max*(a, b: LayoutUnit): LayoutUnit {.borrow.}
+func clamp*(x, a, b: LayoutUnit): LayoutUnit {.borrow.}
diff --git a/src/render/renderdocument.nim b/src/render/renderdocument.nim
index d18dbdd5..6e8b4223 100644
--- a/src/render/renderdocument.nim
+++ b/src/render/renderdocument.nim
@@ -10,6 +10,7 @@ import html/dom
 import io/window
 import layout/box
 import layout/engine
+import layout/layoutunit
 import types/color
 import utils/twtstr
 
@@ -31,7 +32,8 @@ func formatFromWord(computed: ComputedFormat): Format =
     result.blink = true
   else: discard
 
-proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat, x, y: int) {.inline.} =
+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
@@ -167,13 +169,14 @@ proc setText(lines: var FlexibleGrid, linestr: string, cformat: ComputedFormat,
   assert lines[y].formats[fi].pos <= nx
   # That's it!
 
-proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: WindowAttributes) =
+proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: LayoutUnit,
+    window: WindowAttributes) =
   var r: Rune
 
-  var y = (y + word.offset.y) div window.ppl # y cell
+  var y = toInt((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 x = toInt((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)
@@ -183,12 +186,13 @@ proc setRowWord(lines: var FlexibleGrid, word: InlineWord, x, y: int, window: Wi
 
   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
+proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: LayoutUnit,
+    window: WindowAttributes) =
+  var y = toInt((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
+  var x = toInt((x + spacing.offset.x) div window.ppc) # x cell
+  let width = toInt(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
@@ -199,7 +203,8 @@ proc setSpacing(lines: var FlexibleGrid, spacing: InlineSpacing, x, y: int, wind
 
   lines.setText(linestr, spacing.format, x, y)
 
-proc paintBackground(lines: var FlexibleGrid, color: RGBAColor, startx, starty, endx, endy: int, node: StyledNode, window: WindowAttributes) =
+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
@@ -277,18 +282,22 @@ proc paintBackground(lines: var FlexibleGrid, color: RGBAColor, startx, starty,
         lines[y].formats[fi].format.bgcolor = color
         lines[y].formats[fi].node = node
 
-func calculateErrorY(ctx: InlineContext, window: WindowAttributes): int =
+func calculateErrorY(ctx: InlineContext, window: WindowAttributes):
+    LayoutUnit =
   if ctx.lines.len <= 1: return 0
-  var error = 0
+  var error: LayoutUnit = 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, posx = 0, posy = 0)
+proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: LayoutUnit,
+  window: WindowAttributes, posx: LayoutUnit = 0, posy: LayoutUnit = 0)
 
-proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int, window: WindowAttributes, posx = 0, posy = 0) =
+proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext,
+    x, y: LayoutUnit, window: WindowAttributes, posx: LayoutUnit = 0,
+    posy: LayoutUnit = 0) =
   let x = x + ctx.offset.x
   let y = y + ctx.offset.y
   let erry = ctx.calculateErrorY(window)
@@ -316,8 +325,12 @@ proc renderInlineContext(grid: var FlexibleGrid, ctx: InlineContext, x, y: int,
         grid.setSpacing(spacing, x, y, window)
     inc i
 
-proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: WindowAttributes, posx = 0, posy = 0) =
-  var stack = newSeqOfCap[(BlockBox, int, int, int, int)](100)
+proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: LayoutUnit,
+    window: WindowAttributes, posx: LayoutUnit = 0, posy: LayoutUnit = 0) =
+  var stack = newSeqOfCap[tuple[
+    box: BlockBox,
+    x, y, posx, posy: LayoutUnit
+  ]](100)
   stack.add((box, x, y, posx, posy))
 
   while stack.len > 0:
@@ -334,7 +347,12 @@ proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: Wi
 
     if box.computed{"visibility"} == VISIBILITY_VISIBLE:
       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)
+        let ix = toInt(x)
+        let iy = toInt(y)
+        let iex = toInt(x + box.width)
+        let iey = toInt(y + box.height)
+        grid.paintBackground(box.computed{"background-color"}, ix, iy, iex,
+          iey, 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]"
@@ -344,8 +362,8 @@ proc renderBlockBox(grid: var FlexibleGrid, box: BlockBox, x, y: int, window: Wi
           # text is larger than image; center it to minimize error
           ix -= w div 2
           ix += box.width div 2
-        let x = ix div window.ppc
-        let y = y div window.ppl
+        let x = toInt(ix div window.ppc)
+        let y = toInt(y div window.ppl)
         if y >= 0 and x + w >= 0:
           grid.setText(s, ComputedFormat(node: box.node), x, y)