diff --git a/src/karax.nim b/src/karax.nim index ed956d8..0e58c4b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -123,31 +123,20 @@ proc replaceById(id: cstring; newTree: Node) = #newTree.id = id proc equalsShallow(a, b: VNode): bool = - if a.kind != b.kind: return false - if a.id != b.id: return false - if a.key != b.key: return false - if a.kind == VNodeKind.text: - if a.text != b.text: return false - elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: - if a.text != b.text: return false - if a.len != b.len: return false - for i in 0..= 0 and isDirty(newNode.key): @@ -164,7 +153,77 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return -proc updateElement(parent, current: Node, newNode, oldNode: VNode) = +proc printChildren(parent: Node): cstring = + discard + # if parent != nil and parent.hasChildNodes: + # var it = parent.firstChild + # result = "" + # while it != nil: + # if it.id == nil: + # result.add(" nil") + # else: + # result.add(" " & $it.id) + # it = it.nextSibling + +proc printChildren(parent: VNode): cstring = + discard + # if parent != nil: + # result = "" + # for i in 0..parent.len-1: + # if parent[i] == nil or parent[i].id == nil: + # result.add(" nil") + # else: + # result.add(" " & $(parent[i].id)) + +proc print(s: cstring, ident: int) = + discard + # var result = "" + # for i in 0..ident: + # result.add " " + # result.add(s) + # kout cstring(result) + +proc longestIncreasingSubsequence(a: seq[int]): seq[int] = + if len(a) == 0: + return @[] + + result.add 0 + var parent = newSeq[int](len(a)) + for i in 0.. 0: + parent[i] = result[left - 1] + result[left] = i + + var pos = len(result) + var v = result[pos - 1] + while pos > 0: + result[pos] = v + v = parent[v] + dec pos + +proc updateElement(parent, current: Node, newNode, oldNode: VNode, ident: int = 0) = + if newNode.key != -1: + kout cstring($newNode.key) + if oldNode.key != -1: + kout cstring($oldNode.key) + newNode.dom = oldNode.dom if not equalsShallow(newNode, oldNode): detach(oldNode) let n = vnodeToDom(newNode) @@ -188,36 +247,177 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = detach(oldNode[i]) current.removeChild(current.lastChild) else: - var commonPrefix = 0 - while commonPrefix < minLength and - equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): - inc commonPrefix + #kout cstring("start") + print("----------------", ident) + print("----------------", ident) + var before = printChildren(current) - var oldPos = oldLength - 1 - var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix and - equalsTree(newNode[newPos], oldNode[oldPos]): - dec oldPos - dec newPos + # maximal common prefix + var left = 0 + while left < minLength and equalsShallow(newNode[left], oldNode[left]): + updateElement(current, oldNode[left].dom, newNode[left], oldNode[left], ident + 1) + inc left - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + # maximal common suffix + var rightOld = oldLength - 1 + var rightNew = newLength - 1 + while rightOld >= left and rightNew >= left and equalsShallow(newNode[rightNew], oldNode[rightOld]): + updateElement(current, oldNode[rightOld].dom, newNode[rightNew], oldNode[rightOld], ident + 1) + dec rightOld + dec rightNew - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) + var leftOld = left + var leftNew = left + + + var flag = false + #if rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): + print("current", ident) + print(printChildren(current), ident) + print("oldNode", ident) + print(printChildren(oldNode), ident) + print("newNode", ident) + print(printChildren(newNode), ident) + flag = true + + # cross comparing + while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[leftOld], newNode[rightNew]): + print($oldNode[leftOld].id & " " & $newNode[rightNew].id, ident) + print("pos: " & $leftOld & " " & $rightNew, ident) + + var nextNode: Node = nil + if rightNew + 1 < newLength: + nextNode = newNode[rightNew + 1].dom + print("update", ident) + updateElement(current, oldNode[leftOld].dom, newNode[rightNew], oldNode[leftOld], ident + 1) + print("update", ident) + if nextNode == nil: + current.appendChild(oldNode[leftOld].dom) + print("append", ident) else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos + print("insertBefore", ident) + current.insertBefore(oldNode[leftOld].dom, nextNode) + print($oldNode[leftOld], ident) + inc leftOld + dec rightNew - for i in pos..oldPos: - detach(oldNode[i]) - current.removeChild(current.childNodes[pos]) + while rightOld >= leftOld and rightNew >= leftNew and equalsShallow(oldNode[rightOld], newNode[leftNew]): + var nextNode: Node = oldNode[leftOld].dom + updateElement(current, oldNode[rightOld].dom, newNode[leftNew], oldNode[rightOld], ident + 1) + current.insertBefore(oldNode[rightOld].dom, nextNode) + inc leftNew + dec rightOld + + if flag: + print("after", ident) + print(printChildren(current), ident) + + var isKeyed = true + for i in leftNew..rightNew: + if newNode[i].key == -1: + isKeyed = false + break + + for i in leftOld..rightOld: + if oldNode[i].key == -1: + isKeyed = false + if not isKeyed: + break + + if rightNew - leftNew + 1 + rightOld - leftOld + 1 == 0: + isKeyed = false + + + if isKeyed: + if rightNew > leftNew: + # remove redundant old nodes + for i in leftOld..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) + else: + # permute elements using LIS + var positionByKey = newJDict[VKey, int]() + var positions = newSeq[int]() + for i in leftOld..rightOld: + positionByKey[oldNode[i].key] = i + for i in leftNew..rightNew: + if positionByKey.contains(newNode[i].key): + positions.add positionByKey[newNode[i].key] + + #if len(positions) > 0: + # kout cstring("new segment len = " & $(rightNew - leftNew + 1)) + # kout cstring("old segment len = " & $(rightOld - leftOld + 1)) + # var t = "" + # for i in 0..= leftOld and rightNew >= leftNew: + updateElement(current, oldNode[leftOld].dom, newNode[leftNew], oldNode[leftOld], ident + 1) + inc leftNew + inc leftOld + + print("TEMP", ident) + print(printChildren(current), ident) + print($leftOld & " " & $rightOld & " " & $leftNew & " " & $rightNew, ident) + print("other part start", ident) + var isPushBack = (rightNew + 1 == newLength) + var nextNode: Node = nil + if not isPushBack: + print($(rightNew + 1), ident) + nextNode = newNode[rightNew + 1].dom + while leftNew <= rightNew: + var node = vnodeToDom(newNode[leftNew]) + if isPushBack: + current.appendChild(node) + else: + current.insertBefore(node, nextNode) + inc leftNew + print("other part finish", ident) + + for i in leftOld..rightOld: + current.removeChild(oldNode[i].dom) + detach(oldNode[i]) + + print("----------------", ident) + print("before", ident) + print(before, ident) + print("finish", ident) + print(printChildren(current), ident) + print("----------------", ident) + print("----------------", ident) when false: var drawTimeout: Timeout @@ -256,6 +456,8 @@ proc redraw*() = else: dodraw() +proc redrawForce*() = dodraw() + proc init(ev: Event) = reqFrame(dodraw) @@ -263,6 +465,12 @@ proc setRenderer*(renderer: proc (): VNode) = dorender = renderer window.onload = init +proc setRendererOnly*(renderer: proc (): VNode) = + dorender = renderer + +proc setOnloadOnly*() = + window.onload = init + proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## Implements the foundation of Karax's event management. ## Karax DSL transforms ``tag(onEvent = handler)`` to diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 5ba525a..5553d8f 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key"] + SpecialAttrs = ["id", "class", "value", "key", "track"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index 25f6c12..a80fc10 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -102,12 +102,14 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring + track*: int kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] events*: seq[(EventKind, EventHandler)] - hash*: Hash - validHash*: bool + when false: + hash*: Hash + validHash*: bool dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. @@ -146,7 +148,11 @@ proc eq*(a, b: VNode): bool = proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: - n.attrs = @[key, val] + when true: + let x = @[key, val] + shallowCopy n.attrs, x + else: + n.attrs = @[key, val] else: for i in countup(0, n.attrs.len-2, 2): if n.attrs[i] == key: @@ -161,6 +167,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] +proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) @@ -216,32 +223,33 @@ proc toString*(n: VNode; result: var string; indent: int) = for i in 1..indent: result.add ' ' result.add "\L" -proc calcHash*(n: VNode) = - if n.validHash: return - n.validHash = true - var h: Hash = ord n.kind - if n.id != nil: - h &= "id" - h &= n.id - if n.class != nil: - h &= "class" - h &= n.class - if n.key >= 0: - h &= "k" - h &= n.key - for k, v in attrs(n): - h &= " " - h &= k - h &= "=" - h &= v - if n.kind == VNodeKind.text or n.text != nil: - h &= "t" - h &= n.text - else: - for child in items(n): - calcHash(child) - h &= child.hash - n.hash = h +when false: + proc calcHash*(n: VNode) = + if n.validHash: return + n.validHash = true + var h: Hash = ord n.kind + if n.id != nil: + h &= "id" + h &= n.id + if n.class != nil: + h &= "class" + h &= n.class + if n.key >= 0: + h &= "k" + h &= n.key + for k, v in attrs(n): + h &= " " + h &= k + h &= "=" + h &= v + if n.kind == VNodeKind.text or n.text != nil: + h &= "t" + h &= n.text + else: + for child in items(n): + calcHash(child) + h &= child.hash + n.hash = h proc `$`*(n: VNode): cstring = var res = "" diff --git a/tests/diffDomTests.html b/tests/diffDomTests.html index 5ecf130..cb6ab28 100644 --- a/tests/diffDomTests.html +++ b/tests/diffDomTests.html @@ -3,7 +3,6 @@ Diff dom tests - diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index 7fdcf85..ee907c8 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -41,7 +41,7 @@ proc test1() = proc check2() = let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5]) if result: - results.add cstring"test1 - OK" + results.add cstring"test2 - OK" else: results.add cstring"test2 - FAIL" @@ -83,16 +83,16 @@ proc test4() = timeout = setTimeout(check4, 20) proc check5() = - let result = checkOrder(@[0, 1, 3, 5, 4, 5]) + let result = checkOrder(@[0, 1, 3, 5, 4, 8]) if result: results.add cstring"test5 - OK" else: results.add cstring"test5 - FAIL" -# result: 0 1 3 5 4 5 +# result: 0 1 3 5 4 8 proc test5() = results.add cstring"test5 started" - entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ] + entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("8") ] redraw() timeout = setTimeout(check5, 20) @@ -127,14 +127,14 @@ proc test7() = proc createEntry(id: int): VNode = result = buildHtml(): - button(id="" & $id): + button(id="" & $id, key = id): text $id proc createDom(): VNode = result = buildHtml(tdiv()): ul(id="ul"): for e in entries: - createEntry(parseInt(e)) + createEntry(jstrutils.parseInt(e)) for r in results: tdiv: text r diff --git a/uibench/index.html b/uibench/index.html new file mode 100644 index 0000000..5b2cb4c --- /dev/null +++ b/uibench/index.html @@ -0,0 +1,16 @@ + + + + + + UI Benchmark: React + + + +
+ + + + + + diff --git a/uibench/nim_uibench.nim b/uibench/nim_uibench.nim new file mode 100644 index 0000000..3d1f078 --- /dev/null +++ b/uibench/nim_uibench.nim @@ -0,0 +1,105 @@ +import dom, vdom, karax, karaxdsl, jdict, jstrutils, kajax + +type + HomeState {.importc.} = ref object + TableItemState {.importc.} = ref object + id: int + active: bool + props: seq[cstring] + TableState {.importc.} = ref object + items: seq[TableItemState] + AnimBoxState {.importc.} = ref object + id: int + time: int + AnimState {.importc.} = ref object + items: seq[AnimBoxState] + TreeNodeState {.importc.} = ref object + id: int + container: bool + children: seq[TreeNodeState] + TreeState {.importc.} = ref object + root: TreeNodeState + AppState {.importc.} = ref object + location: cstring + home: HomeState + table: TableState + anim: AnimState + tree: TreeState + +var appState: AppState + +proc createTableCell(id: cstring): VNode = + proc tableCellClick(ev: karax.Event; n: VNode) = + kout "Clicked" & id + result = buildHtml(td(class="TableCell", onclick=tableCellClick)): + text id + +proc createTableRow(item: TableItemState): VNode = + let className = if item.active: cstring"TableRow active" else: cstring"TableRow" + result = buildHtml(tr(class=className, `data-id` = &item.id)): + createTableCell("#" & &item.id) + for i in 0.." & myToJson(samples) & cstring"" + +proc init*(a: cstring, b: cstring) {.importc: "uibench.init", nodecl.} +proc run*(a: proc(state: AppState), b: proc(samples: RootRef)) {.importc: "uibench.run", nodecl.} + +init(cstring"Nim-karax", cstring"0.6.1") + +setRendererOnly update +run(a, b) \ No newline at end of file diff --git a/uibench/nim_uibench.nims b/uibench/nim_uibench.nims new file mode 100644 index 0000000..532b206 --- /dev/null +++ b/uibench/nim_uibench.nims @@ -0,0 +1 @@ +--path: "../src"