Skip to content

Commit efdaa92

Browse files
committed
Draw workflow nodes through the given classes
The attributes of each node is tightly coupled with the content of the json, so any changes to the JSON will lead to an error unless the WorkflowNode class is also changed accordingly.
1 parent 759e3e4 commit efdaa92

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import androidx.compose.ui.window.singleWindowApplication
44

55
fun main() {
66
singleWindowApplication(title = "Workflow Trace Viewer") {
7-
App()
7+
SandboxBackground { App() }
88
}
99
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.squareup.workflow1.traceviewer
2+
3+
import androidx.compose.foundation.border
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.LaunchedEffect
13+
import androidx.compose.runtime.MutableState
14+
import androidx.compose.runtime.mutableStateOf
15+
import androidx.compose.runtime.remember
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.geometry.Offset
19+
import androidx.compose.ui.graphics.Color
20+
import androidx.compose.ui.layout.onGloballyPositioned
21+
import androidx.compose.ui.layout.positionInRoot
22+
import androidx.compose.ui.unit.dp
23+
import com.squareup.workflow1.components.Arrow
24+
25+
/**
26+
* Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or children workflows,
27+
* a tree structure is most appropriate for representing the data rather than using flat data structures like an array.
28+
*
29+
* TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes)
30+
*/
31+
public data class WorkflowNode (
32+
val id: String,
33+
val name: String,
34+
val children: List<WorkflowNode>
35+
)
36+
37+
/**
38+
* Main access point for drawing the workflow tree. This does 2 tasks:
39+
* 1) places all the nodes
40+
* 2) draws all the arrows accordingly.
41+
* Since arrows cannot be drawn with Canvas without an [Offset], we would need to get place all the
42+
* nodes first, then launch an event to draw the arrows once all nodes have been placed.
43+
*/
44+
@Composable
45+
public fun DrawWorkflowTree(root: WorkflowNode) {
46+
val nodePositions = remember { mutableMapOf<String, Offset>() }
47+
val nodeCount = remember { mutableStateOf(0) }
48+
val nodeMapSize = remember { mutableStateOf(0)}
49+
val readyToDraw = remember { mutableStateOf(false) }
50+
51+
drawTree(root, nodePositions, nodeCount, nodeMapSize)
52+
53+
LaunchedEffect(nodeMapSize.value) {
54+
if (nodePositions.size == nodeCount.value) {
55+
println("all nodes")
56+
readyToDraw.value = true
57+
}
58+
}
59+
60+
if (readyToDraw.value) {
61+
print("arrow")
62+
drawArrows(root, nodePositions)
63+
}
64+
}
65+
66+
/**
67+
* Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree
68+
*
69+
*/
70+
@Composable
71+
private fun drawTree(
72+
node: WorkflowNode,
73+
nodePositions: MutableMap<String, Offset>,
74+
nodeCount: MutableState<Int>,
75+
nodeMapSize: MutableState<Int>
76+
) {
77+
Column(
78+
modifier = Modifier.padding(20.dp),
79+
horizontalAlignment = Alignment.CenterHorizontally,
80+
) {
81+
drawNode(node, nodePositions, nodeMapSize)
82+
nodeCount.value += 1
83+
println(nodeCount.value)
84+
if (node.children.isEmpty()) return@Column
85+
86+
Spacer(modifier = Modifier.padding(30.dp))
87+
Row (
88+
horizontalArrangement = Arrangement.Center,
89+
verticalAlignment = Alignment.Top
90+
) {
91+
node.children.forEach { childNode ->
92+
drawTree (childNode, nodePositions, nodeCount, nodeMapSize)
93+
}
94+
}
95+
}
96+
}
97+
98+
@Composable
99+
private fun drawArrows(
100+
node: WorkflowNode,
101+
nodePositions: MutableMap<String, Offset>
102+
) {
103+
if (node.children.isEmpty()) return
104+
105+
node.children.forEach{ childNode ->
106+
val parentPosition = nodePositions[node.id] ?: Offset.Zero
107+
val childPosition = nodePositions[childNode.id] ?: Offset.Zero
108+
println("from $parentPosition to $childPosition")
109+
110+
// uses custom canvas composable
111+
Arrow(
112+
start = parentPosition,
113+
end = childPosition,
114+
onArrowClick = { /* Handle click if needed */ }
115+
)
116+
117+
drawArrows(childNode, nodePositions)
118+
}
119+
}
120+
121+
/**
122+
* Basic data, for now.
123+
* These can be designed to be clickable and be expanded to show more information.
124+
*/
125+
@Composable
126+
private fun drawNode(
127+
node: WorkflowNode,
128+
nodePositions: MutableMap<String, Offset>,
129+
nodeMapSize: MutableState<Int>
130+
) {
131+
Box (
132+
modifier = Modifier
133+
.border(1.dp, Color.Black)
134+
.padding(10.dp)
135+
.onGloballyPositioned {
136+
val coords = it.positionInRoot()
137+
nodePositions[node.id] = coords
138+
nodeMapSize.value += 1
139+
}
140+
){
141+
Column (horizontalAlignment = Alignment.CenterHorizontally) {
142+
Text(text = node.name)
143+
Text(text = "ID: ${node.id}")
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)