Skip to content

Commit

Permalink
LazyTree: moving out Tree.Element from LazyTreeState (#92)
Browse files Browse the repository at this point in the history
Move flattenTree out from the treeState
  • Loading branch information
fscarponi authored Aug 9, 2023
1 parent d627ce9 commit d147f50
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 236 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.jetbrains.jewel.foundation.tree

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
Expand All @@ -22,16 +22,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn
import org.jetbrains.jewel.foundation.lazy.SelectableLazyItemScope
import org.jetbrains.jewel.foundation.utils.Log
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

Expand Down Expand Up @@ -78,18 +78,17 @@ fun <T> BasicLazyTree(
platformDoubleClickDelay: Duration = 500.milliseconds,
keyActions: KeyBindingScopedActions = DefaultTreeViewKeyActions(treeState),
pointerEventScopedActions: PointerEventScopedActions = remember {
DefaultTreeViewPointerEventAction(
treeState,
platformDoubleClickDelay.inWholeMilliseconds,
onElementClick,
onElementDoubleClick
)
DefaultTreeViewPointerEventAction(treeState)
},
chevronContent: @Composable (nodeState: TreeElementState) -> Unit,
nodeContent: @Composable SelectableLazyItemScope.(Tree.Element<T>) -> Unit,
) {
LaunchedEffect(tree) {
treeState.attachTree(tree)
var flattenedTree by remember { mutableStateOf(emptyList<Tree.Element<*>>()) }

LaunchedEffect(tree, treeState.openNodes.size) {
// refresh flattenTree
flattenedTree = tree.roots.flatMap { flattenTree(it, treeState.openNodes, treeState.allNodes) }
treeState.delegate.updateKeysIndexes()
}

val scope = rememberCoroutineScope()
Expand All @@ -103,18 +102,18 @@ fun <T> BasicLazyTree(
pointerHandlingScopedActions = pointerEventScopedActions
) {
items(
count = treeState.flattenedTree.size,
count = flattenedTree.size,
key = {
val idPath = treeState.flattenedTree[it].idPath()
val idPath = flattenedTree[it].idPath()
idPath
},
contentType = { treeState.flattenedTree[it].data }
contentType = { flattenedTree[it].data }
) { itemIndex ->
val element = treeState.flattenedTree[itemIndex]
val element = flattenedTree[itemIndex]
val elementState = TreeElementState.of(
focused = isFocused,
selected = isSelected,
expanded = (element as? Tree.Element.Node)?.let { treeState.isNodeOpen(element) } ?: false
expanded = (element as? Tree.Element.Node)?.let { it.idPath() in treeState.openNodes } ?: false
)

val backgroundShape by remember { mutableStateOf(RoundedCornerShape(elementBackgroundCornerSize)) }
Expand All @@ -131,19 +130,27 @@ fun <T> BasicLazyTree(
)
.padding(elementContentPadding)
.padding(start = (element.depth * indentSize.value).dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
(pointerEventScopedActions as? DefaultTreeViewPointerEventAction)?.notifyItemClicked(
item = flattenedTree[itemIndex] as Tree.Element<T>,
scope = scope,
doubleClickTimeDelayMillis = platformDoubleClickDelay.inWholeMilliseconds,
onElementClick = onElementClick,
onElementDoubleClick = onElementDoubleClick
)
}
) {
if (element is Tree.Element.Node) {
Box(
modifier = Modifier.pointerInput(Unit) {
while (true) {
awaitPointerEventScope {
awaitFirstDown(false)
scope.launch {
treeState.toggleNode(element)
onElementDoubleClick(element as Tree.Element<T>)
}
}
}
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
treeState.toggleNode(element.idPath())
onElementDoubleClick(element as Tree.Element<T>)
}
) {
chevronContent(elementState)
Expand Down Expand Up @@ -216,3 +223,48 @@ value class TreeElementState(val state: ULong) {
)
}
}

private suspend fun flattenTree(
element: Tree.Element<*>,
openNodes: SnapshotStateList<Any>,
allNodes: SnapshotStateList<Any>,
): MutableList<Tree.Element<*>> {
val orderedChildren = mutableListOf<Tree.Element<*>>()
when (element) {
is Tree.Element.Node<*> -> {
if (element.idPath() !in allNodes) allNodes.add(element.idPath())
orderedChildren.add(element)
if (element.idPath() !in openNodes) {
return orderedChildren.also {
element.close()
// remove all children key from openNodes
openNodes.removeAll(
buildList {
getAllSubNodes(element)
}
)
}
}
Log.w("the node is open, loading children for ${element.idPath()}")
Log.w("children size: ${element.children?.size}")
element.open(true)
element.children?.forEach { child ->
orderedChildren.addAll(flattenTree(child, openNodes, allNodes))
}
}

is Tree.Element.Leaf<*> -> {
orderedChildren.add(element)
}
}
return orderedChildren
}

private infix fun MutableList<Any>.getAllSubNodes(node: Tree.Element.Node<*>) {
node.children
?.filterIsInstance<Tree.Element.Node<*>>()
?.forEach {
add(it.idPath())
this@getAllSubNodes getAllSubNodes (it)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,37 @@ open class DefaultTreeViewOnKeyEvent(

override suspend fun onSelectFirstItem() {
Log.e(treeState.toString())
if (treeState.flattenedTree.size > 0) treeState.selectSingleElement(0)
if (treeState.delegate.keys.isNotEmpty()) treeState.selectSingleElement(0)
}

override suspend fun onExtendSelectionToFirst(currentIndex: Int) {
if (treeState.flattenedTree.isNotEmpty()) {
if (treeState.delegate.keys.isNotEmpty()) {
treeState.addElementsToSelection((0..currentIndex).toList().reversed())
}
}

override suspend fun onSelectLastItem() {
treeState.flattenedTree.lastIndex.takeIf { it >= 0 }?.let {
treeState.delegate.keys.lastIndex.takeIf { it >= 0 }?.let {
treeState.selectSingleElement(it)
}
}

override suspend fun onExtendSelectionToLastItem(currentIndex: Int) {
if (treeState.flattenedTree.isNotEmpty()) {
treeState.addElementsToSelection((currentIndex..treeState.flattenedTree.lastIndex).toList())
if (treeState.delegate.keys.isNotEmpty()) {
treeState.addElementsToSelection((currentIndex..treeState.delegate.keys.lastIndex).toList())
}
}

override suspend fun onSelectPreviousItem(currentIndex: Int) {
treeState.flattenedTree.getOrNull(currentIndex - 1)?.let {
treeState.delegate.keys.getOrNull(currentIndex - 1)?.let {
treeState.selectSingleElement(currentIndex - 1)
}
}

override suspend fun onExtendSelectionWithPreviousItem(currentIndex: Int) {
val prevIndex = currentIndex - 1
// if (treeState.flattenedTree.isNotEmpty() && prevIndex >= 0) {
// treeState.delegate.onExtendSelectionToIndex(prevIndex)
// }
if (treeState.flattenedTree.isNotEmpty() && prevIndex >= 0) {

if (treeState.delegate.keys.isNotEmpty() && prevIndex >= 0) {
if (treeState.lastKeyEventUsedMouse) {
if (treeState.delegate.selectedItemIndexes.contains(prevIndex)) {
// we are are changing direction so we needs just deselect the current element
Expand All @@ -69,15 +67,15 @@ open class DefaultTreeViewOnKeyEvent(
}

override suspend fun onSelectNextItem(currentIndex: Int) {
if (treeState.flattenedTree.size > currentIndex + 1) {
if (treeState.delegate.keys.size > currentIndex + 1) {
treeState.selectSingleElement(currentIndex + 1)
}
}

override suspend fun onExtendSelectionWithNextItem(currentIndex: Int) {
val nextFlattenIndex = currentIndex + 1

if (treeState.flattenedTree.isNotEmpty() && nextFlattenIndex <= treeState.flattenedTree.lastIndex) {
if (treeState.delegate.keys.isNotEmpty() && nextFlattenIndex <= treeState.delegate.keys.lastIndex) {
if (treeState.lastKeyEventUsedMouse) {
if (treeState.delegate.selectedItemIndexes.contains(nextFlattenIndex)) {
// we are are changing direction so we needs just deselect the current element
Expand All @@ -101,43 +99,24 @@ open class DefaultTreeViewOnKeyEvent(
}

override suspend fun onSelectParent(flattenedIndex: Int) {
treeState.flattenedTree[flattenedIndex].let {
if (it is Tree.Element.Node && treeState.isNodeOpen(it)) {
treeState.toggleNode(it)
treeState.delegate.focusItem(flattenedIndex, animate, scrollOffset)
} else {
treeState.flattenedTree.getOrNull(flattenedIndex)?.parent?.let {
val parentIndex = treeState.flattenedTree.indexOf(it)
treeState.selectSingleElement(parentIndex)
}
}
}
}
val currentKey = treeState.delegate.keys[flattenedIndex].key

override suspend fun onExtendSelectionToParent(flattenedIndex: Int) {
treeState.flattenedTree.getOrNull(flattenedIndex)?.parent?.let {
val parentIndex = treeState.flattenedTree.indexOf(it)
for (index in parentIndex..flattenedIndex) {
treeState.toggleElementSelection(flattenedIndex)
}
if (currentKey in treeState.allNodes && currentKey in treeState.openNodes) {
treeState.toggleNode(currentKey)
} else {
onSelectPreviousItem(flattenedIndex)
}
}

override suspend fun onSelectChild(flattenedIndex: Int) {
treeState.flattenedTree.getOrNull(flattenedIndex)?.let {
if (it is Tree.Element.Node && !treeState.isNodeOpen(it)) {
treeState.toggleNode(it)
treeState.delegate.focusItem(flattenedIndex, animate, scrollOffset)
} else {
onSelectNextItem(flattenedIndex)
}
val currentKey = treeState.delegate.keys[flattenedIndex].key
if (currentKey in treeState.allNodes && currentKey !in treeState.openNodes) {
treeState.toggleNode(currentKey)
} else {
onSelectNextItem(flattenedIndex)
}
}

override suspend fun onExtendSelectionToChild(flattenedIndex: Int) {
treeState.delegate.onExtendSelectionToIndex(flattenedIndex, skipScroll = true)
}

override suspend fun onScrollPageUpAndSelectItem(currentIndex: Int) {
val visibleSize = treeState.delegate.layoutInfo.visibleItemsInfo.size
val targetIndex = max(currentIndex - visibleSize, 0)
Expand All @@ -154,28 +133,20 @@ open class DefaultTreeViewOnKeyEvent(
override suspend fun onScrollPageDownAndSelectItem(currentIndex: Int) {
val firstVisible = treeState.delegate.firstVisibleItemIndex
val visibleSize = treeState.delegate.layoutInfo.visibleItemsInfo.size
val targetIndex = min(firstVisible + visibleSize, treeState.flattenedTree.lastIndex)
val targetIndex = min(firstVisible + visibleSize, treeState.delegate.keys.lastIndex)
treeState.selectSingleElement(targetIndex)
}

override suspend fun onScrollPageDownAndExtendSelection(currentIndex: Int) {
val firstVisible = treeState.delegate.firstVisibleItemIndex
val visibleSize = treeState.delegate.layoutInfo.visibleItemsInfo.size
val targetIndex = min(firstVisible + visibleSize, treeState.flattenedTree.lastIndex)
val targetIndex = min(firstVisible + visibleSize, treeState.delegate.keys.lastIndex)

treeState.addElementsToSelection((currentIndex..targetIndex).toList(), targetIndex)

treeState.delegate.focusItem(targetIndex, animate, scrollOffset)
}

override suspend fun onSelectNextSibling(flattenedIndex: Int) {
treeState.delegate.onExtendSelectionToIndex(flattenedIndex)
}

override suspend fun onSelectPreviousSibling(flattenedIndex: Int) {
treeState.delegate.onExtendSelectionToIndex(flattenedIndex)
}

override suspend fun onEdit(currentIndex: Int) {
// ij with this shortcut just focus the first element with issue
// unavailable here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,8 @@ class DefaultSelectableLazyColumnPointerEventAction(private val state: Selectabl
}
}

class DefaultTreeViewPointerEventAction<T>(
class DefaultTreeViewPointerEventAction(
private val treeState: TreeState,
private val platformDoubleClickDelay: Long,
private val onElementClick: (Tree.Element<T>) -> Unit,
private val onElementDoubleClick: (Tree.Element<T>) -> Unit,
) : PointerEventScopedActions {

override fun handlePointerEventPress(
Expand Down Expand Up @@ -110,16 +107,6 @@ class DefaultTreeViewPointerEventAction<T>(
}

else -> {
val element = treeState.flattenedTree[treeState.delegate.keys.indexOfFirst { it.key == key }]
Log.e(treeState.toString())
@Suppress("UNCHECKED_CAST")
notifyItemClicked(
item = element as Tree.Element<T>,
scope = scope,
doubleClickTimeDelayMillis = platformDoubleClickDelay,
onElementClick = onElementClick,
onElementDoubleClick = onElementDoubleClick
)
scope.launch {
treeState.delegate.selectSingleKey(key, skipScroll = true)
}
Expand All @@ -130,29 +117,29 @@ class DefaultTreeViewPointerEventAction<T>(

// todo warning: this is ugly workaround
// for item click that lose focus and fail to match if a operation is a double-click
var elementClickedTmpHolder: Tree.Element<*>? = null
private var elementClickedTmpHolder: List<Any>? = null
internal fun <T> notifyItemClicked(
item: Tree.Element<T>,
scope: CoroutineScope,
doubleClickTimeDelayMillis: Long,
onElementClick: (Tree.Element<T>) -> Unit,
onElementDoubleClick: (Tree.Element<T>) -> Unit,
) {
if (elementClickedTmpHolder?.id == item.id) {
if (elementClickedTmpHolder == item.idPath()) {
// is a double click
if (item is Tree.Element.Node) {
treeState.toggleNode(item)
treeState.toggleNode(item.idPath())
}
onElementDoubleClick(item)
elementClickedTmpHolder = null
Log.d("doubleClicked!")
} else {
elementClickedTmpHolder = item
elementClickedTmpHolder = item.idPath()
// is a single click
onElementClick(item)
scope.launch {
delay(doubleClickTimeDelayMillis)
if (elementClickedTmpHolder == item) elementClickedTmpHolder = null
if (elementClickedTmpHolder == item.idPath()) elementClickedTmpHolder = null
}

Log.d("singleClicked!")
Expand All @@ -172,10 +159,6 @@ class DefaultTreeViewKeyActions(treeState: TreeState) : DefaultSelectableLazyCol
with(actions) {
Log.d(keyEvent.key.keyCode.toString())
when {
extendSelectionToChild() ?: false -> coroutineScope.launch { onExtendSelectionToChild(focusedIndex) }
extendSelectionToParent() ?: false -> coroutineScope.launch { onExtendSelectionToParent(focusedIndex) }
selectNextSibling() ?: false -> coroutineScope.launch { onSelectNextSibling(focusedIndex) }
selectPreviousSibling() ?: false -> coroutineScope.launch { onSelectPreviousSibling(focusedIndex) }
selectParent() ?: false -> coroutineScope.launch { onSelectParent(focusedIndex) }
selectChild() ?: false -> coroutineScope.launch { onSelectChild(focusedIndex) }
super.handleOnKeyEvent(coroutineScope).invoke(keyEvent, focusedIndex) -> return@lambda true
Expand Down
Loading

0 comments on commit d147f50

Please sign in to comment.