about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-01-29 11:04:21 +0100
committerbptato <nincsnevem662@gmail.com>2022-01-29 11:04:21 +0100
commit5c19adf246650306eaee3605b7b9fc47a6ca73fb (patch)
treea16707ce3c121b255e255980c52b8141ad1d79bf
parenta42f0b169f5f612b3b14026618bcb26a2afeedca (diff)
downloadchawan-5c19adf246650306eaee3605b7b9fc47a6ca73fb.tar.gz
Implement text-align and <center>
-rw-r--r--res/ua.css4
-rw-r--r--src/css/values.nim33
-rw-r--r--src/html/parser.nim4
-rw-r--r--src/layout/box.nim5
-rw-r--r--src/layout/engine.nim163
-rw-r--r--src/utils/radixtree.nim11
6 files changed, 161 insertions, 59 deletions
diff --git a/res/ua.css b/res/ua.css
index a618baf5..3a98099b 100644
--- a/res/ua.css
+++ b/res/ua.css
@@ -162,3 +162,7 @@ dd {
 dl[compact] dt + br {
 	display: none;
 }
+
+center {
+	text-align: -moz-center;
+}
diff --git a/src/css/values.nim b/src/css/values.nim
index 926a9392..ff3244cb 100644
--- a/src/css/values.nim
+++ b/src/css/values.nim
@@ -29,12 +29,13 @@ type
     PROPERTY_HEIGHT, PROPERTY_LIST_STYLE_TYPE, PROPERTY_PADDING,
     PROPERTY_PADDING_TOP, PROPERTY_PADDING_LEFT, PROPERTY_PADDING_RIGHT,
     PROPERTY_PADDING_BOTTOM, PROPERTY_WORD_SPACING, PROPERTY_VERTICAL_ALIGN,
-    PROPERTY_LINE_HEIGHT
+    PROPERTY_LINE_HEIGHT, PROPERTY_TEXT_ALIGN
 
   CSSValueType* = enum
     VALUE_NONE, VALUE_LENGTH, VALUE_COLOR, VALUE_CONTENT, VALUE_DISPLAY,
     VALUE_FONT_STYLE, VALUE_WHITE_SPACE, VALUE_INTEGER, VALUE_TEXT_DECORATION,
-    VALUE_WORD_BREAK, VALUE_LIST_STYLE_TYPE, VALUE_VERTICAL_ALIGN
+    VALUE_WORD_BREAK, VALUE_LIST_STYLE_TYPE, VALUE_VERTICAL_ALIGN,
+    VALUE_TEXT_ALIGN
 
   CSSGlobalValueType* = enum
     VALUE_NOGLOBAL, VALUE_INITIAL, VALUE_INHERIT, VALUE_REVERT, VALUE_UNSET
@@ -75,6 +76,10 @@ type
     VERTICAL_ALIGN_TEXT_TOP, VERTICAL_ALIGN_TEXT_BOTTOM, VERTICAL_ALIGN_MIDDLE,
     VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM
 
+  CSSTextAlign* = enum
+    TEXT_ALIGN_START, TEXT_ALIGN_END, TEXT_ALIGN_LEFT, TEXT_ALIGN_RIGHT,
+    TEXT_ALIGN_CENTER, TEXT_ALIGN_JUSTIFY, TEXT_ALIGN_MOZ_CENTER
+
 type
   CSSLength* = object
     num*: float64
@@ -114,6 +119,8 @@ type
       liststyletype*: CSSListStyleType
     of VALUE_VERTICAL_ALIGN:
       verticalalign*: CSSVerticalAlign
+    of VALUE_TEXT_ALIGN:
+      textalign*: CSSTextAlign
     of VALUE_NONE: discard
 
   CSSSpecifiedValues* = ref array[CSSPropertyType, CSSSpecifiedValue]
@@ -146,6 +153,7 @@ const PropertyNames = {
   "word-spacing": PROPERTY_WORD_SPACING,
   "vertical-align": PROPERTY_VERTICAL_ALIGN,
   "line-height": PROPERTY_LINE_HEIGHT,
+  "text-align": PROPERTY_TEXT_ALIGN,
 }.toTable()
 
 const ValueTypes = [
@@ -175,12 +183,14 @@ const ValueTypes = [
   PROPERTY_WORD_SPACING: VALUE_LENGTH,
   PROPERTY_VERTICAL_ALIGN: VALUE_VERTICAL_ALIGN,
   PROPERTY_LINE_HEIGHT: VALUE_LENGTH,
+  PROPERTY_TEXT_ALIGN: VALUE_TEXT_ALIGN,
 ]
 
 const InheritedProperties = {
   PROPERTY_COLOR, PROPERTY_FONT_STYLE, PROPERTY_WHITE_SPACE,
   PROPERTY_FONT_WEIGHT, PROPERTY_TEXT_DECORATION, PROPERTY_WORD_BREAK,
-  PROPERTY_LIST_STYLE_TYPE, PROPERTY_WORD_SPACING, PROPERTY_LINE_HEIGHT
+  PROPERTY_LIST_STYLE_TYPE, PROPERTY_WORD_SPACING, PROPERTY_LINE_HEIGHT,
+  PROPERTY_TEXT_ALIGN
 }
 
 func getPropInheritedArray(): array[CSSPropertyType, bool] =
@@ -717,6 +727,21 @@ func cssLineHeight(d: CSSDeclaration): CSSLength =
       return cssLength(d)
   raise newException(CSSValueError, "Invalid line height")
 
+func cssTextAlign(d: CSSDeclaration): CSSTextAlign =
+  if isToken(d):
+    let tok = getToken(d)
+    if tok.tokenType == CSS_IDENT_TOKEN:
+      return case $tok.value
+      of "start": TEXT_ALIGN_START
+      of "end": TEXT_ALIGN_END
+      of "left": TEXT_ALIGN_LEFT
+      of "right": TEXT_ALIGN_RIGHT
+      of "center": TEXT_ALIGN_CENTER
+      of "justify": TEXT_ALIGN_JUSTIFY
+      of "-moz-center": TEXT_ALIGN_MOZ_CENTER
+      else: raise newException(CSSValueError, "Invalid text align")
+  raise newException(CSSValueError, "Invalid text align")
+
 proc getValueFromDecl(val: CSSSpecifiedValue, d: CSSDeclaration, vtype: CSSValueType, ptype: CSSPropertyType) =
   case vtype
   of VALUE_COLOR: val.color = cssColor(d)
@@ -739,6 +764,7 @@ proc getValueFromDecl(val: CSSSpecifiedValue, d: CSSDeclaration, vtype: CSSValue
   of VALUE_WORD_BREAK: val.wordbreak = cssWordBreak(d)
   of VALUE_LIST_STYLE_TYPE: val.liststyletype = cssListStyleType(d)
   of VALUE_VERTICAL_ALIGN: val.verticalalign = cssVerticalAlign(d)
+  of VALUE_TEXT_ALIGN: val.textalign = cssTextAlign(d)
   of VALUE_NONE: discard
 
 func getInitialColor(t: CSSPropertyType): CSSColor =
@@ -811,6 +837,7 @@ func equals*(a, b: CSSSpecifiedValue): bool =
   of VALUE_WORD_BREAK: return a.wordbreak == b.wordbreak
   of VALUE_LIST_STYLE_TYPE: return a.liststyletype == b.liststyletype
   of VALUE_VERTICAL_ALIGN: return a.verticalalign == b.verticalalign
+  of VALUE_TEXT_ALIGN: return a.textalign == b.textalign
   of VALUE_NONE: return true
   return false
 
diff --git a/src/html/parser.nim b/src/html/parser.nim
index d28be97b..305d1afa 100644
--- a/src/html/parser.nim
+++ b/src/html/parser.nim
@@ -94,9 +94,9 @@ proc getescapecmd(buf: string, at: var int): string =
       s = ""
     inc i
 
-  if n.leaf:
+  if n.value.issome:
     at = i
-    return n.value
+    return n.value.get
 
   return "&"
 
diff --git a/src/layout/box.nim b/src/layout/box.nim
index aa897ae6..64fe0741 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -50,7 +50,6 @@ type
   InlineContext* = ref object
     relx*: int
     rely*: int
-    width*: int
     height*: int
     rows*: seq[InlineRow]
     thisrow*: InlineRow
@@ -59,6 +58,7 @@ type
     maxwidth*: int
     viewport*: Viewport
     node*: Node
+    shrink*: bool
 
   BlockContext* = ref object of InlineAtom
     inline*: InlineContext
@@ -75,7 +75,10 @@ type
     padding_right*: int
 
     compwidth*: int
+    maxwidth*: int
+    nocenter*: bool
     compheight*: Option[int]
+    shrink*: bool
     done*: bool
 
   InlineBox* = ref object of CSSBox
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 986d6723..18939a57 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -74,52 +74,102 @@ proc newWord(state: var InlineState) =
   word.format.node = state.node
   state.word = word
 
-proc finishRow(ictx: InlineContext, specified: CSSSpecifiedValues, force = false) =
-  if ictx.thisrow.atoms.len != 0 or force:
-    let oldrow = ictx.thisrow
+proc horizontalAlignRow(ictx: InlineContext, row: InlineRow, specified: CSSSpecifiedValues, maxwidth: int, last = false) =
+  let maxwidth = if ictx.shrink:
+    ictx.maxwidth
+  else:
+    maxwidth
+  # we don't support directions for now so left = start and right = end
+  case specified{"text-align"}
+  of TEXT_ALIGN_START, TEXT_ALIGN_LEFT:
+    discard
+  of TEXT_ALIGN_END, TEXT_ALIGN_RIGHT:
+    # move everything
+    let x = max(maxwidth, row.width) - row.width
+    for atom in row.atoms:
+      atom.relx += x
+  of TEXT_ALIGN_CENTER:
+    let x = max((max(maxwidth - row.relx, row.width)) div 2 - row.width div 2, 0)
+    for atom in row.atoms:
+      atom.relx += x
+  of TEXT_ALIGN_JUSTIFY:
+    if not specified.whitespacepre and not last:
+      var sumwidth = 0
+      var spaces = 0
+      for atom in row.atoms:
+        if atom of InlineSpacing:
+          discard
+        else:
+          inc spaces
+          sumwidth += atom.width
+      dec spaces
+      if spaces > 0:
+        let spacingwidth = (ictx.maxwidth - sumwidth) div spaces
+        let oldwidth = row.width
+        row.width = 0
+        for atom in row.atoms:
+          atom.relx = row.width
+          if atom of InlineSpacing:
+            let atom = InlineSpacing(atom)
+            atom.width = spacingwidth
+          row.width += atom.width
+  else:
+    discard
 
-    var baseline = if oldrow.height < oldrow.lineheight:
-      let lines = oldrow.lineheight div ictx.cellheight
-      ceilDiv(lines, 2) * ictx.cellheight
+proc verticalAlignRow(ictx: InlineContext) =
+  let row = ictx.thisrow
+  var baseline = if row.height < row.lineheight:
+    let lines = row.lineheight div ictx.cellheight
+    int(ceil(lines / 2)) * ictx.cellheight
+  else:
+    0
+
+  # line-height is the minimum line height
+  row.height = max(row.height, row.lineheight)
+
+  for atom in row.atoms:
+    case atom.vertalign.keyword
+    of VERTICAL_ALIGN_BASELINE:
+      let len = atom.vertalign.length.px(ictx.viewport, row.lineheight)
+      baseline = max(baseline, atom.height + len)
+    of VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_BOTTOM:
+      row.height = max(atom.height, row.height)
+    of VERTICAL_ALIGN_MIDDLE:
+      baseline = max(baseline, atom.height div 2)
     else:
+      baseline = max(baseline, atom.height)
+  row.height = max(baseline, row.height)
+
+  for atom in row.atoms:
+    let diff = case atom.vertalign.keyword
+    of VERTICAL_ALIGN_BASELINE:
+      let len = atom.vertalign.length.px(ictx.viewport, row.lineheight)
+      baseline - atom.height - len
+    of VERTICAL_ALIGN_MIDDLE:
+      baseline - atom.height div 2
+    of VERTICAL_ALIGN_TOP:
       0
+    of VERTICAL_ALIGN_BOTTOM:
+      row.height - atom.height
+    else:
+      baseline - atom.height
+    atom.rely += diff
 
-    # 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
+proc finishRow(ictx: InlineContext, specified: CSSSpecifiedValues, maxwidth: int, force = false) =
+  if ictx.thisrow.atoms.len != 0 or force:
+    ictx.verticalAlignRow()
 
+    let oldrow = ictx.thisrow
     ictx.rows.add(oldrow)
     ictx.height += oldrow.height
-    ictx.width = max(ictx.width, oldrow.width)
+    ictx.maxwidth = max(ictx.maxwidth, oldrow.width)
     ictx.thisrow = InlineRow(rely: oldrow.rely + oldrow.height)
 
+proc finish(ictx: InlineContext, specified: CSSSpecifiedValues, maxwidth: int) =
+  ictx.finishRow(specified, maxwidth)
+  for row in ictx.rows:
+    ictx.horizontalAlignRow(row, specified, maxwidth, row == ictx.rows[^1])
+
 proc addSpacing(row: InlineRow, width, height: int, format: ComputedFormat) {.inline.} =
   let spacing = InlineSpacing(width: width, height: height, format: format)
   spacing.relx = row.width
@@ -132,7 +182,7 @@ 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(specified)
+      ictx.finishRow(specified, maxwidth, false)
       # Recompute on newline
       shift = ictx.computeShift(specified)
       ictx.whitespace = false
@@ -161,9 +211,9 @@ proc addWord(state: var InlineState) =
     state.newWord()
 
 # Start a new line, even if the previous one is empty
-proc flushLine(ictx: InlineContext, specified: CSSSpecifiedValues) =
+proc flushLine(ictx: InlineContext, specified: CSSSpecifiedValues, maxwidth: int) =
   ictx.thisrow.lineheight = computeLineHeight(ictx.viewport, specified)
-  ictx.finishRow(specified, true)
+  ictx.finishRow(specified, maxwidth, true)
 
 proc checkWrap(state: var InlineState, r: Rune) =
   if state.specified{"white-space"} in {WHITESPACE_NOWRAP, WHITESPACE_PRE}:
@@ -173,11 +223,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.specified)
+      state.ictx.finishRow(state.specified, state.maxwidth, false)
       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.specified)
+      state.ictx.finishRow(state.specified, state.maxwidth, false)
       state.ictx.whitespace = false
   else: discard
 
@@ -188,7 +238,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.specified)
+      state.ictx.flushLine(state.specified, state.maxwidth)
     else:
       state.ictx.whitespace = true
 
