about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2021-11-14 12:57:50 +0100
committerbptato <nincsnevem662@gmail.com>2021-11-14 13:00:34 +0100
commit60b583516262be8754c6563ed9253b8964bef5d8 (patch)
treed77fabc0eb4b8ce4432b45a1845999c9a0698e40 /src
parenta6099512e42cc95cf6276ab784afd70febdd43b7 (diff)
downloadchawan-60b583516262be8754c6563ed9253b8964bef5d8.tar.gz
Initial implementation of CSS :hover
Diffstat (limited to 'src')
-rw-r--r--src/html/dom.nim10
-rw-r--r--src/io/buffer.nim229
-rw-r--r--src/io/cell.nim5
-rw-r--r--src/layout/box.nim3
-rw-r--r--src/layout/engine.nim47
-rw-r--r--src/main.nim4
6 files changed, 183 insertions, 115 deletions
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 96e58ada..a807f824 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -31,13 +31,8 @@ type
     parentElement*: Element
     ownerDocument*: Document
 
-    x*: int
-    y*: int
-    ex*: int
-    ey*: int
-    width*: int
-    height*: int
     hidden*: bool
+    hover*: bool
 
   Attr* = ref AttrObj
   AttrObj = object of NodeObj
@@ -293,9 +288,10 @@ func pseudoSelectorMatches(elem: Element, sel: Selector): bool =
   case sel.pseudo
   of "first-child": return elem.parentNode.firstElementChild == elem
   of "last-child": return elem.parentNode.lastElementChild == elem
+  of "hover": return elem.hover
   else: return false
 
-func pseudoElemSelectorMatches(elem: Element, sel: Selector, pseudo: PseudoElem = PSEUDO_NONE): SelectResult =
+func pseudoElemSelectorMatches(elem: Element, sel: Selector): SelectResult =
   case sel.elem
   of "after": return selectres(true, PSEUDO_AFTER)
   of "before": return selectres(true, PSEUDO_AFTER)
diff --git a/src/io/buffer.nim b/src/io/buffer.nim
index d1a55222..cfe45b5e 100644
--- a/src/io/buffer.nim
+++ b/src/io/buffer.nim
@@ -1,4 +1,3 @@
-import options
 import terminal
 import uri
 import strutils
@@ -14,6 +13,7 @@ import config/config
 import io/term
 import io/lineedit
 import io/cell
+import layout/engine
 
 type
   Buffer* = ref BufferObj
@@ -35,10 +35,12 @@ type
     document*: Document
     displaycontrols*: bool
     redraw*: bool
+    reshape*: bool
     location*: Uri
     source*: string
     showsource*: bool
     rootbox*: CSSBox
+    prevnodes*: seq[Node]
 
 func newBuffer*(attrs: TermAttributes): Buffer =
   new(result)
@@ -150,17 +152,25 @@ func cellOrigin(buffer: Buffer, x: int, y: int): int =
 func currentCellOrigin(buffer: Buffer): int =
   return buffer.cellOrigin(buffer.acursorx, buffer.acursory)
 
-func currentRune(buffer: Buffer): Rune =
+#TODO counter-intuitive naming?
+func currentCell(buffer: Buffer): FixedCell =
   let row = (buffer.cursory - buffer.fromy) * buffer.width
-  return buffer.display[row + buffer.currentCellOrigin()].runes[0]
-
-func cellWidthOverlap*(buffer: Buffer, x: int, y: int): int =
-  let ox = buffer.cellOrigin(x, y)
-  let row = y * buffer.width
-  return buffer.display[row + ox].runes.width()
+  return buffer.display[row + buffer.currentCellOrigin()]
 
-func currentCellWidth*(buffer: Buffer): int =
-  return buffer.cellWidthOverlap(buffer.cursorx - buffer.fromx, buffer.cursory - buffer.fromy)
+func currentRune(buffer: Buffer): Rune =
+  let cell = buffer.currentCell()
+  if cell.runes.len == 0:
+    return Rune(' ')
+  return cell.runes[0]
+
+func getCursorLink(buffer: Buffer): Element =
+  let nodes = buffer.currentCell().nodes
+  for node in nodes:
+    if node.nodeType == ELEMENT_NODE:
+      let elem = Element(node)
+      if elem.tagType == TAG_A:
+        return elem
+  return nil
 
 func currentLineWidth*(buffer: Buffer): int =
   if buffer.cursory > buffer.lines.len:
@@ -175,17 +185,6 @@ func atPercentOf*(buffer: Buffer): int =
   if buffer.lines.len == 0: return 100
   return (100 * (buffer.cursory + 1)) div buffer.numLines
 
-func cursorOnNode*(buffer: Buffer, node: Node): bool =
-  if node.y == node.ey and node.y == buffer.cursory:
-    return buffer.cursorx >= node.x and buffer.cursorx < node.ex
-  else:
-    return (buffer.cursory == node.y and buffer.cursorx >= node.x) or
-           (buffer.cursory > node.y and buffer.cursory < node.ey) or
-           (buffer.cursory == node.ey and buffer.cursorx < node.ex)
-
-func findSelectedElement*(buffer: Buffer): Option[HtmlElement] =
-  discard #TODO
-
 func canScroll*(buffer: Buffer): bool =
   return buffer.numLines >= buffer.height
 
@@ -204,6 +203,41 @@ proc clearBuffer*(buffer: Buffer) =
   buffer.fromy = 0
   buffer.hovertext = ""
 
+proc clearDisplay*(buffer: Buffer) =
+  var i = 0
+  while i < buffer.display.len:
+    buffer.display[i].runes.setLen(0)
+    inc i
+
+proc refreshDisplay*(buffer: Buffer) =
+  var y = 0
+  buffer.prevdisplay = buffer.display
+  buffer.clearDisplay()
+
+  for line in buffer.lines[buffer.fromy..
+                           buffer.lastVisibleLine - 1]:
+    var w = 0
+    var i = 0
+    while w < buffer.fromx and i < line.len:
+      w += line[i].rune.width()
+      inc i
+
+    let dls = y * buffer.width
+    var j = 0
+    var n = 0
+    while w < buffer.fromx + buffer.width and i < line.len:
+      w += line[i].rune.width()
+      if line[i].rune.width() == 0 and j != 0:
+        inc n
+      buffer.display[dls + j - n].runes.add(line[i].rune)
+      buffer.display[dls + j - n].formatting = line[i].formatting
+      buffer.display[dls + j - n].nodes.add(line[i].nodes)
+      j += line[i].rune.width()
+      inc i
+
+    inc y
+
+
 proc restoreCursorX(buffer: Buffer) =
   buffer.cursorx = max(min(buffer.currentLineWidth() - 1, buffer.xend), 0)
 
@@ -250,7 +284,7 @@ proc cursorUp*(buffer: Buffer) =
       buffer.redraw = true
 
 proc cursorRight*(buffer: Buffer) =
-  let cellwidth = buffer.currentCellWidth()
+  let cellwidth = max(buffer.currentCell().width(), 1)
   let cellorigin = buffer.fromx + buffer.currentCellOrigin()
   let lw = buffer.currentLineWidth()
   if buffer.cursorx < lw - 1:
@@ -291,10 +325,24 @@ proc cursorLineEnd*(buffer: Buffer) =
   buffer.fromx = max(buffer.cursorx - buffer.width + 1, 0)
   buffer.redraw = buffer.fromx > 0
 
+#TODO this is sloooooow
+proc cursorRightOverflow(buffer: Buffer) =
+  buffer.cursorRight()
+  if buffer.cursorx >= buffer.currentLineWidth() - 1 and buffer.cursory < buffer.numLines - 1:
+    buffer.cursorDown()
+    buffer.cursorLineBegin()
+  buffer.refreshDisplay()
+
+proc cursorLeftOverflow(buffer: Buffer) =
+  buffer.cursorLeft()
+  if buffer.cursorx <= 0 and buffer.cursory > 0:
+    buffer.cursorUp()
+    buffer.cursorLineEnd()
+  buffer.refreshDisplay()
+
 proc cursorNextWord*(buffer: Buffer) =
   let llen = buffer.currentLineWidth() - 1
   if llen >= 0:
