-
Notifications
You must be signed in to change notification settings - Fork 104
Workflow visualizer prototype #1335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
dceb9e4
759e3e4
efdaa92
56de1f6
f2ec953
6dbd1ff
d192bab
46f2f20
e964c42
97f8e77
e947472
35567b1
761c987
e212736
03d7784
0f8e334
a1099dc
f9a64ff
e36ca77
a778895
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import androidx.compose.foundation.Canvas | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.size | ||
import androidx.compose.material.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.drawscope.DrawScope | ||
import androidx.compose.ui.unit.dp | ||
import kotlin.math.atan2 | ||
|
||
/** | ||
* Since we eventually would want the ability to click into each arrow between nodes and see what | ||
* state/props are being passed, having a custom composable for the arrow where an onArrowClick | ||
* could be set will be helpful. | ||
*/ | ||
@Composable | ||
public fun Arrow( | ||
start: Offset, | ||
end: Offset, | ||
) { | ||
Box( | ||
modifier = Modifier | ||
.size(20.dp) | ||
.clickable { print("clicked") } | ||
) { | ||
Text("top left corner") | ||
Canvas (modifier = Modifier.fillMaxSize()){ | ||
drawArrow( | ||
start = start, | ||
end = end, | ||
color = Color.Black, | ||
strokeWidth = 2f | ||
) | ||
} | ||
} | ||
} | ||
|
||
// iterative | ||
// @Composable | ||
// public fun drawAllArrows( | ||
// arrowLocations: List<Pair<NodePosition, NodePosition>>, | ||
// ) { | ||
// Canvas(modifier = Modifier.fillMaxSize()) { | ||
// arrowLocations.forEach { (start, end) -> | ||
// drawArrow( | ||
// start = start.position, | ||
// end = end.position, | ||
// color = Color.Black, | ||
// strokeWidth = 2f | ||
// ) | ||
// } | ||
// } | ||
// } | ||
|
||
|
||
private fun DrawScope.drawArrow( | ||
start: Offset, | ||
end: Offset, | ||
color: Color, | ||
strokeWidth: Float | ||
) { | ||
drawLine( | ||
color = color, | ||
start = start, | ||
end = end, | ||
strokeWidth = strokeWidth | ||
) | ||
|
||
val arrowHeadSize = 20f | ||
val angle = atan2((end.y - start.y).toDouble(), (end.x - start.x).toDouble()).toFloat() | ||
val arrowPoint1 = Offset( | ||
x = end.x - arrowHeadSize * Math.cos(angle + Math.PI / 6).toFloat(), | ||
y = end.y - arrowHeadSize * Math.sin(angle + Math.PI / 6).toFloat() | ||
) | ||
val arrowPoint2 = Offset( | ||
x = end.x - arrowHeadSize * Math.cos(angle - Math.PI / 6).toFloat(), | ||
y = end.y - arrowHeadSize * Math.sin(angle - Math.PI / 6).toFloat() | ||
) | ||
|
||
drawLine(color, end, arrowPoint1, strokeWidth) | ||
drawLine(color, end, arrowPoint2, strokeWidth) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import androidx.compose.foundation.gestures.awaitEachGesture | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.wrapContentSize | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.geometry.Offset | ||
import androidx.compose.ui.graphics.graphicsLayer | ||
import androidx.compose.ui.input.pointer.PointerEventType | ||
import androidx.compose.ui.input.pointer.pointerInput | ||
|
||
/** | ||
* This is the backdrop for the whole app. Since there can be hundreds of modules at a time, there | ||
* is not realistic way to fit everything on the screen at once. Having the liberty to pan across | ||
* the whole tree as well as zoom into specific subtrees means there's a lot more control when | ||
* analyzing the traces. | ||
* | ||
*/ | ||
@Composable | ||
public fun SandboxBackground(content: @Composable () -> Unit) { | ||
var scale by remember { mutableStateOf(1f) } | ||
var offset by remember { mutableStateOf(Offset.Zero) } | ||
|
||
Box( | ||
modifier = Modifier | ||
.wrapContentSize(unbounded = true) // this allows the content to be larger than the initial screen of the app | ||
.pointerInput(Unit) { // this allows for user's panning to view different parts of content | ||
awaitEachGesture { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this from a blog post or ref article or something? If so, it can be valuable to post the link in a comment as then we can follow up during maintenance. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added note that code is AI generated. |
||
val event = awaitPointerEvent() | ||
|
||
// zooming | ||
if (event.type == PointerEventType.Scroll) { | ||
val scrollDelta = event.changes.first().scrollDelta.y | ||
scale *= if (scrollDelta < 0) 1.1f else 0.9f | ||
scale = scale.coerceIn(0.1f, 10f) | ||
event.changes.forEach { it.consume() } | ||
} | ||
|
||
// panning: this tracks multiple events within one gesture to see what the user is doing, then calculates the offset and pans the screen accordingly | ||
val drag = event.changes.firstOrNull() | ||
if (drag != null && drag.pressed) { | ||
var prev = drag.position | ||
while (true) { | ||
val nextEvent = awaitPointerEvent() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is no way to get stuck here right? As in, is it possible we already captured event.changes but those were all of them and there is nothign to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed functionality with higher-level |
||
val nextDrag = nextEvent.changes.firstOrNull() ?: break | ||
if (!nextDrag.pressed) break | ||
|
||
val delta = nextDrag.position - prev | ||
offset += delta | ||
prev = nextDrag.position | ||
nextDrag.consume() | ||
} | ||
} | ||
} | ||
} | ||
.graphicsLayer { | ||
translationX = offset.x | ||
translationY = offset.y | ||
scaleX = scale | ||
scaleY = scale | ||
} | ||
) { | ||
Box { | ||
content() // this is main content | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import com.squareup.moshi.JsonDataException | ||
import com.squareup.moshi.Moshi | ||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory | ||
import java.io.IOException | ||
|
||
/** | ||
* Parses a JSON string into [WorkflowNode] with Moshi adapters | ||
* All the caught exceptions should be handled by the caller, and appropriate UI feedback should be provided to user | ||
* | ||
*/ | ||
public fun FetchRoot(json: String): WorkflowNode? { | ||
return try{ | ||
val moshi = Moshi.Builder() | ||
.add(KotlinJsonAdapterFactory()) | ||
.build() | ||
val workflowAdapter = moshi.adapter(WorkflowNode::class.java) | ||
val root = workflowAdapter.fromJson(json) | ||
root | ||
} catch (e: JsonDataException) { | ||
throw JsonDataException("Failed to parse JSON: ${e.message}", e) | ||
} catch (e: IOException){ | ||
throw IOException("Malformed JSON: ${e.message}", e) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think getClass().getResource should work here