@@ -255,9 +305,11 @@ proc newBlockContext_common(parent: BlockContext, box: CSSBox): BlockContext {.i
 
 proc newBlockContext(parent: BlockContext, box: BlockBox): BlockContext =
   result = newBlockContext_common(parent, box)
+  result.shrink = result.specified{"width"}.auto and parent.shrink
 
 proc newInlineBlockContext(parent: BlockContext, box: InlineBlockBox): BlockContext =
   result = newBlockContext_common(parent, box)
+  result.shrink = result.specified{"width"}.auto
 
 # Anonymous block box.
 proc newBlockContext(parent: BlockContext): BlockContext =
@@ -265,6 +317,7 @@ proc newBlockContext(parent: BlockContext): BlockContext =
   result.specified = parent.specified.inheritProperties()
   result.viewport = parent.viewport
   result.computedDimensions(parent.compwidth, parent.compheight)
+  result.shrink = result.specified{"width"}.auto and parent.shrink
 
 # Anonymous block box (root).
 proc newBlockContext(viewport: Viewport): BlockContext =
@@ -277,6 +330,7 @@ proc newInlineContext(bctx: BlockContext): InlineContext =
   new(result)
   result.thisrow = InlineRow()
   result.viewport = bctx.viewport
+  result.shrink = bctx.shrink
   bctx.inline = result
 
 # Blocks' positions do not have to be arranged if alignBlocks is called with
@@ -290,10 +344,14 @@ proc arrangeBlocks(bctx: BlockContext, selfcontained: bool) =
   bctx.height += bctx.padding_top
 
   x += bctx.padding_left
+  if bctx.specified{"text-align"} == TEXT_ALIGN_MOZ_CENTER:
+    x += bctx.compwidth div 2
 
   template apply_child(child: BlockContext) =
     child.rely = y
     child.relx = x + child.margin_left
+    if bctx.specified{"text-align"} == TEXT_ALIGN_MOZ_CENTER:
+      child.relx -= child.width div 2
     y += child.height
     bctx.height += child.height
     bctx.width = max(bctx.width, child.width)
@@ -381,7 +439,7 @@ proc alignInlineBlock(bctx: BlockContext, box: InlineBlockBox) =
 proc alignInline(bctx: BlockContext, box: InlineBox) =
   assert box.ictx != nil
   if box.newline:
-    box.ictx.flushLine(bctx.specified)
+    box.ictx.flushLine(bctx.specified, bctx.compwidth)
 
   let margin_left = box.specified{"margin-left"}.px(bctx.viewport, bctx.compwidth)
   box.ictx.thisrow.width += margin_left
@@ -430,12 +488,12 @@ proc alignInlines(bctx: BlockContext, inlines: seq[CSSBox]) =
         bctx.alignInlineBlock(child)
       else:
         assert false, "child.t is " & $child.t
-    ictx.finishRow(bctx.specified)
+    ictx.finish(bctx.specified, bctx.compwidth)
 
   bctx.height += ictx.height
   if bctx.compheight.issome:
     bctx.height = bctx.compheight.get
-  bctx.width = max(bctx.width, ictx.width)
+  bctx.width = max(bctx.width, ictx.maxwidth)
 
 template flush_group() =
   if blockgroup.len > 0:
@@ -538,13 +596,24 @@ proc generateBox(elem: Element, viewport: Viewport, bctx: BlockContext = nil): C
   if viewport.map[elem.uid] != nil:
     let box = viewport.map[elem.uid]
     var bctx = bctx
-    if box.specified{"display"} in {DISPLAY_BLOCK, DISPLAY_LIST_ITEM, DISPLAY_INLINE_BLOCK}:
+    case box.specified{"display"}
+    of DISPLAY_BLOCK, DISPLAY_LIST_ITEM:
       let box = BlockBox(box)
       if bctx == nil:
         box.bctx = viewport.newBlockContext()
       else:
         box.bctx = bctx.newBlockContext(box)
       bctx = box.bctx
+    of DISPLAY_INLINE_BLOCK:
+      let box = InlineBlockBox(box)
+      if bctx == nil:
+        assert false
+        box.bctx = viewport.newBlockContext()
+      else:
+        box.bctx = bctx.newInlineBlockContext(box)
+      bctx = box.bctx
+    else:
+      discard
 
     var i = 0
     while i < box.children.len:
diff --git a/src/utils/radixtree.nim b/src/utils/radixtree.nim
index b361d367..49072d65 100644
--- a/src/utils/radixtree.nim
+++ b/src/utils/radixtree.nim
@@ -2,6 +2,7 @@
 # however it *is* faster.
 
 import json
+import options
 import tables
 
 type
@@ -9,9 +10,7 @@ type
 
   RadixNode*[T] = ref object
     children*: seq[RadixPair[T]]
-    case leaf*: bool
-    of true: value*: T
-    of false: discard
+    value*: Option[T]
 
 func newRadixTree*[T](): RadixNode[T] =
   new(result)
@@ -61,10 +60,10 @@ func contains[T](node: RadixNode[T], k: string): bool =
 
 # O(1) add procedures for insert
 proc add[T](node: RadixNode[T], k: string, v: T) =
-  node.children.add((k, RadixNode[T](leaf: true, value: v)))
+  node.children.add((k, RadixNode[T](value: v.some)))
 
 proc add[T](node: RadixNode[T], k: string) =
-  node.children.add((k, RadixNode[T](leaf: false)))
+  node.children.add((k, RadixNode[T]()))
 
 proc add[T](node: RadixNode[T], k: string, v: RadixNode[T]) =
   node.children.add((k, v))
@@ -124,7 +123,7 @@ proc `[]=`*[T](tree: RadixNode[T], key: string, value: T) =
     p.children.del(k)
   elif key.len == t.len:
     # new matches a node, so replace
-    p.children[k].v = RadixNode[T](leaf: true, value: value, children: n.children)
+    p.children[k].v = RadixNode[T](value: value.some, children: n.children)
   elif key.len > t.len:
     # new is longer than the old, so add child to old
     n.add(key.substr(i), value)