-
     while not buffer.currentRune().breaksWord():
       if buffer.cursorx >= llen:
         break
@@ -329,7 +377,32 @@ proc cursorPrevWord*(buffer: Buffer) =
 
 proc cursorNextLink*(buffer: Buffer) =
   #TODO
-  return
+  #let ocx = buffer.cursorx
+  #let ocy = buffer.cursory
+  #let ofx = buffer.fromx
+  #let ofy = buffer.fromy
+  #let elem = buffer.getCursorLink()
+  #if elem != nil:
+  #  while buffer.getCursorLink() == elem:
+  #    buffer.cursorRightOverflow()
+  #    if buffer.cursorx >= buffer.currentLineWidth() - 1 and
+  #        buffer.cursory >= buffer.numLines - 1:
+  #      buffer.cursorx = ocx
+  #      buffer.cursory = ocy
+  #      buffer.fromx = ofx
+  #      buffer.fromy = ofy
+  #      break
+
+  #while buffer.getCursorLink() == nil:
+  #  buffer.cursorRightOverflow()
+  #  if buffer.cursorx >= buffer.currentLineWidth() - 1 and
+  #      buffer.cursory >= buffer.numLines - 1:
+  #    buffer.cursorx = ocx
+  #    buffer.cursory = ocy
+  #    buffer.fromx = ofx
+  #    buffer.fromy = ofy
+  #    break
+  discard
 
 proc cursorPrevLink*(buffer: Buffer) =
   #TODO
@@ -494,6 +567,7 @@ func cellFromLine(line: CSSRowBox, i: int): FlexibleCell =
     result.formatting.overline = true
   if line.textdecoration == TEXT_DECORATION_LINE_THROUGH:
     result.formatting.strike = true
+  result.nodes = line.nodes
 
 proc setRowBox(buffer: Buffer, line: CSSRowBox) =
   let x = line.x
@@ -530,10 +604,6 @@ proc setRowBox(buffer: Buffer, line: CSSRowBox) =
   if i < oline.len:
     buffer.lines[y].add(oline[i..high(oline)])
 
-proc reshape*(buffer: Buffer) =
-  buffer.display = newFixedGrid(buffer.width, buffer.height)
-  buffer.statusmsg = newFixedGrid(buffer.width)
-
 proc updateCursor(buffer: Buffer) =
   if buffer.fromy > buffer.lastVisibleLine - 1:
     buffer.fromy = 0
@@ -545,38 +615,29 @@ proc updateCursor(buffer: Buffer) =
   if buffer.lines.len == 0:
     buffer.cursory = 0
 
-proc clearDisplay*(buffer: Buffer) =
-  var i = 0
-  while i < buffer.display.len:
-    buffer.display[i].runes.setLen(0)
-    inc i
-
-proc refreshDisplay*(buffer: Buffer) =
-  var y = 0
-  buffer.prevdisplay = buffer.display
-  buffer.clearDisplay()
-
-  for line in buffer.lines[buffer.fromy..
-                           buffer.lastVisibleLine - 1]:
-    var w = 0
-    var i = 0
-    while w < buffer.fromx and i < line.len:
-      w += line[i].rune.width()
-      inc i
-
-    let dls = y * buffer.width
-    var j = 0
-    var n = 0
-    while w < buffer.fromx + buffer.width and i < line.len:
-      w += line[i].rune.width()
-      if line[i].rune.width() == 0 and j != 0:
-        inc n
-      buffer.display[dls + j - n].runes.add(line[i].rune)
-      buffer.display[dls + j - n].formatting = line[i].formatting
-      j += line[i].rune.width()
-      inc i
-
-    inc y
+#TODO this works, but needs rethinking:
+#* reshape is called every time the cursor moves onto or off a line box, which
+#  practically means we're re-interpreting all style-sheets AND re-applying all
+#  rules way too often
+#* reshape also calls redraw so the entire window gets re-painted too which
+#  looks pretty bad
+#* and finally it re-arranges all CSS boxes too, which is a rather
+#  resource-intensive operation
+#overall the second point is the easiest to solve, then the first and finally
+#the last (there's only so much you can do in a flow layout, especially with
+#the current layout engine)
+proc updateHover(buffer: Buffer) =
+  let nodes = buffer.currentCell().nodes
+  if nodes != buffer.prevnodes:
+    for node in nodes:
+      if not node.hover:
+        node.hover = true
+        buffer.reshape = true
+    for node in buffer.prevnodes:
+      if node.hover and not (node in nodes):
+        node.hover = false
+        buffer.reshape = true
+  buffer.prevnodes = nodes
 
 proc renderPlainText*(buffer: Buffer, text: string) =
   buffer.clearText()
