about summary refs log tree commit diff stats
path: root/src/layout/engine.nim
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-01-28 18:58:45 +0100
committerbptato <nincsnevem662@gmail.com>2022-01-28 18:58:45 +0100
commita42f0b169f5f612b3b14026618bcb26a2afeedca (patch)
treec5cf1ee21ad32444ef74b4e06ed5c4bd16c09abc /src/layout/engine.nim
parent876f5e9c64992ffef8630cd2bafa8c5ba9431f01 (diff)
downloadchawan-a42f0b169f5f612b3b14026618bcb26a2afeedca.tar.gz
Implement line-height, vertical-align
Diffstat (limited to 'src/layout/engine.nim')
-rw-r--r--src/layout/engine.nim163
1 files changed, 114 insertions, 49 deletions
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index b5876b5c..986d6723 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -1,5 +1,6 @@
-import unicode
+import math
 import options
+import unicode
 
 import layout/box
 import html/tags
@@ -34,11 +35,17 @@ type InlineState = object
 func whitespacepre(specified: CSSSpecifiedValues): bool {.inline.} =
   specified{"white-space"} in {WHITESPACE_PRE, WHITESPACE_PRE_WRAP}
 
+func cellwidth(viewport: Viewport): int {.inline.} =
+  viewport.term.ppc
+
 func cellwidth(ictx: InlineContext): int {.inline.} =
-  ictx.viewport.term.ppc
+  ictx.viewport.cellwidth
+
+func cellheight(viewport: Viewport): int {.inline.} =
+  viewport.term.ppl
 
 func cellheight(ictx: InlineContext): int {.inline.} =
-  ictx.viewport.term.ppl
+  ictx.viewport.cellheight
 
 # Whitespace between words
 func computeShift(ictx: InlineContext, specified: CSSSpecifiedValues): int =
@@ -51,6 +58,11 @@ func computeShift(ictx: InlineContext, specified: CSSSpecifiedValues): int =
       return spacing.px(ictx.viewport)
   return 0
 
+func computeLineHeight(viewport: Viewport, specified: CSSSpecifiedValues): int =
+  if specified{"line-height"}.auto:
+    return viewport.cellheight
+  return specified{"line-height"}.px(viewport, viewport.cellheight)
+
 proc newWord(state: var InlineState) =
   let word = InlineWord()
   word.format = ComputedFormat()
@@ -62,9 +74,47 @@ proc newWord(state: var InlineState) =
   word.format.node = state.node
   state.word = word
 
-proc finishRow(ictx: InlineContext) =
-  if ictx.thisrow.height != 0:
+proc finishRow(ictx: InlineContext, specified: CSSSpecifiedValues, force = false) =
+  if ictx.thisrow.atoms.len != 0 or force:
     let oldrow = ictx.thisrow
+
+    var baseline = if oldrow.height < oldrow.lineheight:
+      let lines = oldrow.lineheight div ictx.cellheight
+      ceilDiv(lines, 2) * ictx.cellheight
+    else:
+      0
+
+    # line-height is the minimum line height
+    oldrow.height = max(oldrow.height, oldrow.lineheight)
+
+    for atom in oldrow.atoms:
+      case atom.vertalign.keyword
+      of VERTICAL_ALIGN_BASELINE:
+        let len = atom.vertalign.length.px(ictx.viewport, oldrow.lineheight)
+        baseline = max(baseline, atom.height + len)
+      of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM:
+        oldrow.height = max(atom.height, oldrow.height)
+      of VERTICAL_ALIGN_MIDDLE:
+        baseline = max(baseline, atom.height div 2)
+      else:
+        baseline = max(baseline, atom.height)
+    oldrow.height = max(baseline, oldrow.height)
+
+    for atom in oldrow.atoms:
+      let diff = case atom.vertalign.keyword
+      of VERTICAL_ALIGN_BASELINE:
+        let len = atom.vertalign.length.px(ictx.viewport, oldrow.lineheight)
+        baseline - atom.height - len
+      of VERTICAL_ALIGN_MIDDLE:
+        baseline - atom.height div 2
+      of VERTICAL_ALIGN_TOP:
+        0
+      of VERTICAL_ALIGN_BOTTOM:
+        oldrow.height - atom.height
+      else:
+        baseline - atom.height
+      atom.rely += diff
+
     ictx.rows.add(oldrow)
     ictx.height += oldrow.height
     ictx.width = max(ictx.width, oldrow.width)
@@ -82,20 +132,23 @@ proc addAtom(ictx: InlineContext, atom: InlineAtom, maxwidth: int, specified: CS
   # Line wrapping
   if specified{"white-space"} notin {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
     if ictx.thisrow.width + atom.width + shift > maxwidth:
-      ictx.finishRow()
+      ictx.finishRow(specified)
       # Recompute on newline
       shift = ictx.computeShift(specified)
       ictx.whitespace = false
 
   if atom.width > 0 and atom.height > 0:
+    atom.vertalign = specified{"vertical-align"}
+
     if shift > 0:
       let format = if atom of InlineWord:
         InlineWord(atom).format
       else:
         nil
-      ictx.thisrow.addSpacing(shift, atom.height, format)
+      ictx.thisrow.addSpacing(shift, ictx.cellheight, format)
 
     atom.relx += ictx.thisrow.width
+    ictx.thisrow.lineheight = max(ictx.thisrow.lineheight, computeLineHeight(ictx.viewport, specified))
     ictx.thisrow.width += atom.width
     ictx.thisrow.height = max(ictx.thisrow.height, atom.height)
     ictx.thisrow.atoms.add(atom)
@@ -108,9 +161,9 @@ proc addWord(state: var InlineState) =
     state.newWord()
 
 # Start a new line, even if the previous one is empty
-proc flushLine(ictx: InlineContext) =
-  ictx.thisrow.height = max(ictx.thisrow.height, ictx.cellheight)
-  ictx.finishRow()
+proc flushLine(ictx: InlineContext, specified: CSSSpecifiedValues) =
+  ictx.thisrow.lineheight = computeLineHeight(ictx.viewport, specified)
+  ictx.finishRow(specified, true)
 
 proc checkWrap(state: var InlineState, r: Rune) =
   if state.specified{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
@@ -120,11 +173,11 @@ proc checkWrap(state: var InlineState, r: Rune) =
   of WORD_BREAK_BREAK_ALL:
     if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
       state.addWord()
-      state.ictx.finishRow()
+      state.ictx.finishRow(state.specified)
       state.ictx.whitespace = false
   of WORD_BREAK_KEEP_ALL:
     if state.ictx.thisrow.width + state.word.width + shift + r.width() * state.ictx.cellwidth > state.maxwidth:
-      state.ictx.finishRow()
+      state.ictx.finishRow(state.specified)
       state.ictx.whitespace = false
   else: discard
 
@@ -135,7 +188,7 @@ proc processWhitespace(state: var InlineState, c: char) =
     state.ictx.whitespace = true
   of WHITESPACE_PRE_LINE, WHITESPACE_PRE, WHITESPACE_PRE_WRAP:
     if c == '\n':
-      state.ictx.flushLine()
+      state.ictx.flushLine(state.specified)
     else:
       state.ictx.whitespace = true
 
@@ -163,9 +216,6 @@ proc renderText*(ictx: InlineContext, str: string, maxwidth: int, specified: CSS
 
   state.addWord()
 
-proc finish(ictx: InlineContext) =
-  ictx.finishRow()
-
 proc computedDimensions(bctx: BlockContext, width: int, height: Option[int]) =
   let pwidth = bctx.specified{"width"}
   if pwidth.auto:
@@ -207,7 +257,7 @@ proc newBlockContext(parent: BlockContext, box: BlockBox): BlockContext =
   result = newBlockContext_common(parent, box)
 
 proc newInlineBlockContext(parent: BlockContext, box: InlineBlockBox): BlockContext =
-  newBlockContext_common(parent, box)
+  result = newBlockContext_common(parent, box)
 
 # Anonymous block box.
 proc newBlockContext(parent: BlockContext): BlockContext =
@@ -231,7 +281,7 @@ proc newInlineContext(bctx: BlockContext): InlineContext =
 
 # Blocks' positions do not have to be arranged if alignBlocks is called with
 # children, whence the separate procedure.
-proc arrangeBlocks(bctx: BlockContext) =
+proc arrangeBlocks(bctx: BlockContext, selfcontained: bool) =
   var y = 0
   var x = 0
   var margin_todo = 0
@@ -258,6 +308,11 @@ proc arrangeBlocks(bctx: BlockContext) =
     if mtop > bctx.margin_top or mtop < 0:
       bctx.margin_top = mtop - bctx.margin_top
 
+    if selfcontained:
+      margin_todo = bctx.margin_top
+      bctx.height += margin_todo
+      y += margin_todo
+
     apply_child(child)
     inc i
 
@@ -277,6 +332,9 @@ proc arrangeBlocks(bctx: BlockContext) =
   if mbot > bctx.margin_bottom or mbot < 0:
     bctx.margin_bottom = mbot - bctx.margin_bottom
 
+  if selfcontained:
+    bctx.height += bctx.margin_bottom
+
   bctx.height += bctx.padding_bottom
 
   if bctx.compheight.issome:
@@ -285,10 +343,18 @@ proc arrangeBlocks(bctx: BlockContext) =
   bctx.width += bctx.padding_left
   bctx.width += bctx.padding_right
 
-proc arrangeInlines(bctx: BlockContext) =
+proc arrangeInlines(bctx: BlockContext, selfcontained: bool) =
+  bctx.margin_top = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
+  bctx.margin_bottom = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)
+
   bctx.width += bctx.padding_left
   bctx.inline.relx += bctx.padding_left
 
+  if selfcontained:
+    bctx.inline.rely += bctx.margin_top
+    bctx.height += bctx.margin_top
+    bctx.height += bctx.margin_bottom
+
   bctx.height += bctx.padding_top
   bctx.inline.rely += bctx.padding_top
 
@@ -298,28 +364,24 @@ proc arrangeInlines(bctx: BlockContext) =
 
   bctx.width = min(bctx.width, bctx.compwidth)
 
-proc alignBlock(box: BlockBox)
+proc alignBlock(box: BlockBox, selfcontained = false)
 
-proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox, parentcss: CSSSpecifiedValues) =
+proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox) =
   if box.bctx.done:
     return
-  alignBlock(box)
-
-  box.bctx.rely += box.bctx.margin_top
-  box.bctx.height += box.bctx.margin_top
-  box.bctx.height += box.bctx.margin_bottom
+  alignBlock(box, true)
 
   box.bctx.relx += box.bctx.margin_left
   box.bctx.width += box.bctx.margin_left
   box.bctx.width += box.bctx.margin_right
 
-  box.ictx.addAtom(box.bctx, bctx.compwidth, parentcss)
+  box.ictx.addAtom(box.bctx, bctx.compwidth, box.specified)
   box.ictx.whitespace = false
 
 proc alignInline(bctx: BlockContext, box: InlineBox) =
   assert box.ictx != nil
   if box.newline:
-    box.ictx.flushLine()
+    box.ictx.flushLine(bctx.specified)
 
   let margin_left = box.specified{"margin-left"}.px(bctx.viewport, bctx.compwidth)
   box.ictx.thisrow.width += margin_left
@@ -327,7 +389,7 @@ proc alignInline(bctx: BlockContext, box: InlineBox) =
   let paddingformat = ComputedFormat(node: box.node)
   let padding_left = box.specified{"padding-left"}.px(bctx.viewport, bctx.compwidth)
   if padding_left > 0:
-    box.ictx.thisrow.addSpacing(padding_left, max(box.ictx.thisrow.height, 1), paddingformat)
+    box.ictx.thisrow.addSpacing(padding_left, box.ictx.cellheight, paddingformat)
 
   for text in box.text:
     assert box.children.len == 0
@@ -342,7 +404,7 @@ proc alignInline(bctx: BlockContext, box: InlineBox) =
     of DISPLAY_INLINE_BLOCK:
       let child = InlineBlockBox(child)
       child.ictx = box.ictx
-      bctx.alignInlineBlock(child, box.specified)
+      bctx.alignInlineBlock(child)
     else:
       assert false, "child.t is " & $child.t
 
@@ -355,25 +417,25 @@ proc alignInline(bctx: BlockContext, box: InlineBox) =
 
 proc alignInlines(bctx: BlockContext, inlines: seq[CSSBox]) =
   let ictx = bctx.newInlineContext()
-  for child in inlines:
-    case child.t
-    of DISPLAY_INLINE:
-      let child = InlineBox(child)
-      child.ictx = ictx
-      bctx.alignInline(child)
-    of DISPLAY_INLINE_BLOCK:
-      let child = InlineBlockBox(child)
-      child.ictx = ictx
-      bctx.alignInlineBlock(child, bctx.specified)
-    else:
-      assert false, "child.t is " & $child.t
-  ictx.finish()
+  if inlines.len > 0:
+    for child in inlines:
+      case child.t
+      of DISPLAY_INLINE:
+        let child = InlineBox(child)
+        child.ictx = ictx
+        bctx.alignInline(child)
+      of DISPLAY_INLINE_BLOCK:
+        let child = InlineBlockBox(child)
+        child.ictx = ictx
+        bctx.alignInlineBlock(child)
+      else:
+        assert false, "child.t is " & $child.t
+    ictx.finishRow(bctx.specified)
+
   bctx.height += ictx.height
   if bctx.compheight.issome:
     bctx.height = bctx.compheight.get
   bctx.width = max(bctx.width, ictx.width)
-  bctx.margin_top = bctx.specified{"margin-top"}.px(bctx.viewport, bctx.compwidth)
-  bctx.margin_bottom = bctx.specified{"margin-bottom"}.px(bctx.viewport, bctx.compwidth)
 
 template flush_group() =
   if blockgroup.len > 0:
@@ -402,19 +464,19 @@ proc alignBlocks(bctx: BlockContext, blocks: seq[CSSBox], blockgroup: var seq[CS
       blockgroup.add(child)
     else: discard #TODO
 
-proc alignBlock(box: BlockBox) =
+proc alignBlock(box: BlockBox, selfcontained = false) =
   if box.bctx.done:
     return
   if box.inlinelayout:
     # Box only contains inline boxes.
     box.bctx.alignInlines(box.children)
-    box.bctx.arrangeInlines()
+    box.bctx.arrangeInlines(selfcontained)
   else:
     var blockgroup: seq[CSSBox]
     box.bctx.alignBlocks(box.children, blockgroup, box.node)
     let bctx = box.bctx
     flush_group()
-    box.bctx.arrangeBlocks()
+    box.bctx.arrangeBlocks(selfcontained)
   box.bctx.done = true
 
 proc getBox(specified: CSSSpecifiedValues): CSSBox =
@@ -443,7 +505,10 @@ proc getTextBox(box: CSSBox): InlineBox =
   new(result)
   result.t = DISPLAY_INLINE
   result.inlinelayout = true
-  result.specified = box.specified.inheritProperties()
+  if box.specified{"display"} == DISPLAY_INLINE:
+    result.specified = box.specified
+  else:
+    result.specified = box.specified.inheritProperties()
 
 proc getPseudoBox(bctx: BlockContext, specified: CSSSpecifiedValues): CSSBox =
   let box = getBox(specified)