///|
pub fn mount(
container : @three.Object3D,
node : Node,
on_change? : () -> Unit = fn() { () },
) -> Root {
let root : Root = {
container,
children: [],
active: false,
updating: 0,
on_change,
claims: Map([]),
}
root.children = [create_mounted(root, @luna.fragment([node]))]
root.active = true
@reactivity.on_cleanup(fn() { root.unmount() })
root.changed()
root
}
///|
/// Child scopes must not register cleanup with, or subscribe, the parent's effect.
fn[T] in_scope(owner : @signals.Owner, f : () -> T) -> T {
let previous = @reactivity.set_current_cleanups(None)
let result = @reactivity.untracked(fn() {
@reactivity.run_with_owner(owner, f)
})
@reactivity.set_current_cleanups(previous) |> ignore
result
}
///|
fn node_key(node : Node) -> String? {
if node is @luna.Element(el) {
let mut key = None
for (_, attr) in el.attrs {
if attr is @luna.VStatic(Key(value)) {
key = Some(value)
}
}
key
} else {
None
}
}
///|
fn node_tag(node : Node) -> String {
match node {
@luna.Element(el) => el.tag
@luna.Fragment(_) => "#fragment"
@luna.Component(..) => "#component"
@luna.Show(..) => "#show"
@luna.For(..) => "#for"
@luna.Switch(_) => "#switch"
_ => "#unsupported"
}
}
///|
fn primitive_object(node : Node) -> @three.Object3D? {
if node is @luna.Element(el) {
let mut object = None
for (_, attr) in el.attrs {
if attr is @luna.VStatic(Object(value)) {
object = Some(value)
}
}
object
} else {
None
}
}
///|
fn reusable(mounted : Mounted, node : Node) -> Bool {
if physical_equal(mounted.source, node) {
return true
}
if mounted.tag != node_tag(node) {
return false
}
if mounted.tag == "primitive" {
match (primitive_object(mounted.source), primitive_object(node)) {
(Some(a), Some(b)) => a.same_reference(b)
_ => false
}
} else {
node is @luna.Element(_)
}
}
///|
fn reconcile(root : Root, parent : Mounted, nodes : Array[Node]) -> Unit {
let keys : Map[String, Bool] = Map([])
for node in nodes {
if node_key(node) is Some(key) {
if keys.contains(key) {
parent.children_error = Some("Duplicate sibling key: " + key)
return
}
keys.set(key, true)
}
}
parent.children_error = None
let old = parent.children
let keyed : Map[String, Int] = Map([])
for index, child in old {
if child.key is Some(key) {
keyed.set(key, index)
}
}
let used : Array[Bool] = Array::make(old.length(), false)
let matches : Array[Int?] = []
for index, node in nodes {
let candidate = match node_key(node) {
Some(key) => keyed.get(key)
None =>
if index < old.length() && old[index].key is None {
Some(index)
} else {
None
}
}
if candidate is Some(i) && !used[i] && reusable(old[i], node) {
used[i] = true
matches.push(Some(i))
} else {
matches.push(None)
}
}
// Release removed/replaced slots first, so a primitive may move to a new key.
for index, child in old {
if !used[index] {
child.dispose()
}
}
let next : Array[Mounted] = []
for index, source in nodes {
if matches[index] is Some(i) {
let child = old[i]
if !physical_equal(child.source, source) {
child.source = source
in_scope(child.owner, fn() { bind_element(root, child, source) })
}
next.push(child)
} else {
next.push(create_mounted(root, source))
}
}
parent.children = next
}
///|
fn create_mounted(root : Root, source : Node) -> Mounted {
let node : Mounted = {
root,
source,
tag: node_tag(source),
key: node_key(source),
owner: @signals.Owner::new(@reactivity.get_owner()),
stop_binding: fn() { () },
object: None,
children: [],
geometry: None,
material: None,
previous: Map([]),
defaults: Map([]),
error: None,
children_error: None,
}
in_scope(node.owner, fn() {
match source {
@luna.Element(_) => bind_element(root, node, source)
@luna.Fragment(children) => reconcile(root, node, children)
@luna.Component(render~) => reconcile(root, node, [render()])
@luna.Show(condition~, child~) => {
let (_, stop) = @reactivity.create_root_with_dispose(fn() {
@reactivity.render_effect(fn() {
let visible = condition()
root.updating += 1
in_scope(node.owner, fn() {
if visible {
if node.children.is_empty() {
reconcile(root, node, [child()])
}
} else {
reconcile(root, node, [])
}
})
root.updating -= 1
root.changed()
})
|> ignore
})
node.stop_binding = stop
}
@luna.For(render~) => bind_list(root, node, render)
@luna.Switch(value) => {
// Track conditions, but render the selected component outside tracking.
let selected = Ref(-2)
let (_, stop) = @reactivity.create_root_with_dispose(fn() {
@reactivity.render_effect(fn() {
let mut index = -1
for i, case_ in value.cases {
if (case_.when)() {
index = i
break
}
}
if selected.val != index {
selected.val = index
root.updating += 1
in_scope(node.owner, fn() {
let children = if index >= 0 {
[(value.cases[index].render)()]
} else if value.fallback is Some(fallback) {
[fallback()]
} else {
[]
}
reconcile(root, node, children)
})
root.updating -= 1
root.changed()
}
})
|> ignore
})
node.stop_binding = stop
}
_ =>
node.error = Some(
"Unsupported Luna node: use Element, Fragment, Component, Show, For or Switch",
)
}
})
node
}
///|
fn bind_list(root : Root, node : Mounted, render : () -> Array[Node]) -> Unit {
let (_, stop) = @reactivity.create_root_with_dispose(fn() {
@reactivity.render_effect(fn() {
let children = render()
root.updating += 1
in_scope(node.owner, fn() { reconcile(root, node, children) })
root.updating -= 1
root.changed()
})
|> ignore
})
node.stop_binding = stop
}
///|
fn bind_element(root : Root, node : Mounted, source : Node) -> Unit {
guard source is @luna.Element(el) else { return }
let existing = node.object is Some(_)
(node.stop_binding)()
let (_, stop) = @reactivity.create_root_with_dispose(fn() {
@reactivity.render_effect(fn() {
let props : Map[String, Property] = Map([])
let mut error = None
for (_, attr) in el.attrs {
match attr {
@luna.VStatic(value) => props.set(value.slot(), value)
@luna.VDynamic(get) => {
let value = get()
if value is Key(_) || value is Object(_) {
error = Some(
"Key and Object must be static; change them through a keyed list",
)
} else {
props.set(value.slot(), value)
}
}
_ =>
error = Some(
"Luna event handlers and actions are not supported by the 3D renderer",
)
}
}
let had_object = node.object is Some(_)
match error {
Some(_) => node.error = error
None => apply_properties(node, props)
}
if !had_object && node.object is Some(_) {
root.updating += 1
in_scope(node.owner, fn() { reconcile(root, node, el.children) })
root.updating -= 1
root.changed()
} else {
root.notify()
}
})
|> ignore
})
node.stop_binding = stop
// These children belong to the element, independently of its property effects.
if existing && node.object is Some(_) {
reconcile(root, node, el.children)
}
}