Skip to content

Commit d192bab

Browse files
committed
WIP drawing arrows
- Density of displays caused weird errors of how the arrows appear - Scaling and translating the arrow positions was also separate from the movement of the nodes
1 parent 6dbd1ff commit d192bab

File tree

3 files changed

+209
-49
lines changed

3 files changed

+209
-49
lines changed

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

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import androidx.compose.ui.Modifier
1010
import androidx.compose.ui.geometry.Offset
1111
import androidx.compose.ui.graphics.Color
1212
import androidx.compose.ui.graphics.drawscope.DrawScope
13+
import androidx.compose.ui.platform.LocalDensity
14+
import androidx.compose.ui.unit.Density
15+
import androidx.compose.ui.unit.IntOffset
1316
import androidx.compose.ui.unit.dp
1417
import kotlin.math.atan2
1518

@@ -23,6 +26,11 @@ public fun Arrow(
2326
start: Offset,
2427
end: Offset,
2528
) {
29+
println("start before: $start, end before: $end")
30+
val scaledStart = (start as Offset).toDp(LocalDensity.current)
31+
val scaledEnd = (end as Offset).toDp(LocalDensity.current)
32+
println("start after: $scaledStart, end after: $scaledEnd")
33+
2634
Box(
2735
modifier = Modifier
2836
.clickable { println("arrow clicked") }
@@ -42,30 +50,36 @@ public fun Arrow(
4250
}
4351
}
4452

45-
private fun DrawScope.drawArrow(
53+
fun Offset.toDp(density: Density): Offset = with(density) {
54+
Offset(x.toDp().value, y.toDp().value)
55+
}
56+
57+
fun DrawScope.drawArrow(
4658
start: Offset,
4759
end: Offset,
4860
color: Color,
4961
strokeWidth: Float
5062
) {
63+
5164
drawLine(
5265
color = color,
53-
start = start,
54-
end = end,
66+
start = Offset(start.x.dp.toPx(), start.y.dp.toPx()),
67+
end = Offset(end.x.dp.toPx(), end.y.dp.toPx()),
5568
strokeWidth = strokeWidth
5669
)
5770

58-
val arrowHeadSize = 20f
59-
val angle = atan2((end.y - start.y).toDouble(), (end.x - start.x).toDouble()).toFloat()
60-
val arrowPoint1 = Offset(
61-
x = end.x - arrowHeadSize * Math.cos(angle + Math.PI / 6).toFloat(),
62-
y = end.y - arrowHeadSize * Math.sin(angle + Math.PI / 6).toFloat()
63-
)
64-
val arrowPoint2 = Offset(
65-
x = end.x - arrowHeadSize * Math.cos(angle - Math.PI / 6).toFloat(),
66-
y = end.y - arrowHeadSize * Math.sin(angle - Math.PI / 6).toFloat()
67-
)
6871

69-
drawLine(color, end, arrowPoint1, strokeWidth)
70-
drawLine(color, end, arrowPoint2, strokeWidth)
72+
// val arrowHeadSize = 20f
73+
// val angle = atan2((end.y - start.y).toDouble(), (end.x - start.x).toDouble()).toFloat()
74+
// val arrowPoint1 = Offset(
75+
// x = end.x - arrowHeadSize * Math.cos(angle + Math.PI / 6).toFloat(),
76+
// y = end.y - arrowHeadSize * Math.sin(angle + Math.PI / 6).toFloat()
77+
// )
78+
// val arrowPoint2 = Offset(
79+
// x = end.x - arrowHeadSize * Math.cos(angle - Math.PI / 6).toFloat(),
80+
// y = end.y - arrowHeadSize * Math.sin(angle - Math.PI / 6).toFloat()
81+
// )
82+
//
83+
// drawLine(color, end, arrowPoint1, strokeWidth)
84+
// drawLine(color, end, arrowPoint2, strokeWidth)
7185
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ import androidx.compose.ui.input.pointer.pointerInput
2323
*
2424
*/
2525
@Composable
26-
public fun SandboxBackground(content: @Composable () -> Unit) {
26+
public fun SandboxBackground(
27+
content: @Composable (
28+
translationX: Float,
29+
translationY: Float,
30+
scale: Float,
31+
) -> Unit
32+
) {
2733
var scale by remember { mutableStateOf(1f) }
2834
var offset by remember { mutableStateOf(Offset.Zero) }
2935

3036
Box(
3137
modifier = Modifier
32-
.wrapContentSize(unbounded = true, align = Alignment.TopStart) // this allows the content to be larger than the initial screen of the app
38+
.wrapContentSize(
39+
unbounded = true,
40+
align = Alignment.TopStart
41+
) // this allows the content to be larger than the initial screen of the app
3342
.pointerInput(Unit) { // this allows for user's panning to view different parts of content
3443
awaitEachGesture {
3544
val event = awaitPointerEvent()
@@ -66,8 +75,9 @@ public fun SandboxBackground(content: @Composable () -> Unit) {
6675
scaleY = scale
6776
}
6877
) {
69-
Box {
70-
content() // this is main content
71-
}
78+
content(
79+
offset.x, offset.y,
80+
scale
81+
) // this is main content
7282
}
7383
}
Lines changed: 165 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.squareup.workflow1.traceviewer
22

3+
import androidx.compose.desktop.ui.tooling.preview.Preview
4+
import androidx.compose.foundation.Canvas
35
import androidx.compose.foundation.background
46
import androidx.compose.foundation.border
57
import androidx.compose.foundation.clickable
@@ -8,20 +10,31 @@ import androidx.compose.foundation.layout.Box
810
import androidx.compose.foundation.layout.Column
911
import androidx.compose.foundation.layout.Row
1012
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxSize
1114
import androidx.compose.foundation.layout.padding
15+
import androidx.compose.foundation.shape.RoundedCornerShape
16+
import androidx.compose.material.MaterialTheme
1217
import androidx.compose.material.Text
1318
import androidx.compose.runtime.Composable
1419
import androidx.compose.runtime.LaunchedEffect
1520
import androidx.compose.runtime.MutableState
21+
import androidx.compose.runtime.mutableStateMapOf
1622
import androidx.compose.runtime.mutableStateOf
1723
import androidx.compose.runtime.remember
1824
import androidx.compose.ui.Alignment
1925
import androidx.compose.ui.Modifier
2026
import androidx.compose.ui.geometry.Offset
2127
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.graphicsLayer
29+
import androidx.compose.ui.layout.LayoutCoordinates
2230
import androidx.compose.ui.layout.onGloballyPositioned
31+
import androidx.compose.ui.layout.positionInParent
2332
import androidx.compose.ui.layout.positionInRoot
33+
import androidx.compose.ui.layout.positionOnScreen
34+
import androidx.compose.ui.platform.LocalDensity
2435
import androidx.compose.ui.unit.dp
36+
import androidx.compose.ui.util.fastAny
37+
import javax.swing.tree.TreeNode
2538

2639
/**
2740
* Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or children workflows,
@@ -33,7 +46,27 @@ public data class WorkflowNode (
3346
val id: String,
3447
val name: String,
3548
val children: List<WorkflowNode>
36-
)
49+
) {
50+
// fun findParentById(id: String): WorkflowNode? {
51+
// if (this.id == id) return this
52+
// for (child in children) {
53+
// val found = child.findParentById(id)
54+
// if (found != null) return found
55+
// }
56+
// return null
57+
// }
58+
fun findParentForId(id: String): WorkflowNode? {
59+
if (this.id == id) {
60+
return null // This is the root node, so it has no parent
61+
}
62+
if (children.any { it.id == id }) {
63+
return this
64+
}
65+
return children.firstNotNullOfOrNull {
66+
it.findParentForId(id)
67+
}
68+
}
69+
}
3770

3871
/**
3972
* Main access point for drawing the workflow tree. This does 2 tasks:
@@ -43,22 +76,53 @@ public data class WorkflowNode (
4376
* nodes first, then launch an event to draw the arrows once all nodes have been placed.
4477
*/
4578
@Composable
46-
public fun DrawWorkflowTree(root: WorkflowNode) {
79+
public fun DrawWorkflowTree(
80+
root: WorkflowNode,
81+
translationXArg: Float,
82+
translationYArg: Float,
83+
scale: Float,
84+
) {
4785
val nodePositions = remember { mutableMapOf<String, Offset>() }
48-
val nodeCount = remember { mutableStateOf(0) }
49-
val nodeMapSize = remember { mutableStateOf(0)}
50-
val readyToDraw = remember { mutableStateOf(false) }
51-
52-
drawTree(root, nodePositions, nodeCount, nodeMapSize)
86+
// val nodeCount = remember { mutableStateOf(0) }
87+
// val nodeMapSize = remember { mutableStateOf(0)}
88+
// val readyToDraw = remember { mutableStateOf(false) }
89+
Box(modifier = Modifier.fillMaxSize()) {
5390

54-
LaunchedEffect(nodeMapSize.value) {
55-
if (nodePositions.size == nodeCount.value) {
56-
readyToDraw.value = true
91+
drawTree(root, nodePositions
92+
// , nodeCount, nodeMapSize
93+
)
94+
Canvas(modifier = Modifier.fillMaxSize()
95+
// .graphicsLayer {
96+
// translationX = translationXArg
97+
// translationY = translationYArg
98+
// scaleX = scale
99+
// scaleY = scale
100+
// }
101+
) {
102+
nodePositions.forEach { (id, position) ->
103+
val start = position
104+
val end = root.findParentForId(id)?.let { parent -> nodePositions[parent.id] } ?: return@forEach
105+
drawLine(
106+
color = Color.Black,
107+
start = Offset(start.x.dp.toPx(), start.y.dp.toPx()),
108+
end = Offset(end.x.dp.toPx(), end.y.dp.toPx()),
109+
strokeWidth = 2.dp.toPx()
110+
)
111+
}
57112
}
58-
}
113+
// LaunchedEffect(nodeMapSize.value) {
114+
// if (nodePositions.size == nodeCount.value) {
115+
// readyToDraw.value = true
116+
// }
117+
// }
59118

60-
if (readyToDraw.value) {
61-
drawArrows(root, nodePositions)
119+
// if (readyToDraw.value) {
120+
// LaunchedEffect(nodePositions) {
121+
// drawArrows(root, nodePositions)
122+
// drawArrows(root, nodePositions)
123+
//
124+
// }
125+
// }
62126
}
63127
}
64128

@@ -70,24 +134,31 @@ public fun DrawWorkflowTree(root: WorkflowNode) {
70134
private fun drawTree(
71135
node: WorkflowNode,
72136
nodePositions: MutableMap<String, Offset>,
73-
nodeCount: MutableState<Int>,
74-
nodeMapSize: MutableState<Int>
137+
138+
// nodeCount: MutableState<Int>,
139+
// nodeMapSize: MutableState<Int>
75140
) {
141+
76142
Column(
77-
modifier = Modifier.padding(10.dp),
143+
modifier = Modifier.padding(10.dp).border(1.dp,Color.Black),
78144
horizontalAlignment = Alignment.CenterHorizontally,
79145
) {
80-
drawNode(node, nodePositions, nodeMapSize)
81-
nodeCount.value += 1
146+
drawNode(node, nodePositions
147+
// , nodeMapSize
148+
)
149+
// nodeCount.value += 1
82150
if (node.children.isEmpty()) return@Column
83151

84-
Spacer(modifier = Modifier.padding(30.dp))
152+
// Spacer(modifier = Modifier.padding(30.dp))
85153
Row (
86154
horizontalArrangement = Arrangement.Center,
87-
verticalAlignment = Alignment.Top
155+
verticalAlignment = Alignment.Top,
156+
modifier = Modifier.border(1.dp, Color.Black)
88157
) {
89158
node.children.forEach { childNode ->
90-
drawTree (childNode, nodePositions, nodeCount, nodeMapSize)
159+
drawTree (childNode, nodePositions
160+
// , nodeCount, nodeMapSize
161+
)
91162
}
92163
}
93164
}
@@ -101,17 +172,19 @@ private fun drawTree(
101172
private fun drawNode(
102173
node: WorkflowNode,
103174
nodePositions: MutableMap<String, Offset>,
104-
nodeMapSize: MutableState<Int>
175+
// nodeMapSize: MutableState<Int>
105176
) {
177+
val density = LocalDensity.current
178+
106179
val open = remember { mutableStateOf(false) }
107180
Box (
108181
modifier = Modifier
109-
.border(1.dp, Color.Black)
182+
// .border(1.dp, Color.Black)
110183
.clickable { open.value = !open.value }
111184
.onGloballyPositioned {
112185
val coords = it.positionInRoot()
113-
nodePositions[node.id] = coords
114-
nodeMapSize.value += 1
186+
nodePositions[node.id] = with(density) { coords.toDp(density) }
187+
// nodeMapSize.value += 1
115188
}
116189
){
117190
Column (horizontalAlignment = Alignment.CenterHorizontally) {
@@ -129,11 +202,11 @@ private fun drawArrows(
129202
node: WorkflowNode,
130203
nodePositions: MutableMap<String, Offset>
131204
){
132-
if (node.children.isEmpty()) return
133-
205+
// if (node.children.isEmpty()) return
206+
println(nodePositions)
134207
node.children.forEach { childNode ->
135-
val parentPosition = nodePositions[node.id] ?: Offset.Zero
136-
val childPosition = nodePositions[childNode.id] ?: Offset.Zero
208+
val parentPosition = nodePositions[node.id] ?: error("Child must have a position")
209+
val childPosition = nodePositions[childNode.id] ?: error("Child must have a position")
137210

138211
Arrow(
139212
start = parentPosition,
@@ -143,3 +216,66 @@ private fun drawArrows(
143216
drawArrows(childNode, nodePositions)
144217
}
145218
}
219+
@Composable
220+
fun TreeNode(
221+
id: String,
222+
modifier: Modifier = Modifier,
223+
onPositioned: ((LayoutCoordinates) -> Unit)? = null
224+
) {
225+
Box(
226+
modifier = modifier
227+
.padding(8.dp)
228+
// .onGloballyPositioned { coords ->
229+
// onPositioned?.invoke(coords)
230+
// }
231+
.background(Color(0xFFE0F7FA), shape = RoundedCornerShape(8.dp))
232+
.border(1.dp, Color.Black, shape = RoundedCornerShape(8.dp))
233+
.padding(horizontal = 12.dp, vertical = 6.dp)
234+
) {
235+
Text(text = id)
236+
}
237+
}
238+
239+
@Preview
240+
@Composable
241+
fun test() {
242+
val nodePositions = remember { mutableStateMapOf<String, Offset>() }
243+
val density = LocalDensity.current
244+
245+
Box(modifier = Modifier.fillMaxSize()) {
246+
Column {
247+
TreeNode(
248+
id = "A",
249+
modifier = Modifier
250+
.padding(horizontal = 200.dp)
251+
.onGloballyPositioned { coordinates ->
252+
val localOffset = coordinates.positionInParent()
253+
nodePositions["A"] = with(density) { localOffset.toDp(density) }
254+
}
255+
)
256+
257+
TreeNode(
258+
id = "B",
259+
modifier = Modifier
260+
.onGloballyPositioned { coordinates ->
261+
val localOffset = coordinates.positionInParent()
262+
nodePositions["B"] = with(density) { localOffset.toDp(density) }
263+
}
264+
)
265+
}
266+
267+
Canvas(modifier = Modifier.fillMaxSize()) {
268+
val start = nodePositions["A"]
269+
val end = nodePositions["B"]
270+
if (start != null && end != null) {
271+
// Convert dp to px inside the DrawScope
272+
drawLine(
273+
color = Color.Black,
274+
start = Offset(start.x.dp.toPx(), start.y.dp.toPx()),
275+
end = Offset(end.x.dp.toPx(), end.y.dp.toPx()),
276+
strokeWidth = 2.dp.toPx()
277+
)
278+
}
279+
}
280+
}
281+
}

0 commit comments

Comments
 (0)