|
import androidx.compose.foundation.* |
|
import androidx.compose.foundation.layout.* |
|
import androidx.compose.foundation.lazy.* |
|
import androidx.compose.material.* |
|
import androidx.compose.material.icons.* |
|
import androidx.compose.material.icons.filled.* |
|
import androidx.compose.runtime.* |
|
import androidx.compose.ui.* |
|
import androidx.compose.ui.unit.* |
|
import androidx.compose.ui.window.* |
|
|
|
fun main() = singleWindowApplication { |
|
val rootNode = CommentNode("Hi there", listOf( |
|
CommentNode("Oh hi, did it work?", listOf( |
|
CommentNode("Kind of, but a lot of shit is missing") |
|
)), |
|
CommentNode("Here, this should break the tree, surely?", |
|
(1..1_000_000).map { i -> CommentNode("Comment $i") }) |
|
)) |
|
|
|
CleverTreeView(rootNode) { node -> |
|
Text(node.content) |
|
} |
|
} |
|
|
|
data class CommentNode( |
|
val content: String, |
|
val children: List<CommentNode> = emptyList() |
|
) |
|
|
|
data class TreeRow( |
|
val nestingLevel: Int, |
|
val node: CommentNode, |
|
val expanded: Boolean = false, |
|
) |
|
|
|
@Composable |
|
fun CleverTreeView( |
|
rootNode: CommentNode, |
|
content: @Composable (CommentNode) -> Unit |
|
) { |
|
val treeRows = remember { mutableStateListOf(TreeRow(0, rootNode)) } |
|
val lazyListState = rememberLazyListState() |
|
|
|
Box(modifier = Modifier.fillMaxWidth()) { |
|
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxWidth()) { |
|
items(treeRows.size) { index -> |
|
val row = treeRows[index] |
|
Row( |
|
verticalAlignment = Alignment.CenterVertically, |
|
modifier = Modifier.padding(start = 8.dp * row.nestingLevel) |
|
) { |
|
IconButton( |
|
onClick = { |
|
// TODO: It would be super nice if these tree mutations were done in one step |
|
if (row.expanded) { |
|
println("Collapsing tree node...") |
|
treeRows[index] = row.copy(expanded = false) |
|
// Scan to find the next sibling to figure out how many rows to remove. |
|
// Not the optimal solution, but seems to work. |
|
println("Figuring out how many rows to remove...") |
|
var descendantCount = treeRows.subList(index + 1, treeRows.size) |
|
.indexOfFirst { r -> r.nestingLevel <= row.nestingLevel } |
|
if (descendantCount < 0) { |
|
descendantCount = treeRows.size - (index + 1) |
|
} |
|
println("Removing $descendantCount rows from the row list...") |
|
treeRows.removeRange(index + 1, index + 1 + descendantCount) |
|
println("Done!") |
|
} else { |
|
println("Expanding tree node...") |
|
treeRows[index] = row.copy(expanded = true) |
|
println("Adding ${row.node.children.size} rows to the row list...") |
|
treeRows.addAll(index + 1, row.node.children.map { child -> TreeRow(row.nestingLevel + 1, child) }) |
|
println("Done!") |
|
} |
|
}, |
|
// TODO: Figure out a sensible row height. Using dp isn't sensible because the text will be in sp. |
|
modifier = Modifier.size(24.dp) |
|
) { |
|
// TODO: Figure out a sane contentDescription for disclosure triangles |
|
Icon( |
|
imageVector = if (row.expanded) Icons.Filled.ArrowDropDown else Icons.Filled.ArrowRight, |
|
contentDescription = "" |
|
) |
|
} |
|
|
|
content(row.node) |
|
} |
|
} |
|
} |
|
|
|
VerticalScrollbar( |
|
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), |
|
adapter = rememberScrollbarAdapter(lazyListState) |
|
) |
|
} |
|
} |