@@ -2,7 +2,6 @@ package com.squareup.workflow1.traceviewer.ui
2
2
3
3
import androidx.compose.foundation.background
4
4
import androidx.compose.foundation.border
5
- import androidx.compose.foundation.clickable
6
5
import androidx.compose.foundation.layout.Arrangement
7
6
import androidx.compose.foundation.layout.Box
8
7
import androidx.compose.foundation.layout.Column
@@ -14,12 +13,18 @@ import androidx.compose.runtime.Composable
14
13
import androidx.compose.runtime.LaunchedEffect
15
14
import androidx.compose.runtime.getValue
16
15
import androidx.compose.runtime.mutableStateListOf
16
+ import androidx.compose.runtime.mutableStateMapOf
17
17
import androidx.compose.runtime.mutableStateOf
18
18
import androidx.compose.runtime.remember
19
19
import androidx.compose.runtime.setValue
20
20
import androidx.compose.ui.Alignment
21
+ import androidx.compose.ui.ExperimentalComposeUiApi
21
22
import androidx.compose.ui.Modifier
22
23
import androidx.compose.ui.graphics.Color
24
+ import androidx.compose.ui.input.pointer.PointerEventType
25
+ import androidx.compose.ui.input.pointer.isPrimaryPressed
26
+ import androidx.compose.ui.input.pointer.isSecondaryPressed
27
+ import androidx.compose.ui.input.pointer.onPointerEvent
23
28
import androidx.compose.ui.unit.dp
24
29
import com.squareup.workflow1.traceviewer.model.Node
25
30
import com.squareup.workflow1.traceviewer.util.ParseResult
@@ -69,19 +74,29 @@ internal fun RenderDiagram(
69
74
70
75
if (! isLoading) {
71
76
val previousFrame = if (frameInd > 0 ) fullTree[frameInd - 1 ] else null
72
- DrawTree (fullTree[frameInd], previousFrame, affectedNodes[frameInd], onNodeSelect)
77
+ DrawTree (
78
+ node = fullTree[frameInd],
79
+ previousNode = previousFrame,
80
+ affectedNodes = affectedNodes[frameInd],
81
+ expandedNodes = remember(frameInd) { mutableStateMapOf() },
82
+ onNodeSelect = onNodeSelect,
83
+ )
73
84
}
74
85
}
75
86
76
87
/* *
77
88
* Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree
78
89
* The Column holds a subtree of nodes, and the Row holds all the children of the current node
90
+ *
91
+ * A mutable map is used to persist the expansion state of the nodes, allowing them to be open and
92
+ * closed from user clicks.
79
93
*/
80
94
@Composable
81
95
private fun DrawTree (
82
96
node : Node ,
83
97
previousNode : Node ? ,
84
98
affectedNodes : Set <Node >,
99
+ expandedNodes : MutableMap <String , Boolean >,
85
100
onNodeSelect : (Node , Node ? ) -> Unit ,
86
101
modifier : Modifier = Modifier ,
87
102
) {
@@ -93,21 +108,42 @@ private fun DrawTree(
93
108
horizontalAlignment = Alignment .CenterHorizontally ,
94
109
) {
95
110
val isAffected = affectedNodes.contains(node)
96
- DrawNode (node, previousNode, isAffected, onNodeSelect)
111
+ // By default, nodes that relevant to this specific frame are expanded. All others are closed.
112
+ LaunchedEffect (expandedNodes) {
113
+ expandedNodes[node.id] = isAffected
114
+ }
115
+ val isExpanded = expandedNodes[node.id] == true
116
+
117
+ DrawNode (
118
+ node,
119
+ previousNode,
120
+ isAffected,
121
+ isExpanded,
122
+ onNodeSelect,
123
+ onExpandToggle = { expandedNodes[node.id] = ! expandedNodes[node.id]!! }
124
+ )
97
125
98
126
// Draws the node's children recursively.
99
- Row (
100
- horizontalArrangement = Arrangement .Center ,
101
- verticalAlignment = Alignment .Top
102
- ) {
103
- /*
127
+ if (isExpanded) {
128
+ Row (
129
+ horizontalArrangement = Arrangement .Center ,
130
+ verticalAlignment = Alignment .Top
131
+ ) {
132
+ /*
104
133
We pair up the current node's children with previous frame's children.
105
134
In the edge case that the current frame has additional children compared to the previous
106
135
frame, we replace with null and will check before next recursive call.
107
- */
108
- node.children.forEach { (index, childNode) ->
109
- val prevChildNode = previousNode?.children?.get(index)
110
- DrawTree (childNode, prevChildNode, affectedNodes, onNodeSelect)
136
+ */
137
+ node.children.forEach { (index, childNode) ->
138
+ val prevChildNode = previousNode?.children?.get(index)
139
+ DrawTree (
140
+ node = childNode,
141
+ previousNode = prevChildNode,
142
+ affectedNodes = affectedNodes,
143
+ expandedNodes = expandedNodes,
144
+ onNodeSelect = onNodeSelect
145
+ )
146
+ }
111
147
}
112
148
}
113
149
}
@@ -116,24 +152,38 @@ private fun DrawTree(
116
152
/* *
117
153
* A basic box that represents a workflow node
118
154
*/
155
+ @OptIn(ExperimentalComposeUiApi ::class )
119
156
@Composable
120
157
private fun DrawNode (
121
158
node : Node ,
122
159
previousNode : Node ? ,
123
160
isAffected : Boolean ,
161
+ isExpanded : Boolean ,
124
162
onNodeSelect : (Node , Node ? ) -> Unit ,
163
+ onExpandToggle : (Node ) -> Unit ,
125
164
) {
126
165
Box (
127
166
modifier = Modifier
128
167
.background(if (isAffected) Color .Green else Color .Transparent )
129
- .clickable {
130
- // Selecting a node will bubble back up to the main view to handle the selection
131
- onNodeSelect(node, previousNode)
168
+ .onPointerEvent(PointerEventType .Press ) {
169
+ if (it.buttons.isPrimaryPressed) {
170
+ onNodeSelect(node, previousNode)
171
+ } else if (it.buttons.isSecondaryPressed) {
172
+ onExpandToggle(node)
173
+ }
132
174
}
133
175
.padding(10 .dp)
134
176
) {
135
177
Column (horizontalAlignment = Alignment .CenterHorizontally ) {
136
- Text (text = node.name)
178
+ Row (
179
+ verticalAlignment = Alignment .CenterVertically ,
180
+ horizontalArrangement = Arrangement .spacedBy(4 .dp)
181
+ ) {
182
+ if (node.children.isNotEmpty()) {
183
+ Text (text = if (isExpanded) " ▼" else " ▶" )
184
+ }
185
+ Text (text = node.name)
186
+ }
137
187
Text (text = " ID: ${node.id} " )
138
188
}
139
189
}
0 commit comments