2 Commits

Author SHA1 Message Date
Adrian Kuta
bec1fe02a7 feat: tree-structure-compose (LazyTree) + O(n) addChild cycle check
- New published module tree-structure-compose: a LazyTree composable for Compose
  Multiplatform (JVM/desktop, iOS, Wasm) with lazy rendering and expand/collapse.
- Fix an O(n^2) regression in addChild(): only walk ancestors for cycle detection
  when the child already has a subtree (a fresh leaf can never form a cycle), so
  building deep trees is O(n) again. Caught by the deep-chain stack-safety test on JS.
- README: Compose usage section; align all install snippets to 4.0.0.
- Version catalog: Compose Multiplatform + compose-compiler plugins.

Verified locally: JVM, JS(node), Wasm(node), iOS-simulator tests + apiCheck all green;
Compose module compiles for JVM, Wasm and iOS.
2026-06-07 18:55:07 +02:00
Adrian Kuta
69d19f89e3 feat!: v4.0 breaking API cleanup + explicitApi
BREAKING changes to the core:
- treeIterator is now a read-only `val`; added `iterator(order)` and use `asSequence(order)`.
- removeChild() only removes a direct child of the receiver; added `detach()` to unhook a node.
- addChild() rejects re-parenting and cycles (throws TreeNodeException); detach() first to move.
- clear() no longer nulls the receiver's own parent; only removes descendants.
- path() returns List<TreeNode<T>>? (null) instead of throwing.

Also:
- Enable strict explicitApi() across core + both modules; add explicit `public` modifiers.
- Update tests for the new contracts + add TreeNodeV4Test; refresh .api baselines.
- README + CHANGELOG (with migration notes); bump version to 4.0.0.

47 JVM tests green.
2026-06-07 18:47:40 +02:00
27 changed files with 445 additions and 102 deletions

View File

