// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
fn[C] validate_node_alive(
tree : ChicleTree[C],
node : NodeId,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(n) => if !n.alive { raise InvalidNodeId(node) }
None => raise InvalidNodeId(node)
}
}
///|
fn array_contains_node(children : Array[NodeId], child : NodeId) -> Bool {
children.search_by(fn(x) { x == child }) is Some(_)
}
///|
fn array_contains_node_before(children : Array[NodeId], child : NodeId) -> Bool {
let mut seen = false
for item in children {
if item == child {
if seen {
return true
}
seen = true
}
}
false
}
///|
fn[C] reject_child_cycle(
tree : ChicleTree[C],
parent : NodeId,
child : NodeId,
) -> Unit raise ChicleError {
if parent == child {
raise CycleDetected(parent~, child~)
}
let mut cur = Some(parent)
while true {
match cur {
None => break
Some(id) =>
if id == child {
raise CycleDetected(parent~, child~)
} else {
cur = tree.parents[id]
}
}
}
}
///|
fn[C] detach_from_parent(
tree : ChicleTree[C],
child : NodeId,
new_parent : NodeId,
) -> Unit {
match tree.parents[child] {
Some(old_parent) =>
if old_parent != new_parent {
let old_children = tree.children[old_parent]
match old_children.search_by(fn(x) { x == child }) {
Some(idx) => ignore(old_children.remove(idx))
None => ()
}
tree.nodes[old_parent].cache.clear()
}
None => ()
}
}
///|
fn[C] attach_children_unchecked(
tree : ChicleTree[C],
parent : NodeId,
children : Array[NodeId],
) -> Unit {
let parent_children = tree.children[parent]
for child in children {
detach_from_parent(tree, child, parent)
tree.parents[child] = Some(parent)
parent_children.push(child)
}
}
///|
pub fn[C] ChicleTree::add_child(
tree : ChicleTree[C],
parent : NodeId,
child : NodeId,
) -> Unit raise ChicleError {
validate_node_alive(tree, parent)
validate_node_alive(tree, child)
reject_child_cycle(tree, parent, child)
let parent_children = tree.children[parent]
if array_contains_node(parent_children, child) {
return
}
detach_from_parent(tree, child, parent)
tree.parents[child] = Some(parent)
parent_children.push(child)
tree.mark_dirty(parent)
}
///|
pub fn[C] ChicleTree::set_children(
tree : ChicleTree[C],
parent : NodeId,
children : Array[NodeId],
) -> Unit raise ChicleError {
validate_node_alive(tree, parent)
for child_id in children {
validate_node_alive(tree, child_id)
reject_child_cycle(tree, parent, child_id)
if array_contains_node_before(children, child_id) {
raise DuplicateChild(parent~, child=child_id)
}
}
let old_children = tree.children[parent]
for child_id in old_children {
tree.parents[child_id] = None
}
old_children.clear()
attach_children_unchecked(tree, parent, children)
tree.mark_dirty(parent)
}
///|
pub fn[C] ChicleTree::remove_child(
tree : ChicleTree[C],
parent : NodeId,
child : NodeId,
) -> NodeId raise ChicleError {
match tree.nodes.get(parent) {
Some(_) => ()
None => raise InvalidNodeId(parent)
}
match tree.nodes.get(child) {
Some(_) => ()
None => raise InvalidNodeId(child)
}
if !tree.nodes[parent].alive {
raise InvalidNodeId(parent)
}
if !tree.nodes[child].alive {
raise InvalidNodeId(child)
}
let siblings = tree.children[parent]
match siblings.search_by(fn(x) { x == child }) {
Some(idx) => {
ignore(siblings.remove(idx))
tree.parents[child] = None
tree.mark_dirty(parent)
child
}
None => raise ChildNotFound(parent~, child~)
}
}
///|
pub fn[C] ChicleTree::mark_dirty(
tree : ChicleTree[C],
node : NodeId,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if !tree.nodes[node].alive {
raise InvalidNodeId(node)
}
let mut cur : NodeId? = Some(node)
while true {
match cur {
None => break
Some(id) => {
tree.nodes[id].dirty = true
tree.nodes[id].cache.clear()
cur = tree.parents[id]
}
}
}
}
///|
pub fn[C] ChicleTree::dirty(
tree : ChicleTree[C],
node : NodeId,
) -> Bool raise ChicleError {
match tree.nodes.get(node) {
Some(_) =>
if tree.nodes[node].alive {
tree.nodes[node].dirty
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] ChicleTree::remove(
tree : ChicleTree[C],
node : NodeId,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if !tree.nodes[node].alive {
raise InvalidNodeId(node)
}
// Detach from parent.
let old_parent = tree.parents[node]
match old_parent {
Some(parent_id) => {
let siblings = tree.children[parent_id]
match siblings.search_by(fn(x) { x == node }) {
Some(idx) => ignore(siblings.remove(idx))
None => ()
}
tree.parents[node] = None
}
None => ()
}
// Orphan children.
let kids = tree.children[node]
for child_id in kids {
tree.parents[child_id] = None
}
kids.clear()
tree.nodes[node].alive = false
match old_parent {
Some(p) => tree.mark_dirty(p)
None => ()
}
}
///|
pub fn[C] ChicleTree::remove_subtree(
tree : ChicleTree[C],
node : NodeId,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) => ()
None => raise InvalidNodeId(node)
}
if !tree.nodes[node].alive {
raise InvalidNodeId(node)
}
// Detach root of subtree from its parent.
let old_parent = tree.parents[node]
match old_parent {
Some(parent_id) => {
let siblings = tree.children[parent_id]
match siblings.search_by(fn(x) { x == node }) {
Some(idx) => ignore(siblings.remove(idx))
None => ()
}
tree.parents[node] = None
}
None => ()
}
let stack : Array[NodeId] = [node]
while true {
match stack.pop() {
None => break
Some(cur) => {
if !tree.nodes[cur].alive {
continue
}
let kids = tree.children[cur]
for child_id in kids {
stack.push(child_id)
}
kids.clear()
tree.parents[cur] = None
tree.node_context_data[cur] = None
tree.nodes[cur].cache.clear()
tree.nodes[cur].unrounded_layout = Layout::zero()
tree.nodes[cur].alive = false
}
}
}
match old_parent {
Some(p) => tree.mark_dirty(p)
None => ()
}
}
///|
pub fn[C] ChicleTree::set_node_context(
tree : ChicleTree[C],
node : NodeId,
context : C,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) =>
if tree.nodes[node].alive {
tree.node_context_data[node] = Some(context)
tree.nodes[node].cache.clear()
tree.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] ChicleTree::clear_node_context(
tree : ChicleTree[C],
node : NodeId,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) =>
if tree.nodes[node].alive {
tree.node_context_data[node] = None
tree.nodes[node].cache.clear()
tree.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}
///|
pub fn[C] ChicleTree::set_style(
tree : ChicleTree[C],
node : NodeId,
style : Style,
) -> Unit raise ChicleError {
match tree.nodes.get(node) {
Some(_) =>
if tree.nodes[node].alive {
tree.nodes[node].style = style
tree.nodes[node].cache.clear()
tree.mark_dirty(node)
} else {
raise InvalidNodeId(node)
}
None => raise InvalidNodeId(node)
}
}