@@ -610,7 +671,8 @@ proc renderPlainText*(buffer: Buffer, text: string) =
 
 proc renderDocument*(buffer: Buffer) =
   buffer.clearText()
-  #TODO
+  buffer.document.applyStylesheets()
+  buffer.rootbox = buffer.document.alignBoxes(buffer.width, buffer.height)
   if buffer.rootbox == nil:
     return
   var stack: seq[CSSBox]
@@ -633,6 +695,15 @@ proc renderDocument*(buffer: Buffer) =
       dec i
   buffer.updateCursor()
 
+proc reshapeBuffer*(buffer: Buffer) =
+  buffer.display = newFixedGrid(buffer.width, buffer.height)
+  #TODO
+  #buffer.statusmsg = newFixedGrid(buffer.width)
+  if buffer.showsource:
+    buffer.renderPlainText(buffer.source)
+  else:
+    buffer.renderDocument()
+
 proc cursorBufferPos(buffer: Buffer) =
   let x = max(buffer.cursorx - buffer.fromx, 0)
   let y = buffer.cursory - buffer.fromy
@@ -677,8 +748,8 @@ proc displayBuffer(buffer: Buffer) =
 
 proc displayStatusMessage(buffer: Buffer) =
   termGoto(0, buffer.height)
-  eraseLine()
   print(buffer.generateStatusMessage())
+  print(EL())
 
 proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
   var s = ""
@@ -697,8 +768,6 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
     let c = getch()
     s &= c
     let action = getNormalAction(s)
-    var redraw = false
-    var reshape = false
     var nostatus = false
     case action
     of ACTION_QUIT:
@@ -743,38 +812,35 @@ proc inputLoop(attrs: TermAttributes, buffer: Buffer): bool =
         buffer.setLocation(parseUri(url))
         return true
     of ACTION_LINE_INFO:
-      buffer.setStatusMessage("line " & $(buffer.cursory + 1) & "/" & $buffer.numLines & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineWidth() & " cell width: " & $buffer.currentCellWidth())
+      buffer.setStatusMessage("line " & $(buffer.cursory + 1) & "/" & $buffer.numLines & " col " & $(buffer.cursorx + 1) & "/" & $buffer.currentLineWidth() & " cell width: " & $buffer.currentCell().width())
       nostatus = true
     of ACTION_FEED_NEXT:
       feedNext = true
     of ACTION_RELOAD: return true
     of ACTION_RESHAPE:
-      reshape = true
-      redraw = true
-    of ACTION_REDRAW: redraw = true
+      buffer.reshape = true
+      buffer.redraw = true
+    of ACTION_REDRAW: buffer.redraw = true
     of ACTION_TOGGLE_SOURCE:
       buffer.showsource = not buffer.showsource
-      if buffer.showsource:
-        buffer.renderPlainText(buffer.source)
-      else:
-        buffer.renderDocument()
-      redraw = true
+      buffer.reshape = true
+      buffer.redraw = true
     else: discard
     stdout.hideCursor()
+    buffer.updateHover()
 
     if buffer.refreshTermAttrs():
-      redraw = true
-      reshape = true
+      buffer.redraw = true
+      buffer.reshape = true
 
+    if buffer.reshape:
+      buffer.reshapeBuffer()
+      buffer.reshape = false
+      buffer.redraw = true #?
     if buffer.redraw:
