Skip to content

Improve visualizer usability #1382

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

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e3b81f3
Complete reset when changing trace files
wenli-cai Jul 21, 2025
cbcea20
Fix naming for node current and past state
wenli-cai Jul 22, 2025
9c1fc6a
Enumerate the specific changes for each node and color them accordingly
wenli-cai Jul 22, 2025
25d04a4
Move away from using reflection for accessing node fields
wenli-cai Jul 22, 2025
1ce9943
Change node data being visualized
wenli-cai Jul 22, 2025
b5d3b16
Remove unnecessary frame tracking in main composable
wenli-cai Jul 22, 2025
6dddf94
Add legend for the colors used in the frame
wenli-cai Jul 22, 2025
6d2d31b
Group unaffected and affected children separately
wenli-cai Jul 24, 2025
e71a870
Separate composable components and change workflow UI pattern
wenli-cai Jul 24, 2025
18c3b6f
Don't apply simple/nested pattern for unaffected children group
wenli-cai Jul 24, 2025
5646d10
Add file dump functionality from live tracing mode
wenli-cai Jul 25, 2025
18f82cd
Fix NPE when enabling live view without a connected device
japplin Jul 24, 2025
07ab5fd
Enable compose hot reload
japplin Jul 25, 2025
58c58a9
Change sample/tutorial module's agp version
wenli-cai Jul 25, 2025
2aaa120
WIP text diff
wenli-cai Jul 25, 2025
5ec8a18
Add text-diff functionality
wenli-cai Jul 26, 2025
82eae8b
Fix compose violations
wenli-cai Jul 26, 2025
1633f4c
Merge branch 'main' into wenli/improve-visualizer
wenli-cai Jul 26, 2025
1e1da90
Apply changes from dependencyGuardBaseline --refresh-dependencies
wenli-cai Jul 26, 2025
8fbeab4
Apply changes from artifactsDump
workflow-pr-fixer[bot] Jul 26, 2025
a50f9a5
Fix merge bugs
wenli-cai Jul 26, 2025
c4fe05e
Allow device selection
wenli-cai Jul 26, 2025
6687f04
Manual api dump
wenli-cai Jul 26, 2025
cdfbc82
Remove compose hot reload changes
wenli-cai Jul 28, 2025
5d3e3f4
Apply changes from dependencyGuardBaseline --refresh-dependencies
wenli-cai Jul 28, 2025
fdf2253
Apply changes from apiDump
workflow-pr-fixer[bot] Jul 28, 2025
5280f84
Apply changes from artifactsDump
workflow-pr-fixer[bot] Jul 28, 2025
a2c98ae
Clean up
wenli-cai Jul 28, 2025
325b239
Add search box for available nodes in the specified frame
wenli-cai Jul 29, 2025
688688e
Refactor directories
wenli-cai Jul 29, 2025
24fae61
Improve error handling
wenli-cai Jul 29, 2025
cc40120
Make app window snap to the specific node when it is being searched.
wenli-cai Jul 30, 2025
a9a44e9
Fix zooming for sandbox
wenli-cai Jul 30, 2025
4f9f92b
Replace color legend with tooltip display on node hover
wenli-cai Aug 1, 2025
418abf1
New frame selector format
wenli-cai Aug 4, 2025
f2b5169
Fill background of info panel arrow
wenli-cai Aug 5, 2025
7b7b41a
Usage guide gifs
wenli-cai Aug 5, 2025
a489f00
Improve readme
wenli-cai Aug 5, 2025
03c505d
Fix reset state
wenli-cai Aug 12, 2025
08f412c
Cleanup code for review
wenli-cai Aug 12, 2025
543258b
Revert dependencies classpath
wenli-cai Aug 14, 2025
4a8d29d
Add remaining internal modifiers
wenli-cai Aug 14, 2025
ea48f89
Fix PR comments
wenli-cai Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
// Hardcoded as this is upstream of the version catalog. Keep this in sync with that.
kotlin("jvm") version "2.0.21" apply false
kotlin("jvm") version "2.2.0" apply false
}

