You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

178 lines
5.1 KiB
Kotlin

package me.eater.threedom.dom
import me.eater.threedom.dom.event.DOMTreeUpdate
import me.eater.threedom.dom.event.NodeClassListUpdate
import me.eater.threedom.dom.event.NodeIDUpdate
import me.eater.threedom.event.Event
import me.eater.threedom.event.EventListener
import me.eater.threedom.event.trigger
import me.eater.threedom.utils.ObservableSet
import org.joml.Matrix4d
abstract class Node<T : INode<T>>(document: IDocument?) : INode<T>, EventListener {
override var document: IDocument? = document
protected set
override val nodeId = INode.getNextNodeId()
private val nodes: MutableSet<INode<*>> = mutableSetOf()
override var parentNode: INode<*>? = null
protected set
private var _id: String? = null
override var id: String?
get() = _id
set(value) {
val old = _id
_id = value
trigger(NodeIDUpdate(this, old, value))
}
override val classList: MutableSet<String> = ObservableSet {
when (it.action) {
ObservableSet.Action.Removed -> trigger(NodeClassListUpdate.Removed(it.elements, this))
ObservableSet.Action.Added -> trigger(NodeClassListUpdate.Added(it.elements, this))
}
}
private inline fun <reified T> trigger(event: T) {
if (parentNode !== null) {
document?.trigger(Event(event, this))
}
}
override var model: Matrix4d = Matrix4d()
var children: List<INode<*>> = nodes.toList()
override fun addNode(newNode: INode<*>) {
nodes.add(newNode)
if (newNode.parentNode != this) {
newNode.updateParent(this)
}
}
override fun removeNode(refNode: INode<*>) {
nodes.remove(refNode)
if (refNode.parentNode == this) {
refNode.updateParent(null)
}
}
override fun removeAll() {
nodes.forEach(::removeNode)
}
override fun replaceNode(newNode: INode<*>, refNode: INode<*>): Boolean {
if (nodes.remove(refNode)) {
nodes.add(newNode)
return true
}
return false
}
override fun sequence(): Sequence<INode<*>> = nodes.asSequence()
override fun iterator(): Iterator<INode<*>> = nodes.iterator()
fun recursiveSequence(): Sequence<INode<*>> = sequence {
val iterators = mutableListOf<Iterator<INode<*>>>()
var current = iterator()
while (true) {
for (node in current) {
yield(node)
iterators.add(node.iterator())
}
current = iterators.firstOrNull() ?: return@sequence
}
}
protected abstract fun cloneSelf(): T
override fun clone(deep: Boolean): T {
val cloned = cloneSelf()
if (!deep) {
return cloned
}
for (node in cloned) {
cloned.addNode(node.clone(true))
}
return cloned
}
override fun updateParent(refNode: INode<*>?): Boolean {
val parent = parentNode
if (parent == refNode) {
return true
}
if (refNode != null && !refNode.hasChild(this)) {
return false
}
parentNode = refNode
parent?.removeNode(this)
val event = when {
parent == null && refNode != null ->
DOMTreeUpdate.Insert(refNode, this).also(::trigger)
refNode == null && parent != null -> {
val data = DOMTreeUpdate.Remove(parent, this)
// Trigger on removed as well as on the parent
// Since you can't bubble from a detached node
var ev = Event(data, this)
document?.trigger(this, ev)
if (ev.bubble) {
document?.trigger(parent, Event(data, this))
}
// Trigger on removed as well as on the parent
// Since you can't bubble from a detached node
ev = Event(data, this)
document?.trigger<DOMTreeUpdate>(this, ev)
if (ev.bubble) {
document?.trigger<DOMTreeUpdate>(parent, Event(data, this))
}
return true
}
refNode != null && parent != null ->
DOMTreeUpdate.Move(parent, refNode, this).also(::trigger)
else -> return true
}
trigger(event)
return true
}
override fun hasChild(refNode: INode<*>) = nodes.contains(refNode)
override fun <T> on(eventName: String, block: (Event<T>) -> Unit) {
document?.addEventListener(eventName, this, block)
}
override fun detachFromDocument() {
val doc = this.document ?: return
this.document = null
doc.removeNode(this)
}
fun finalize() {
document?.removeNode(this)
}
override fun getNodeById(id: String): INode<*>? = document?.getNodeById(id)?.takeIf { it.hasParent(this) }
override fun getNodesByClassName(className: String): Sequence<INode<*>> = if (nodes.isEmpty())
emptySequence()
else
document?.getNodesByClassName(className)?.filter { it.hasParent(this) } ?: emptySequence()
}