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