@@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on
## [Unreleased] ## [Unreleased]
## [4.0.0]
A breaking release that cleans up the core API and enforces an explicit public surface.
### Changed (breaking)
- `TreeNode.treeIterator` is now a read-only `val` (set it via the constructor). Use
`iterator(order)` or `asSequence(order)` to traverse in a different order per call.
- `removeChild(child)` now only removes a **direct** child of the receiver (previously it removed
the node from its actual parent regardless). Use `child.detach()` to unhook a node from wherever
it lives.
- `addChild(child)` now throws `TreeNodeException` if `child` already has a parent or if the
attachment would create a cycle. Call `detach()` first to move a node.
- `clear()` no longer detaches the receiver from its own parent; it only removes its descendants.
- `path(descendant)` now returns `List<TreeNode<T>>?` (`null` when `descendant` is the root or not a
descendant) instead of throwing `TreeNodeException`.
### Added
- `TreeNode.detach()` — removes a node from its parent.
- `TreeNode.iterator(order)` — a one-shot iterator in a specific order.
- Strict `explicitApi()` mode across all modules.
- New `tree-structure-compose` module: a `LazyTree` composable for Compose Multiplatform.
### Migration
- `node.treeIterator = PostOrder; for (n in node) { … }``for (n in node.asSequence(PostOrder)) { … }`
- `root.removeChild(deepNode)``deepNode.detach()`
- `try { node.path(x) } catch (e: TreeNodeException) { … }``node.path(x)?.let { … }`
## [3.4.0] ## [3.4.0]
### Added ### Added
@@ -44,7 +71,8 @@ All notable changes to this project are documented here. The format is based on
## [3.1.3] ## [3.1.3]
- iOS targets and Maven Central (Sonatype Central Portal) publishing. - iOS targets and Maven Central (Sonatype Central Portal) publishing.
[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...HEAD [Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.0.0...HEAD
[4.0.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...v4.0.0
[3.4.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.5...v3.4.0 [3.4.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.5...v3.4.0
[3.1.5]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.3...v3.1.5 [3.1.5]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.3...v3.1.5
[3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4 [3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4

View File

@@ -20,14 +20,14 @@ Gradle (Kotlin DSL):
```kotlin ```kotlin
// commonMain for KMP projects, or any sourceSet/module where you need it // commonMain for KMP projects, or any sourceSet/module where you need it
dependencies { dependencies {
implementation("com.github.adriankuta:tree-structure:3.1.5") // see badge above for the latest version implementation("com.github.adriankuta:tree-structure:4.0.0") // see badge above for the latest version
} }
``` ```
Gradle (Groovy): Gradle (Groovy):
```groovy ```groovy
dependencies { dependencies {
implementation "com.github.adriankuta:tree-structure:3.1.5" // see badge above for the latest implementation "com.github.adriankuta:tree-structure:4.0.0" // see badge above for the latest
} }
``` ```
@@ -36,7 +36,7 @@ Maven:
<dependency> <dependency>
<groupId>com.github.adriankuta</groupId> <groupId>com.github.adriankuta</groupId>
<artifactId>tree-structure</artifactId> <artifactId>tree-structure</artifactId>
<version>3.1.5</version> <version>4.0.0</version>
</dependency> </dependency>
``` ```
@@ -104,9 +104,8 @@ World
val root = TreeNode("root") val root = TreeNode("root")
// ... build your tree // ... build your tree
// Choose iteration order (default is PreOrder) // Choose iteration order per call (the default order is set in the constructor and is read-only)
root.treeIterator = TreeNodeIterators.PostOrder for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
for (node in root) println(node.value)
// Utilities // Utilities
root.nodeCount() // number of descendants root.nodeCount() // number of descendants
@@ -114,10 +113,10 @@ root.height() // longest path to a leaf (in edges)
root.depth() // distance from current node to the root root.depth() // distance from current node to the root
val path = root.path(root.children.first()) // nodes from descendant up to root val path = root.path(root.children.first()) // nodes from descendant up to root
// Mutations // Mutations — removeChild removes a *direct* child; detach() unhooks a node from wherever it lives
val child = root.children.first() val child = root.children.first()
root.removeChild(child) root.removeChild(child) // child is now detached from root
root.clear() // remove entire subtree root.clear() // remove all descendants of root
``` ```
### Lazy traversal with Sequence ### Lazy traversal with Sequence
@@ -180,7 +179,7 @@ separate, opt-in artifacts that depend on the core.
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead. be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead.
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:3.4.0") implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
``` ```
```kotlin ```kotlin
@@ -193,7 +192,7 @@ val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines). Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines).
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:3.4.0") implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
``` ```
```kotlin ```kotlin
@@ -201,6 +200,24 @@ tree.preOrderFlow().collect { println(it.value) }
tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value } tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
``` ```
### Compose UI — `tree-structure-compose`
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
are composed, and you decide how each node looks:
```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
```
```kotlin
LazyTree(root) { node, depth, expanded, toggle ->
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
if (!node.isLeaf) Text(if (expanded) "" else "")
Text(node.value.toString())
}
}
```
## Publishing to Maven Central (central.sonatype.com) ## Publishing to Maven Central (central.sonatype.com)
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal. This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal.

View File

@@ -13,6 +13,7 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri
public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode; public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V public final fun clear ()V
public final fun depth ()I public final fun depth ()I
public final fun detach ()Z
public final fun getChildren ()Ljava/util/List; public final fun getChildren ()Ljava/util/List;
public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode; public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators; public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
@@ -20,11 +21,11 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri
public final fun height ()I public final fun height ()I
public final fun isRoot ()Z public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator; public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun nodeCount ()I public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List; public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String; public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public final fun setTreeIterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V
public fun toString ()Ljava/lang/String; public fun toString ()Ljava/lang/String;
} }

View File

@@ -11,7 +11,7 @@ plugins {
val PUBLISH_GROUP_ID = "com.github.adriankuta" val PUBLISH_GROUP_ID = "com.github.adriankuta"
val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc. val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc.
val PUBLISH_VERSION = "3.4.0" val PUBLISH_VERSION = "4.0.0"
val snapshot: String? by project val snapshot: String? by project
@@ -60,6 +60,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -6,6 +6,7 @@ binaryCompatibilityValidator = "0.16.3"
kover = "0.8.3" kover = "0.8.3"
coroutines = "1.9.0" coroutines = "1.9.0"
kotlinxSerialization = "1.7.3" kotlinxSerialization = "1.7.3"
composeMultiplatform = "1.7.3"
[plugins] [plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
@@ -14,6 +15,8 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
[libraries] [libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }

View File

@@ -2,3 +2,4 @@ rootProject.name = "tree-structure"
include(":tree-structure-serialization") include(":tree-structure-serialization")
include(":tree-structure-coroutines") include(":tree-structure-coroutines")
include(":tree-structure-compose")

View File

@@ -2,7 +2,7 @@ package com.github.adriankuta.datastructure.tree
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
interface ChildDeclarationInterface<T> { public interface ChildDeclarationInterface<T> {
/** /**
* This method is used to easily create child in node. * This method is used to easily create child in node.
@@ -20,5 +20,5 @@ interface ChildDeclarationInterface<T> {
* @return New created TreeNode. * @return New created TreeNode.
*/ */
@JvmSynthetic @JvmSynthetic
fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T> public fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T>
} }

View File

@@ -27,14 +27,14 @@ import kotlin.jvm.JvmSynthetic
* @param treeIterator the default traversal order used by [iterator]. Prefer the * @param treeIterator the default traversal order used by [iterator]. Prefer the
* `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state. * `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state.
*/ */
open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> { public open class TreeNode<T>(public val value: T, public val treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> {
private var _parent: TreeNode<T>? = null private var _parent: TreeNode<T>? = null
/** /**
* The converse notion of a child, an immediate ancestor. * The converse notion of a child, an immediate ancestor.
*/ */
val parent: TreeNode<T>? public val parent: TreeNode<T>?
get() = _parent get() = _parent
private val _children = mutableListOf<TreeNode<T>>() private val _children = mutableListOf<TreeNode<T>>()
@@ -42,28 +42,60 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/** /**
* A group of nodes with the same parent. * A group of nodes with the same parent.
*/ */
val children: List<TreeNode<T>> public val children: List<TreeNode<T>>
get() = _children get() = _children
/** /**
* Checks whether the current tree node is the root of the tree * Checks whether the current tree node is the root of the tree
* @return `true` if the current tree node is root of the tree, `false` otherwise. * @return `true` if the current tree node is root of the tree, `false` otherwise.
*/ */
val isRoot: Boolean public val isRoot: Boolean
get() = _parent == null get() = _parent == null
/** /**
* Add new child to current node or root. * Adds [child] as a direct child of this node.
* *
* @param child A node which will be directly connected to current node. * @param child a node that is not already attached to a tree. To move a node that already has a
* parent, call [detach] on it first.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/ */
fun addChild(child: TreeNode<T>) { public fun addChild(child: TreeNode<T>) {
if (child._parent != null) {
throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.")
}
if (child === this) {
throw TreeNodeException("Adding $child here would create a cycle.")
}
// Only a node that already has its own subtree can contain `this` and thus form a cycle.
// Skipping this walk for leaves keeps building deep trees O(n) instead of O(n²).
if (child._children.isNotEmpty()) {
var ancestor: TreeNode<T>? = _parent
while (ancestor != null) {
if (ancestor === child) {
throw TreeNodeException("Adding $child here would create a cycle.")
}
ancestor = ancestor._parent
}
}
child._parent = this child._parent = this
_children.add(child) _children.add(child)
} }
/**
* Detaches this node from its parent, removing it from the parent's [children].
*
* @return `true` if this node was attached and is now detached; `false` if it was already a root.
*/
public fun detach(): Boolean {
val currentParent = _parent ?: return false
currentParent._children.remove(this)
_parent = null
return true
}
@JvmSynthetic @JvmSynthetic
override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> { public override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
val newChild = TreeNode(value) val newChild = TreeNode(value)
newChild._parent = this newChild._parent = this
if (childDeclaration != null) if (childDeclaration != null)
@@ -73,21 +105,24 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
} }
/** /**
* Removes a single instance of the specified node from this tree, if it is present. * Removes [child] from this node's direct [children], if present.
* *
* @return `true` if the node has been successfully removed; `false` if it was not present in the tree. * This only removes a *direct* child of the receiver; it does not reach into other nodes. To
* remove a node from wherever it currently lives, call [detach] on it instead.
*
* @return `true` if [child] was a direct child and has been removed; `false` otherwise.
*/ */
fun removeChild(child: TreeNode<T>): Boolean { public fun removeChild(child: TreeNode<T>): Boolean {
val removed = child._parent?._children?.remove(child) val removed = _children.remove(child)
child._parent = null if (removed) child._parent = null
return removed ?: false return removed
} }
/** /**
* This function go through tree and counts children. Root element is not counted. * This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count. * @return All child and nested child count.
*/ */
fun nodeCount(): Int { public fun nodeCount(): Int {
var count = 0 var count = 0
val stack = ArrayDeque<TreeNode<T>>() val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children) stack.addAll(_children)
@@ -102,7 +137,7 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/** /**
* @return The number of edges on the longest path between current node and a descendant leaf. * @return The number of edges on the longest path between current node and a descendant leaf.
*/ */
fun height(): Int { public fun height(): Int {
var maxDepth = 0 var maxDepth = 0
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>() val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(this to 0) stack.addLast(this to 0)
@@ -118,7 +153,7 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
* Distance is the number of edges along the shortest path between two nodes. * Distance is the number of edges along the shortest path between two nodes.
* @return The distance between current node and the root. * @return The distance between current node and the root.
*/ */
fun depth(): Int { public fun depth(): Int {
var depth = 0 var depth = 0
var tempParent = parent var tempParent = parent
@@ -130,53 +165,52 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
} }
/** /**
* Returns the collection of nodes, which connect the current node * Returns the chain of nodes from [descendant] up to and including this node, or `null` if
* with its descendants * [descendant] is not a strict descendant of this node.
* *
* @param descendant the bottom child node for which the path is calculated * @param descendant the node to walk up from.
* @return collection of nodes, which connect the current node with its descendants * @return the path `[descendant, …, this]`, or `null` if [descendant] is the root or is not
* @throws TreeNodeException exception that may be thrown in case if the * located in this node's subtree.
* current node does not have such descendant or if the
* specified tree node is root
*/ */
@Throws(TreeNodeException::class) public fun path(descendant: TreeNode<T>): List<TreeNode<T>>? {
fun path(descendant: TreeNode<T>): List<TreeNode<T>> { if (descendant.isRoot) return null
val path = mutableListOf<TreeNode<T>>() val path = mutableListOf<TreeNode<T>>()
var node = descendant var node = descendant
path.add(node) path.add(node)
while (!node.isRoot) { while (!node.isRoot) {
node = node.parent!! node = node.parent!!
path.add(node) path.add(node)
if (node == this) if (node == this) return path
return path
} }
throw TreeNodeException("The specified tree node $descendant is not the descendant of tree node $this") return null
} }
/** /**
* Remove all children from root and every node in tree. * Removes every descendant of this node. Afterwards [children] is empty and all former
* descendants are detached (their parent is `null`). This node itself stays attached to its own
* parent.
*/ */
fun clear() { public fun clear() {
val all = ArrayDeque<TreeNode<T>>() val descendants = ArrayDeque<TreeNode<T>>()
val stack = ArrayDeque<TreeNode<T>>() val stack = ArrayDeque<TreeNode<T>>()
stack.addLast(this) stack.addAll(_children)
while (stack.isNotEmpty()) { while (stack.isNotEmpty()) {
val node = stack.removeLast() val node = stack.removeLast()
all.addLast(node) descendants.addLast(node)
stack.addAll(node._children) stack.addAll(node._children)
} }
all.forEach { node -> descendants.forEach { node ->
node._parent = null node._parent = null
node._children.clear() node._children.clear()
} }
_children.clear()
} }
override fun toString(): String { public override fun toString(): String {
return value.toString() return value.toString()
} }
fun prettyString(): String { public fun prettyString(): String {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
print(stringBuilder, "", "") print(stringBuilder, "", "")
return stringBuilder.toString() return stringBuilder.toString()
@@ -198,9 +232,14 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
} }
/** /**
* You can change default iterator by changing [treeIterator] property. * Returns an iterator over this node and its descendants using the default [treeIterator] order.
* Use [iterator] with an explicit order, or the `asSequence(order)` extension, to traverse in a
* different order without changing this node's default.
*/ */
override fun iterator(): Iterator<TreeNode<T>> = when (treeIterator) { public override fun iterator(): Iterator<TreeNode<T>> = iterator(treeIterator)
/** Returns an iterator over this node and its descendants in the given [order]. */
public fun iterator(order: TreeNodeIterators): Iterator<TreeNode<T>> = when (order) {
PreOrder -> PreOrderTreeIterator(this) PreOrder -> PreOrderTreeIterator(this)
PostOrder -> PostOrderTreeIterator(this) PostOrder -> PostOrderTreeIterator(this)
LevelOrder -> LevelOrderTreeIterator(this) LevelOrder -> LevelOrderTreeIterator(this)

View File

@@ -3,7 +3,7 @@ package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit public typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
/** /**
* This method can be used to initialize new tree. * This method can be used to initialize new tree.
@@ -14,7 +14,7 @@ typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
* @see [ChildDeclarationInterface.child] * @see [ChildDeclarationInterface.child]
*/ */
@JvmSynthetic @JvmSynthetic
inline fun <reified T> tree( public inline fun <reified T> tree(
root: T, root: T,
defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder, defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder,
childDeclaration: ChildDeclaration<T> childDeclaration: ChildDeclaration<T>

View File

@@ -1,34 +1,34 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
/** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */ /** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */
fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? = public fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? =
preOrderSequence().firstOrNull { predicate(it.value) } preOrderSequence().firstOrNull { predicate(it.value) }
/** All nodes (pre-order) whose value matches [predicate]. */ /** All nodes (pre-order) whose value matches [predicate]. */
fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> = public fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> =
preOrderSequence().filter { predicate(it.value) }.toList() preOrderSequence().filter { predicate(it.value) }.toList()
/** `true` if any node's value matches [predicate]. Short-circuits. */ /** `true` if any node's value matches [predicate]. Short-circuits. */
fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean = public fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean =
preOrderSequence().any { predicate(it.value) } preOrderSequence().any { predicate(it.value) }
/** `true` if every node's value matches [predicate]. Short-circuits. */ /** `true` if every node's value matches [predicate]. Short-circuits. */
fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean = public fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean =
preOrderSequence().all { predicate(it.value) } preOrderSequence().all { predicate(it.value) }
/** Counts nodes whose value matches [predicate]. */ /** Counts nodes whose value matches [predicate]. */
fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int = public fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int =
preOrderSequence().count { predicate(it.value) } preOrderSequence().count { predicate(it.value) }
/** Folds [operation] over all nodes in pre-order, starting from [initial]. */ /** Folds [operation] over all nodes in pre-order, starting from [initial]. */
fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R = public fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R =
preOrderSequence().fold(initial) { acc, node -> operation(acc, node) } preOrderSequence().fold(initial) { acc, node -> operation(acc, node) }
/** /**
* Returns a new tree with the same shape whose values are produced by [transform]. The original is * Returns a new tree with the same shape whose values are produced by [transform]. The original is
* left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees. * left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees.
*/ */
fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> { public fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
val newRoot = TreeNode(transform(value), treeIterator) val newRoot = TreeNode(transform(value), treeIterator)
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>() val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>()
stack.addLast(this to newRoot) stack.addLast(this to newRoot)
@@ -44,13 +44,13 @@ fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
} }
/** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */ /** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */
fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it } public fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it }
/** /**
* Structural equality: `true` when [other] holds the same values in the same shape. Unlike * Structural equality: `true` when [other] holds the same values in the same shape. Unlike
* [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe. * [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe.
*/ */
fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean { public fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean {
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>() val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>()
stack.addLast(this to other) stack.addLast(this to other)
while (stack.isNotEmpty()) { while (stack.isNotEmpty()) {

View File

@@ -1,13 +1,13 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
/** `true` when this node has no children. */ /** `true` when this node has no children. */
val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty() public val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty()
/** The number of direct children of this node. */ /** The number of direct children of this node. */
val <T> TreeNode<T>.degree: Int get() = children.size public val <T> TreeNode<T>.degree: Int get() = children.size
/** Walks up the parent chain and returns the topmost ancestor (the tree root). */ /** Walks up the parent chain and returns the topmost ancestor (the tree root). */
fun <T> TreeNode<T>.root(): TreeNode<T> { public fun <T> TreeNode<T>.root(): TreeNode<T> {
var node = this var node = this
var parent = node.parent var parent = node.parent
while (parent != null) { while (parent != null) {
@@ -18,7 +18,7 @@ fun <T> TreeNode<T>.root(): TreeNode<T> {
} }
/** The chain of ancestors from the immediate [parent] up to (and including) the root. */ /** The chain of ancestors from the immediate [parent] up to (and including) the root. */
fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> { public fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
val result = mutableListOf<TreeNode<T>>() val result = mutableListOf<TreeNode<T>>()
var parent = this.parent var parent = this.parent
while (parent != null) { while (parent != null) {
@@ -29,13 +29,13 @@ fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
} }
/** The other children of this node's parent (excludes this node). Empty for the root. */ /** The other children of this node's parent (excludes this node). Empty for the root. */
fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> = public fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> =
parent?.children?.filter { it !== this } ?: emptyList() parent?.children?.filter { it !== this } ?: emptyList()
/** All leaf nodes in this subtree, in pre-order. */ /** All leaf nodes in this subtree, in pre-order. */
fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> = public fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> =
preOrderSequence().filter { it.isLeaf }.toList() preOrderSequence().filter { it.isLeaf }.toList()
/** All nodes in this subtree except this node, in pre-order. */ /** All nodes in this subtree except this node, in pre-order. */
fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> = public fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> =
preOrderSequence().filter { it !== this }.toList() preOrderSequence().filter { it !== this }.toList()

View File

@@ -10,7 +10,7 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
* traversal up front. Pairs with the Kotlin stdlib, e.g. * traversal up front. Pairs with the Kotlin stdlib, e.g.
* `root.asSequence().map { it.value }.firstOrNull { it == target }`. * `root.asSequence().map { it.value }.firstOrNull { it == target }`.
*/ */
fun <T> TreeNode<T>.asSequence( public fun <T> TreeNode<T>.asSequence(
order: TreeNodeIterators = TreeNodeIterators.PreOrder, order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Sequence<TreeNode<T>> { ): Sequence<TreeNode<T>> {
val self = this val self = this
@@ -22,10 +22,10 @@ fun <T> TreeNode<T>.asSequence(
} }
/** Lazy pre-order traversal as a [Sequence]. */ /** Lazy pre-order traversal as a [Sequence]. */
fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder) public fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder)
/** Lazy post-order traversal as a [Sequence]. */ /** Lazy post-order traversal as a [Sequence]. */
fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder) public fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder)
/** Lazy level-order (breadth-first) traversal as a [Sequence]. */ /** Lazy level-order (breadth-first) traversal as a [Sequence]. */
fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder) public fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder)

View File

@@ -2,5 +2,5 @@ package com.github.adriankuta.datastructure.tree.exceptions
import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmOverloads
class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) : public class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause) RuntimeException(message, cause)

View File

@@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 1 2 3 4 5 6 7 8 9 10 11 12 13 * Output: 1 2 3 4 5 6 7 8 9 10 11 12 13
* ``` * ```
*/ */
class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>() private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root) stack.addLast(root)
} }
override fun hasNext(): Boolean = stack.isNotEmpty() public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> { public override fun next(): TreeNode<T> {
val node = stack.removeFirst() val node = stack.removeFirst()
node.children node.children
.forEach { stack.addLast(it) } .forEach { stack.addLast(it) }

View File

@@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 10 5 11 12 13 6 2 3 7 8 9 4 1 * Output: 10 5 11 12 13 6 2 3 7 8 9 4 1
* ``` * ```
*/ */
class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val result = ArrayDeque<TreeNode<T>>() private val result = ArrayDeque<TreeNode<T>>()
@@ -37,7 +37,7 @@ class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
} }
} }
override fun hasNext(): Boolean = result.isNotEmpty() public override fun hasNext(): Boolean = result.isNotEmpty()
override fun next(): TreeNode<T> = result.removeFirst() public override fun next(): TreeNode<T> = result.removeFirst()
} }

View File

@@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 1 2 5 10 6 11 12 13 3 4 7 8 9 * Output: 1 2 5 10 6 11 12 13 3 4 7 8 9
* ``` * ```
*/ */
class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>() private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root) stack.addLast(root)
} }
override fun hasNext(): Boolean = stack.isNotEmpty() public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> { public override fun next(): TreeNode<T> {
val node = stack.removeLast() val node = stack.removeLast()
node.children node.children
.asReversed() .asReversed()

View File

@@ -5,7 +5,7 @@ package com.github.adriankuta.datastructure.tree.iterators
* @see PostOrder * @see PostOrder
* @see LevelOrder * @see LevelOrder
*/ */
enum class TreeNodeIterators { public enum class TreeNodeIterators {
/** /**
* Tree is iterated by using `Pre-order Traversal Algorithm" * Tree is iterated by using `Pre-order Traversal Algorithm"
* The pre-order traversal is a topologically sorted one, * The pre-order traversal is a topologically sorted one,

View File

@@ -37,13 +37,11 @@ class TreeNodeStackSafetyTest {
@Test @Test
fun postOrderIterationDoesNotOverflowOnDeepTree() { fun postOrderIterationDoesNotOverflowOnDeepTree() {
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PostOrder } assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PostOrder).count())
assertEquals(depth + 1, tree.toList().size)
} }
@Test @Test
fun preOrderIterationDoesNotOverflowOnDeepTree() { fun preOrderIterationDoesNotOverflowOnDeepTree() {
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PreOrder } assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PreOrder).count())
assertEquals(depth + 1, tree.toList().size)
} }
} }

View File

@@ -51,7 +51,7 @@ class TreeNodeTest {
) )
root.removeChild(curdNode) root.removeChild(curdNode)
root.removeChild(gingerTeaNode) gingerTeaNode.detach()
assertEquals( assertEquals(
"Root\n" + "Root\n" +
"└── Beverages\n" + "└── Beverages\n" +

View File

@@ -1,10 +1,9 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertNull
class TreeNodeUtilitiesTest { class TreeNodeUtilitiesTest {
@@ -43,12 +42,12 @@ class TreeNodeUtilitiesTest {
} }
@Test @Test
fun pathThrowsWhenNotADescendant() { fun pathReturnsNullWhenNotADescendant() {
assertFailsWith<TreeNodeException> { root.path(TreeNode(99)) } assertNull(root.path(TreeNode(99)))
} }
@Test @Test
fun pathThrowsWhenDescendantIsRootItself() { fun pathReturnsNullWhenDescendantIsRootItself() {
assertFailsWith<TreeNodeException> { root.path(root) } assertNull(root.path(root))
} }
} }

View File

@@ -0,0 +1,105 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeV4Test {
@Test
fun addChildRejectsNodeThatAlreadyHasAParent() {
val a = TreeNode("a")
val b = TreeNode("b")
a.addChild(b)
val other = TreeNode("other")
assertFailsWith<TreeNodeException> { other.addChild(b) }
}
@Test
fun addChildRejectsCycles() {
val root = TreeNode("root")
val child = TreeNode("child")
root.addChild(child)
// Attaching an ancestor under its own descendant would create a cycle.
assertFailsWith<TreeNodeException> { child.addChild(root) }
// Attaching a node under itself is also a cycle.
assertFailsWith<TreeNodeException> { root.addChild(root) }
}
@Test
fun detachRemovesFromParent() {
val root = TreeNode("root")
val child = TreeNode("child")
root.addChild(child)
assertTrue(child.detach())
assertNull(child.parent)
assertContentEquals(emptyList(), root.children)
// Detached node can now be re-attached elsewhere.
val newParent = TreeNode("newParent")
newParent.addChild(child)
assertSame(newParent, child.parent)
}
@Test
fun detachOnRootReturnsFalse() {
assertFalse(TreeNode("root").detach())
}
@Test
fun removeChildOnlyRemovesDirectChildren() {
val root = TreeNode("root")
val parent = TreeNode("parent")
val grandchild = TreeNode("grandchild")
root.addChild(parent)
parent.addChild(grandchild)
// grandchild is not a direct child of root -> no-op, returns false.
assertFalse(root.removeChild(grandchild))
assertSame(parent, grandchild.parent)
// direct child removal works.
assertTrue(parent.removeChild(grandchild))
assertNull(grandchild.parent)
}
@Test
fun clearOnNonRootKeepsItAttachedToItsParent() {
val root = TreeNode("root")
val branch = TreeNode("branch")
val leaf = TreeNode("leaf")
root.addChild(branch)
branch.addChild(leaf)
branch.clear()
assertContentEquals(emptyList(), branch.children)
assertSame(root, branch.parent) // branch stays attached to root
assertContentEquals(listOf(branch), root.children)
assertNull(leaf.parent) // former descendant is detached
}
@Test
fun iteratorAcceptsExplicitOrderWithoutMutatingDefault() {
val tree = tree(1) {
child(2) { child(4) }
child(3)
}
val postOrder = tree.iterator(TreeNodeIterators.PostOrder).asSequence().map { it.value }.toList()
assertContentEquals(listOf(4, 2, 3, 1), postOrder)
// Default order is unchanged (PreOrder).
assertEquals(TreeNodeIterators.PreOrder, tree.treeIterator)
assertContentEquals(listOf(1, 2, 4, 3), tree.map { it.value })
}
}

View File

@@ -0,0 +1,4 @@
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -0,0 +1,74 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.dokka)
alias(libs.plugins.mavenPublish)
signing
}
group = "com.github.adriankuta"
version = rootProject.version
mavenPublishing {
publishToMavenCentral(automaticRelease = false)
signAllPublications()
coordinates("com.github.adriankuta", "tree-structure-compose", version.toString())
pom {
name.set("Tree Data Structure — Compose Multiplatform")
description.set("A LazyTree composable (expand/collapse, lazy rendering) for the tree-structure library.")
url.set("https://github.com/AdrianKuta/Tree-Data-Structure")
licenses {
license {
name.set("MIT License")
url.set("https://opensource.org/licenses/MIT")
distribution.set("repo")
}
}
developers {
developer {
id.set("AdrianKuta")
name.set("Adrian Kuta")
email.set("adrian.kuta93@gmail.com")
}
}
scm {
url.set("https://github.com/AdrianKuta/Tree-Data-Structure")
connection.set("scm:git:https://github.com/AdrianKuta/Tree-Data-Structure.git")
developerConnection.set("scm:git:ssh://git@github.com/AdrianKuta/Tree-Data-Structure.git")
}
}
}
repositories {
mavenCentral()
google()
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
api(project(":"))
implementation(compose.runtime)
implementation(compose.foundation)
}
}
}

View File

@@ -0,0 +1,71 @@
package com.github.adriankuta.datastructure.tree.compose
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* A lazily-rendered, expand/collapse tree for Compose Multiplatform. Only the currently-visible
* nodes are composed, so it scales to large trees. Expansion state is remembered internally, keyed
* by node identity.
*
* ```
* LazyTree(root) { node, depth, expanded, toggle ->
* Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
* if (!node.isLeaf) Text(if (expanded) "▾" else "▸")
* Text(node.value.toString())
* }
* }
* ```
*
* @param root the root of the tree to display.
* @param modifier the [Modifier] applied to the underlying [LazyColumn].
* @param initiallyExpanded whether nodes start expanded.
* @param nodeContent renders a single node. Receives the node, its depth (root = 0), whether it is
* expanded, and a `toggle` callback that flips this node's expansion state.
*/
@Composable
public fun <T> LazyTree(
root: TreeNode<T>,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = true,
nodeContent: @Composable (node: TreeNode<T>, depth: Int, expanded: Boolean, toggle: () -> Unit) -> Unit,
) {
val expansion = remember(root) { mutableStateMapOf<TreeNode<T>, Boolean>() }
val isExpanded: (TreeNode<T>) -> Boolean = { node -> expansion[node] ?: initiallyExpanded }
val visible = flattenVisible(root, isExpanded)
LazyColumn(modifier = modifier) {
items(visible.size) { index ->
val (node, depth) = visible[index]
nodeContent(node, depth, isExpanded(node)) {
expansion[node] = !isExpanded(node)
}
}
}
}
/**
* Flattens the tree into the list of currently-visible `(node, depth)` pairs in pre-order, skipping
* the subtrees of collapsed nodes. Iterative, so it is safe on deep trees.
*/
private fun <T> flattenVisible(
root: TreeNode<T>,
isExpanded: (TreeNode<T>) -> Boolean,
): List<Pair<TreeNode<T>, Int>> {
val result = mutableListOf<Pair<TreeNode<T>, Int>>()
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(root to 0)
while (stack.isNotEmpty()) {
val (node, depth) = stack.removeLast()
result.add(node to depth)
if (isExpanded(node)) {
node.children.asReversed().forEach { child -> stack.addLast(child to depth + 1) }
}
}
return result
}

View File

@@ -47,6 +47,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -48,6 +48,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -14,8 +14,8 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
public data class TreeNodeDto<T>( public data class TreeNodeDto<T>(
val value: T, public val value: T,
val children: List<TreeNodeDto<T>> = emptyList(), public val children: List<TreeNodeDto<T>> = emptyList(),
) )
/** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */ /** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */