diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 365f5a8216..30f000e286 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -31,22 +31,6 @@ internal data class Node( override fun hashCode(): Int { return id.hashCode() } - - companion object { - fun getNodeField(node: Node, field: String): String { - return when (field.lowercase()) { - "name" -> node.name - "id" -> node.id - "parent" -> node.parent - "parentid" -> node.parentId - "props" -> node.props - "state" -> node.state - "rendering" -> node.rendering - "children" -> node.children.toString() - else -> throw IllegalArgumentException("Unknown field: $field") - } - } - } } internal fun Node.addChild(child: Node): Node { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt index c811e7168e..1cdcc013b8 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt @@ -1,6 +1,8 @@ package com.squareup.workflow1.traceviewer.ui +import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState @@ -11,8 +13,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch /** * A trace tab selector that allows devs to switch between different states within the provided trace. @@ -24,17 +30,31 @@ internal fun FrameSelectTab( onIndexChange: (Int) -> Unit, modifier: Modifier = Modifier ) { - val state = rememberLazyListState() + val lazyListState = rememberLazyListState() Surface( - modifier = modifier - .padding(4.dp), + modifier = modifier, color = Color.White, ) { LazyRow( + state = lazyListState, modifier = Modifier - .padding(8.dp), - state = state + .padding(8.dp) + .pointerInput(Unit) { + coroutineScope { + awaitEachGesture { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Scroll) { + val scrollDeltaY = event.changes.first().scrollDelta.y + launch { + lazyListState.scroll(MutatePriority.Default) { + scrollBy(scrollDeltaY * 10f) + } + } + } + } + } + }, ) { items(frames.size) { index -> Text( diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index a74b550e50..bcf09fddcb 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -35,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.model.NodeUpdate +import kotlin.reflect.full.memberProperties /** * A panel that displays information about the selected workflow node. @@ -103,14 +103,18 @@ private fun NodePanelDetails( modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) ) } - val fields = listOf("Name", "Id", "Props", "State", "Rendering") - items(fields) { field -> - DetailCard( - label = field, - currValue = Node.getNodeField(node.current, field), - pastValue = if (node.previous != null) Node.getNodeField(node.previous, field) else null - ) + val fields = Node::class.memberProperties + for (field in fields) { + val currVal = field.get(node.current).toString() + val pastVal = if (node.previous != null) field.get(node.previous).toString() else null + item { + DetailCard( + label = field.name, + currValue = currVal, + pastValue = pastVal + ) + } } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index bd2c21f8a6..7ece7092ef 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -2,7 +2,6 @@ package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,12 +13,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.util.ParseResult @@ -69,19 +74,29 @@ internal fun RenderDiagram( if (!isLoading) { val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null - DrawTree(fullTree[frameInd], previousFrame, affectedNodes[frameInd], onNodeSelect) + DrawTree( + node = fullTree[frameInd], + previousNode = previousFrame, + affectedNodes = affectedNodes[frameInd], + expandedNodes = remember(frameInd) { mutableStateMapOf() }, + onNodeSelect = onNodeSelect, + ) } } /** * Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree * The Column holds a subtree of nodes, and the Row holds all the children of the current node + * + * A mutable map is used to persist the expansion state of the nodes, allowing them to be open and + * closed from user clicks. */ @Composable private fun DrawTree( node: Node, previousNode: Node?, affectedNodes: Set, + expandedNodes: MutableMap, onNodeSelect: (Node, Node?) -> Unit, modifier: Modifier = Modifier, ) { @@ -93,21 +108,42 @@ private fun DrawTree( horizontalAlignment = Alignment.CenterHorizontally, ) { val isAffected = affectedNodes.contains(node) - DrawNode(node, previousNode, isAffected, onNodeSelect) + // By default, nodes that relevant to this specific frame are expanded. All others are closed. + LaunchedEffect(expandedNodes) { + expandedNodes[node.id] = isAffected + } + val isExpanded = expandedNodes[node.id] == true + + DrawNode( + node, + previousNode, + isAffected, + isExpanded, + onNodeSelect, + onExpandToggle = { expandedNodes[node.id] = !expandedNodes[node.id]!! } + ) // Draws the node's children recursively. - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.Top - ) { - /* + if (isExpanded) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top + ) { + /* We pair up the current node's children with previous frame's children. In the edge case that the current frame has additional children compared to the previous frame, we replace with null and will check before next recursive call. - */ - node.children.forEach { (index, childNode) -> - val prevChildNode = previousNode?.children?.get(index) - DrawTree(childNode, prevChildNode, affectedNodes, onNodeSelect) + */ + node.children.forEach { (index, childNode) -> + val prevChildNode = previousNode?.children?.get(index) + DrawTree( + node = childNode, + previousNode = prevChildNode, + affectedNodes = affectedNodes, + expandedNodes = expandedNodes, + onNodeSelect = onNodeSelect + ) + } } } } @@ -116,24 +152,38 @@ private fun DrawTree( /** * A basic box that represents a workflow node */ +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun DrawNode( node: Node, previousNode: Node?, isAffected: Boolean, + isExpanded: Boolean, onNodeSelect: (Node, Node?) -> Unit, + onExpandToggle: (Node) -> Unit, ) { Box( modifier = Modifier .background(if (isAffected) Color.Green else Color.Transparent) - .clickable { - // Selecting a node will bubble back up to the main view to handle the selection - onNodeSelect(node, previousNode) + .onPointerEvent(PointerEventType.Press) { + if (it.buttons.isPrimaryPressed) { + onNodeSelect(node, previousNode) + } else if (it.buttons.isSecondaryPressed) { + onExpandToggle(node) + } } .padding(10.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = node.name) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (node.children.isNotEmpty()) { + Text(text = if (isExpanded) "▼" else "▶") + } + Text(text = node.name) + } Text(text = "ID: ${node.id}") } }