-      redraw = true
-
-    if reshape:
-      buffer.reshape()
-      buffer.displayBuffer()
-    if redraw:
       buffer.refreshDisplay()
       buffer.displayBuffer()
+      buffer.redraw = false
 
     if not nostatus:
       buffer.statusMsgForBuffer()
@@ -787,4 +853,3 @@ proc displayPage*(attrs: TermAttributes, buffer: Buffer): bool =
   buffer.displayBuffer()
   buffer.statusMsgForBuffer()
   return inputLoop(attrs, buffer)
-
diff --git a/src/io/cell.nim b/src/io/cell.nim
index 67b7361c..d757c2de 100644
--- a/src/io/cell.nim
+++ b/src/io/cell.nim
@@ -5,6 +5,7 @@ import sugar
 
 import types/color
 import utils/twtstr
+import html/dom
 
 type
   Formatting* = object
@@ -18,6 +19,7 @@ type
 
   Cell* = object of RootObj
     formatting*: Formatting
+    nodes*: seq[Node]
 
   FlexibleCell* = object of Cell
     rune*: Rune
@@ -38,6 +40,9 @@ func width*(line: FlexibleLine): int =
   for c in line:
     result += c.rune.width()
 
+func width*(cell: FixedCell): int =
+  return cell.runes.width()
+
 func newFormatting*(): Formatting =
   return Formatting(fgcolor: defaultColor, bgcolor: defaultColor)
 
diff --git a/src/layout/box.nim b/src/layout/box.nim
index 60da87f7..271fbd10 100644
--- a/src/layout/box.nim
+++ b/src/layout/box.nim
@@ -2,6 +2,7 @@ import unicode
 
 import types/enums
 import css/style
+import html/dom
 
 type
   CSSRect* = object
@@ -37,6 +38,7 @@ type
     context*: FormatContextType
     marginx*: int
     marginy*: int
+    nodes*: seq[Node]
 
   CSSRowBox* = object
     x*: int
@@ -48,6 +50,7 @@ type
     fontweight*: int
     textdecoration*: CSSTextDecoration
     runes*: seq[Rune]
+    nodes*: seq[Node]
 
   CSSInlineBox* = ref CSSInlineBoxObj
   CSSInlineBoxObj = object of CSSBox
diff --git a/src/layout/engine.nim b/src/layout/engine.nim
index 55c8be00..52069b6f 100644
--- a/src/layout/engine.nim
+++ b/src/layout/engine.nim
@@ -4,7 +4,6 @@ import layout/box
 import types/enums
 import html/dom
 import css/style
-import io/buffer
 import utils/twtstr
 
 func newContext*(box: CSSBox): Context =
@@ -48,11 +47,12 @@ func newInlineBox*(parent: CSSBox, vals: CSSComputedValues): CSSInlineBox =
     result.context = newContext(parent)
 
 #TODO there should be actual inline contexts to store these stuff
-proc setup(rowbox: var CSSRowBox, cssvalues: CSSComputedValues) =
+proc setup(rowbox: var CSSRowBox, cssvalues: CSSComputedValues, nodes: seq[Node]) =
   rowbox.color = cssvalues[PROPERTY_COLOR].color
   rowbox.fontstyle = cssvalues[PROPERTY_FONT_STYLE].fontstyle
   rowbox.fontweight = cssvalues[PROPERTY_FONT_WEIGHT].integer
   rowbox.textdecoration = cssvalues[PROPERTY_TEXT_DECORATION].textdecoration
+  rowbox.nodes = nodes
 
 proc inlineWrap(ibox: var CSSInlineBox, rowi: var int, fromx: var int, rowbox: var CSSRowBox) =
   ibox.content.add(rowbox)