dependencyResolutionManagement {
Expand Down
13 changes: 7 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[versions]

agpVersion = "8.8.0"
agpVersion = "8.11.0"

compileSdk = "34"
minSdk = "24"
targetSdk = "33"

jdk-target = "1.8"
jdk-target = "11"
jdk-toolchain = "17"

androidx-activity = "1.8.2"
Expand Down Expand Up @@ -46,19 +46,19 @@ filekit-dialogs-compose = "0.10.0-beta03"

google-accompanist = "0.18.0"
google-dagger = "2.40.5"
google-ksp = "2.0.21-1.0.26"
google-ksp = "2.2.0-2.0.2"
google-material = "1.4.0"

groovy = "3.0.9"
jUnit = "4.13.2"
java-diff-utils = "4.12"
javaParser = "3.24.0"
jetbrains-compose-plugin = "1.7.3"
jetbrains-compose-plugin = "1.8.2"
kgx = "0.1.12"
kotest = "5.1.0"
# Keep this in sync with what is hard-coded in build-logic/settings.gradle.kts as that is upstream
# of loading the library versions from this file but should be the same.
kotlin = "2.0.21"
kotlin = "2.2.0"

kotlinx-binary-compatibility = "0.17.0"
kotlinx-coroutines = "1.7.3"
Expand Down Expand Up @@ -95,9 +95,10 @@ timber = "5.0.1"
truth = "1.4.4"
turbine = "1.0.0"
vanniktech-publish = "0.32.0"
compose-hot-reload = "1.0.0-beta02"

[plugins]

compose-hot-reload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
2 changes: 1 addition & 1 deletion samples/tutorial/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {

deps = [
activityktx: 'androidx.activity:activity-ktx:1.3.0',
agp: "com.android.tools.build:gradle:8.8.0",
agp: "com.android.tools.build:gradle:8.11.0",
appcompat: 'androidx.appcompat:appcompat:1.3.1',
constraintlayout: 'androidx.constraintlayout:constraintlayout:2.0.1',
kotlin: [
Expand Down
10 changes: 9 additions & 1 deletion workflow-trace-viewer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.reload.gradle.ComposeHotRun

plugins {
id("kotlin-multiplatform")
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.compose.hot.reload)
}

kotlin {
Expand All @@ -25,6 +27,7 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation(libs.squareup.moshi.kotlin)
implementation(libs.filekit.dialogs.compose)
implementation(libs.java.diff.utils)
}
}
jvmTest {
Expand All @@ -35,7 +38,12 @@ kotlin {
}
}
}

tasks.withType<ComposeHotRun>().configureEach {
mainClass.set("com.squareup.workflow1.traceviewer.MainKt")
jvmArgs(
"-Dapple.awt.application.appearance=system"
)
}
compose {
desktop {
application {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.model.NodeUpdate
import com.squareup.workflow1.traceviewer.ui.ColorLegend
import com.squareup.workflow1.traceviewer.ui.FrameSelectTab
import com.squareup.workflow1.traceviewer.ui.RenderTrace
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
import com.squareup.workflow1.traceviewer.ui.TraceModeToggleSwitch
import com.squareup.workflow1.traceviewer.util.FileDump
import com.squareup.workflow1.traceviewer.util.SandboxBackground
import com.squareup.workflow1.traceviewer.util.SocketClient
import com.squareup.workflow1.traceviewer.util.UploadFile
Expand All @@ -33,13 +33,15 @@ internal fun App(
modifier: Modifier = Modifier
) {
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
val workflowFrames = remember { mutableStateListOf<Node>() }
var frameSize by remember { mutableIntStateOf(0) }
var rawRenderPass by remember { mutableStateOf("") }
var frameIndex by remember { mutableIntStateOf(0) }
val sandboxState = remember { SandboxState() }

// Default to File mode, and can be toggled to be in Live mode.
var traceMode by remember { mutableStateOf<TraceMode>(TraceMode.File(null)) }
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
var active by remember(traceMode) { mutableStateOf(false) }
val socket = remember { SocketClient() }

Runtime.getRuntime().addShutdownHook(
Expand All @@ -62,7 +64,7 @@ internal fun App(
selectedTraceFile = null
selectedNode = null
frameIndex = 0
workflowFrames.clear()
frameSize = 0
}

// Main content
Expand All @@ -74,31 +76,25 @@ internal fun App(
val readyForLiveTrace = traceMode is TraceMode.Live

if (readyForFileTrace || readyForLiveTrace) {
active = true
RenderTrace(
traceSource = traceMode,
frameInd = frameIndex,
onFileParse = { workflowFrames.addAll(it) },
onNodeSelect = { node, prevNode ->
selectedNode = NodeUpdate(node, prevNode)
},
onNewFrame = { frameIndex += 1 }
onFileParse = { frameSize += it },
onNodeSelect = { selectedNode = it },
onNewFrame = { frameIndex += 1 },
onNewData = { rawRenderPass += "$it," },
)
}
}

FrameSelectTab(
frames = workflowFrames,
size = frameSize,
currentIndex = frameIndex,
onIndexChange = { frameIndex = it },
modifier = Modifier.align(Alignment.TopCenter)
)

RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier
.align(Alignment.TopEnd)
)

TraceModeToggleSwitch(
onToggle = {
resetStates()
Expand Down Expand Up @@ -131,6 +127,25 @@ internal fun App(
modifier = Modifier.align(Alignment.BottomStart)
)
}

if (traceMode is TraceMode.Live) {
FileDump(
trace = rawRenderPass,
modifier = Modifier.align(Alignment.BottomStart)
)
}

if (active) {
ColorLegend(
modifier = Modifier.align(Alignment.BottomEnd)
)
}

RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier
.align(Alignment.TopEnd)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ internal data class Node(
override fun hashCode(): Int {
return id.hashCode()
}

companion object {
fun getNodeFields(): List<String> {
return listOf("Props", "State", "Rendering")
}

fun getNodeData(node: Node, field: String): String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be more idiomatic for this to be an extension fun on Node.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this because I'm interacting directly with a Node object?

return when (field.lowercase()) {
"props" -> node.props
"state" -> node.state
"rendering" -> node.rendering
else -> throw IllegalArgumentException("Unknown field: $field")
}
}
}
}

internal fun Node.addChild(child: Node): Node {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
package com.squareup.workflow1.traceviewer.model

import androidx.compose.ui.graphics.Color

/**
* Represents the difference between the current and previous state of a node in the workflow trace.
* This will be what is passed as a state between UI to display the diff.
* This will be what is passed as a state between UI to display the diff. The states all have an
* associated color
*
* If it's the first node in the frame, [previous] will be null and there is no difference to show.
* If it's the first node in the frame, [past] will be null and there is no difference to show.
*/
internal class NodeUpdate(
internal data class NodeUpdate(
val current: Node,
val previous: Node?,
)
val past: Node?,
val state: NodeState
) {
companion object {
fun create(current: Node, past: Node?, isAffected: Boolean): NodeUpdate {
val state = when {
!isAffected -> NodeState.UNCHANGED
past == null -> NodeState.NEW
current.props != past.props -> NodeState.PROPS_CHANGED
current.state != past.state -> NodeState.STATE_CHANGED
else -> NodeState.CHILDREN_CHANGED
}

return NodeUpdate(current, past, state)
}
}
}

enum class NodeState(val color: Color) {
NEW(Color(0x804CAF50)), // green
STATE_CHANGED(Color(0xFFE57373)), // red
PROPS_CHANGED(Color(0xFFFF8A65)), // orange
CHILDREN_CHANGED(Color(0x802196F3)), // blue
UNCHANGED(Color.Transparent),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.squareup.workflow1.traceviewer.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import com.squareup.workflow1.traceviewer.model.NodeState

/**
* Simple UI displaying the color legend for the different node states in the trace
*/
@Composable
fun ColorLegend(
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(8.dp)
) {
NodeState.entries.forEach { state ->
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 2.dp)
) {
Box(
modifier = Modifier
.size(16.dp)
.background(
color = state.color,
)
.then(
if (state.color == Color.Transparent) {
modifier.border(1.dp, Color.Gray)
} else {
modifier
}
)
)
Text(
text = state.name,
fontStyle = FontStyle.Italic,
)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import com.squareup.workflow1.traceviewer.model.Node
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

Expand All @@ -26,7 +25,7 @@ import kotlinx.coroutines.launch
*/
@Composable
internal fun FrameSelectTab(
frames: List<Node>,
size: Int,
currentIndex: Int,
onIndexChange: (Int) -> Unit,
modifier: Modifier = Modifier
Expand All @@ -36,7 +35,7 @@ internal fun FrameSelectTab(
if (currentIndex < 0) return@LaunchedEffect
lazyListState.animateScrollToItem(currentIndex)
}

Surface(
modifier = modifier,
color = Color.White,
Expand All @@ -61,7 +60,7 @@ internal fun FrameSelectTab(
}
},
) {
items(frames.size) { index ->
items(size) { index ->
Text(
text = "Frame ${index + 1}",
color = if (index == currentIndex) Color.Black else Color.LightGray,
Expand Down
Loading