@@ -61,7 +61,7 @@ proc inlineWrap(ibox: var CSSInlineBox, rowi: var int, fromx: var int, rowbox: v
   ibox.context.whitespace = true
   ibox.context.conty = true
   rowbox = CSSRowBox(x: ibox.x, y: ibox.y + rowi)
-  rowbox.setup(ibox.cssvalues)
+  rowbox.setup(ibox.cssvalues, ibox.bcontext.nodes)
 
 #TODO statify
 proc processInlineBox(parent: CSSBox, str: string): CSSBox =
@@ -80,7 +80,7 @@ proc processInlineBox(parent: CSSBox, str: string): CSSBox =
   var rowi = 0
   var fromx = ibox.context.fromx
   var rowbox = CSSRowBox(x: fromx, y: ibox.context.fromy)
-  rowbox.setup(ibox.cssvalues)
+  rowbox.setup(ibox.cssvalues, ibox.bcontext.nodes)
   var r: Rune
   while i < str.len:
     fastRuneAt(str, i, r)
@@ -158,22 +158,23 @@ proc add(parent: var CSSBox, box: CSSBox) =
   parent.context.fromy = box.context.fromy
   parent.children.add(box)
 
-proc processPseudoBox(parent: CSSBox, cssvalues: CSSComputedValues): CSSBox =
-  case cssvalues[PROPERTY_DISPLAY].display
-  of DISPLAY_BLOCK:
-    result = newBlockBox(parent, cssvalues)
-    result.add(processInlineBox(parent, $cssvalues[PROPERTY_CONTENT].content)) 
-  of DISPLAY_INLINE:
-    result = processInlineBox(parent, $cssvalues[PROPERTY_CONTENT].content)
-  of DISPLAY_NONE:
-    return nil
-  else:
-    return nil
+#proc processPseudoBox(parent: CSSBox, cssvalues: CSSComputedValues): CSSBox =
+#  case cssvalues[PROPERTY_DISPLAY].display
+#  of DISPLAY_BLOCK:
+#    result = newBlockBox(parent, cssvalues)
+#    result.add(processInlineBox(parent, $cssvalues[PROPERTY_CONTENT].content)) 
+#  of DISPLAY_INLINE:
+#    result = processInlineBox(parent, $cssvalues[PROPERTY_CONTENT].content)
+#  of DISPLAY_NONE:
+#    return nil
+#  else:
+#    return nil
 
 proc processNode(parent: CSSBox, node: Node): CSSBox =
   case node.nodeType
   of ELEMENT_NODE:
     let elem = Element(node)
+    parent.bcontext.nodes.add(node)
 
     result = processElemBox(parent, elem)
     if result == nil:
@@ -192,14 +193,16 @@ proc processNode(parent: CSSBox, node: Node): CSSBox =
     #  let abox = processPseudoBox(parent, elem.cssvalues_after.get)
     #  if abox != nil:
     #    result.add(abox)
+    discard parent.bcontext.nodes.pop()
   of TEXT_NODE:
     let text = Text(node)
-    return processInlineBox(parent, text.data)
+    result = processInlineBox(parent, text.data)
   else: discard
 
-proc alignBoxes*(buffer: Buffer) =
-  buffer.rootbox = CSSBlockBox(x: 0, y: 0, width: buffer.width, height: 0)
-  buffer.rootbox.context = newContext(buffer.rootbox)
-  buffer.rootbox.bcontext = new(BlockContext)
-  for child in buffer.document.root.childNodes:
-    buffer.rootbox.add(processNode(buffer.rootbox, child))
+proc alignBoxes*(document: Document, width: int, height: int): CSSBox =
+  var rootbox = CSSBlockBox(x: 0, y: 0, width: width, height: 0)
+  rootbox.context = newContext(rootbox)
+  rootbox.bcontext = new(BlockContext)
+  for child in document.root.childNodes:
+    CSSBox(rootbox).add(processNode(rootbox, child))
+  return rootbox
diff --git a/src/main.nim b/src/main.nim
index 15cb5578..873364b0 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -5,8 +5,6 @@ import streams
 import terminal
 
 import html/parser
-import html/dom
-import layout/engine
 import io/buffer
 import io/term
 import config/config
@@ -61,8 +59,6 @@ proc main*() =
 
   buffer.setLocation(lastUri)
   buffer.document = parseHtml(newStringStream(buffer.source))
-  buffer.document.applyStylesheets()
-  buffer.alignBoxes()
   buffer.renderDocument()
   while displayPage(attrs, buffer):
     buffer.setStatusMessage("Loading...")