From adbd15385ccbc0e69f52283f393ad92ba75e459b Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Wed, 27 Aug 2025 16:24:59 -0400 Subject: [PATCH 1/9] Open selected files in a new window --- .../{App.kt => FileTraceViewer.kt} | 83 ++------------ .../workflow1/traceviewer/LandingWindow.kt | 104 ++++++++++++++++++ .../squareup/workflow1/traceviewer/Main.kt | 77 ++++++++++++- .../traceviewer/ui/control/DisplayDevices.kt | 6 +- .../ui/control/TraceModeToggleSwitch.kt | 48 -------- .../traceviewer/ui/control/UploadFile.kt | 6 +- 6 files changed, 195 insertions(+), 129 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{App.kt => FileTraceViewer.kt} (69%) create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt delete mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/TraceModeToggleSwitch.kt diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt similarity index 69% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt index 1a512d35dd..c7b6f889c1 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt @@ -2,7 +2,7 @@ package com.squareup.workflow1.traceviewer 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.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -25,12 +25,9 @@ import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.model.NodeUpdate import com.squareup.workflow1.traceviewer.ui.RightInfoPanel -import com.squareup.workflow1.traceviewer.ui.control.DisplayDevices import com.squareup.workflow1.traceviewer.ui.control.FileDump import com.squareup.workflow1.traceviewer.ui.control.FrameNavigator import com.squareup.workflow1.traceviewer.ui.control.SearchBox -import com.squareup.workflow1.traceviewer.ui.control.TraceModeToggleSwitch -import com.squareup.workflow1.traceviewer.ui.control.UploadFile import com.squareup.workflow1.traceviewer.util.SandboxBackground import com.squareup.workflow1.traceviewer.util.parser.RenderTrace import io.github.vinceglb.filekit.PlatformFile @@ -39,21 +36,20 @@ import io.github.vinceglb.filekit.PlatformFile * Main composable that provides the different layers of UI. */ @Composable -internal fun App( - modifier: Modifier = Modifier +internal fun TraceViewerWindow( + modifier: Modifier = Modifier, + traceMode: TraceMode, ) { var appWindowSize by remember { mutableStateOf(IntSize(0, 0)) } var selectedNode by remember { mutableStateOf(null) } var frameSize by remember { mutableIntStateOf(0) } var rawRenderPass by remember { mutableStateOf("") } - var frameIndex by remember { mutableIntStateOf(0) } + var frameIndex by remember { mutableIntStateOf(if (traceMode is TraceMode.Live) -1 else 0) } val sandboxState = remember { SandboxState() } val nodeLocations = remember { mutableStateListOf>() } // Default to File mode, and can be toggled to be in Live mode. var active by remember { mutableStateOf(false) } - var traceMode by remember { mutableStateOf(TraceMode.File(null)) } - var selectedTraceFile by remember { mutableStateOf(null) } // frameIndex is set to -1 when app is in Live Mode, so we increment it by one to avoid off-by-one errors val frameInd = if (traceMode is TraceMode.Live) frameIndex + 1 else frameIndex @@ -68,15 +64,6 @@ internal fun App( appWindowSize = it } ) { - fun resetStates() { - selectedTraceFile = null - selectedNode = null - frameIndex = 0 - frameSize = 0 - rawRenderPass = "" - active = false - nodeLocations.clear() - } // Main content SandboxBackground( @@ -101,12 +88,12 @@ internal fun App( } } - Column( + Row( modifier = Modifier .align(Alignment.TopCenter) .padding(top = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, ) { if (active) { // Frames that appear in composition may not happen sequentially, so when the current frame @@ -141,50 +128,10 @@ internal fun App( ) } } - - TraceModeToggleSwitch( - onToggle = { - resetStates() - traceMode = if (traceMode is TraceMode.Live) { - frameIndex = 0 - TraceMode.File(null) - } else { - /* - We set the frame to -1 here since we always increment it during Live mode as the list of - frames get populated, so we avoid off by one when indexing into the frames. - */ - frameIndex = -1 - TraceMode.Live() - } - }, - traceMode = traceMode, - modifier = Modifier.align(Alignment.BottomCenter) - ) - - // The states are reset when a new file is selected. - if (traceMode is TraceMode.File) { - UploadFile( - resetOnFileSelect = { - resetStates() - selectedTraceFile = it - traceMode = TraceMode.File(it) - }, - modifier = Modifier.align(Alignment.BottomStart) - ) - } - - if (traceMode is TraceMode.Live && (traceMode as TraceMode.Live).device == null) { - DisplayDevices( - onDeviceSelect = { selectedDevice -> - traceMode = TraceMode.Live(selectedDevice) - }, - devices = listDevices(), - modifier = Modifier.align(Alignment.Center) - ) - + if (traceMode is TraceMode.Live) { FileDump( trace = rawRenderPass, - modifier = Modifier.align(Alignment.BottomStart) + modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp) ) } @@ -218,13 +165,3 @@ internal sealed interface TraceMode { } } } - -/** - * Allows users to select from multiple devices that are currently running. - */ -private fun listDevices(): List { - val process = ProcessBuilder("adb", "devices", "-l").start() - process.waitFor() - // We drop the header "List of devices attached" - return process.inputStream.bufferedReader().readLines().drop(1).dropLast(1) -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt new file mode 100644 index 0000000000..31d48ecf57 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt @@ -0,0 +1,104 @@ +package com.squareup.workflow1.traceviewer + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.squareup.workflow1.traceviewer.ui.control.DisplayDevices +import com.squareup.workflow1.traceviewer.ui.control.UploadFile +import io.github.vinceglb.filekit.PlatformFile +import kotlinx.coroutines.delay + +/** + * Main window composable that shows both file upload and device selection options. + */ +@Composable +internal fun LandingWindow( + modifier: Modifier = Modifier, + onFileSelected: (PlatformFile) -> Unit, + onDeviceSelected: (String) -> Unit +) { + val devices by produceState(initialValue = listDevices()) { + while (true) { + delay(3000L) + value = listDevices() + } + } + + Box(modifier = modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Workflow Trace Viewer", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 48.dp) + ) + + // File selection section + UploadFile( + resetOnFileSelect = { file -> + file?.let { onFileSelected(it) } + } + ) + + Text( + text = "— OR —", + fontSize = 14.sp, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), + modifier = Modifier.padding(bottom = 24.dp) + ) + + // Device selection section + Text( + text = "Connect to Device", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 24.dp) + ) + + DisplayDevices( + onDeviceSelect = onDeviceSelected, + devices = devices + ) + } + } +} + +/** + * Allows users to select from multiple devices that are currently running. + */ +private fun listDevices(): List { + val process = ProcessBuilder("adb", "devices", "-l").start() + process.waitFor() + // We drop the header "List of devices attached" + val devices = process.inputStream.use { + it.bufferedReader().readLines().drop(1).dropLast(1) + } + return devices.map { device -> + val deviceId = device.split(' ').first() + val deviceName = ProcessBuilder("adb", "-s", deviceId, "emu", "avd", "name").start() + deviceName.waitFor() + "$deviceId " + deviceName.inputStream.use { it.bufferedReader().readLines().first() } + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt index 17954f90ca..79f78fbf73 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt @@ -1,14 +1,85 @@ package com.squareup.workflow1.traceviewer import androidx.compose.foundation.layout.fillMaxSize +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.window.singleWindowApplication +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.name +import kotlin.system.exitProcess /** * Main entry point for the desktop application, see [README.md] for more details. */ fun main() { - singleWindowApplication(title = "Workflow Trace Viewer", exitProcessOnExit = false) { - App(Modifier.fillMaxSize()) + application { + var openWindows by remember { mutableStateOf(setOf()) } + var isLandingWindowOpen by remember { mutableStateOf(true) } + + // Main window - always visible + if (isLandingWindowOpen) { + Window( + onCloseRequest = { + if (openWindows.isEmpty()) { + exitProcess(0) + } + isLandingWindowOpen = false + }, + title = "Workflow Trace Viewer", + state = rememberWindowState() + ) { + LandingWindow( + modifier = Modifier.fillMaxSize(), + onFileSelected = { file -> + openWindows = openWindows + TraceWindow.FileWindow(file) + }, + onDeviceSelected = { device -> + openWindows = openWindows + TraceWindow.DeviceWindow(device) + } + ) + } + } + + // Additional windows for each opened trace + for (window in openWindows) { + Window( + onCloseRequest = { + openWindows = openWindows - window + if (!isLandingWindowOpen && openWindows.isEmpty()) { + exitProcess(0) + } + }, + title = when (window) { + is TraceWindow.FileWindow -> window.file.name + is TraceWindow.DeviceWindow -> "Live: ${window.device}" + }, + state = rememberWindowState() + ) { + when (window) { + is TraceWindow.FileWindow -> { + TraceViewerWindow( + modifier = Modifier.fillMaxSize(), + traceMode = TraceMode.File(window.file), + ) + } + is TraceWindow.DeviceWindow -> { + TraceViewerWindow( + modifier = Modifier.fillMaxSize(), + traceMode = TraceMode.Live(window.device), + ) + } + } + } + } } } + +sealed interface TraceWindow { + data class FileWindow(val file: PlatformFile) : TraceWindow + data class DeviceWindow(val device: String) : TraceWindow +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt index b28d0fb50d..ada200b435 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt @@ -3,6 +3,7 @@ package com.squareup.workflow1.traceviewer.ui.control import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -52,12 +53,13 @@ internal fun DisplayDevices( }, shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Gray), - modifier = Modifier.padding(4.dp), + modifier = Modifier.padding(4.dp).defaultMinSize(minWidth = 500.dp), elevation = 2.dp ) { Text( text = device, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + modifier = Modifier.align(Alignment.CenterHorizontally) + .padding(horizontal = 16.dp, vertical = 8.dp) ) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/TraceModeToggleSwitch.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/TraceModeToggleSwitch.kt deleted file mode 100644 index e2e101294d..0000000000 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/TraceModeToggleSwitch.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.squareup.workflow1.traceviewer.ui.control - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Switch -import androidx.compose.material.SwitchDefaults -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 androidx.compose.ui.unit.sp -import com.squareup.workflow1.traceviewer.TraceMode - -@Composable -internal fun TraceModeToggleSwitch( - onToggle: () -> Unit, - traceMode: TraceMode, - modifier: Modifier = Modifier -) { - Column( - modifier = modifier.padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Switch( - checked = traceMode is TraceMode.Live, - onCheckedChange = { - onToggle() - }, - colors = SwitchDefaults.colors( - checkedThumbColor = Color.Black, - checkedTrackColor = Color.Black, - ) - ) - - Text( - text = if (traceMode is TraceMode.Live) { - "Live Mode" - } else { - "File Mode" - }, - fontSize = 12.sp, - fontStyle = FontStyle.Italic - ) - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/UploadFile.kt index 725347f366..70541372c3 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/UploadFile.kt @@ -37,10 +37,10 @@ internal fun UploadFile( colors = buttonColors(Color.Black) ) { Text( - text = "+", + text = "Open Trace File", color = Color.White, - fontSize = 24.sp, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + fontSize = 14.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.Medium ) } } From 5cd6afcb36dc7d34f0123413ad04098e5d974e1f Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Wed, 27 Aug 2025 19:03:24 -0400 Subject: [PATCH 2/9] Follow workflow versioning, drop snapshot name for compatability --- workflow-trace-viewer/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 6bfd41a3ef..c4912c7235 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -50,7 +50,7 @@ compose { includeAllModules = true targetFormats(TargetFormat.Dmg) packageName = "Workflow Trace Viewer" - packageVersion = "1.0.0" + packageVersion = (property("VERSION_NAME") as String).substringBefore("-SNAPSHOT") macOS { bundleID = "com.squareup.workflow1.traceviewer" } From f8c8a5bbdfd9dd0dc3d3746dc6aaddb6d6272c77 Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Wed, 27 Aug 2025 21:35:43 -0400 Subject: [PATCH 3/9] Support adb commands when running from a dmg installation --- .../workflow1/traceviewer/LandingWindow.kt | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt index 31d48ecf57..2f5f7d3dd8 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt @@ -8,12 +8,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -89,16 +85,40 @@ internal fun LandingWindow( * Allows users to select from multiple devices that are currently running. */ private fun listDevices(): List { - val process = ProcessBuilder("adb", "devices", "-l").start() + if (adb == null) return emptyList() + val process = ProcessBuilder(adb, "devices", "-l").start() process.waitFor() // We drop the header "List of devices attached" val devices = process.inputStream.use { it.bufferedReader().readLines().drop(1).dropLast(1) } - return devices.map { device -> - val deviceId = device.split(' ').first() - val deviceName = ProcessBuilder("adb", "-s", deviceId, "emu", "avd", "name").start() - deviceName.waitFor() - "$deviceId " + deviceName.inputStream.use { it.bufferedReader().readLines().first() } + + return devices.mapNotNull { device -> + if (device.isBlank()) return@mapNotNull null + val deviceId = device.split(' ').first() + val deviceName = ProcessBuilder(adb, "-s", deviceId, "emu", "avd", "name").start() + deviceName.waitFor() + "$deviceId " + deviceName.inputStream.use { + it.bufferedReader().readLines().firstOrNull() ?: "" + } + } +} + +val adb: String? by lazy { + listOfNotNull( + System.getenv("ANDROID_HOME")?.let { "$it/platform-tools/adb"}, + // Common macOS Android SDK locations + "${System.getProperty("user.home")}/Library/Android/sdk/platform-tools/adb", + "/Users/${System.getProperty("user.name")}/Library/Android/sdk/platform-tools/adb", + ).firstOrNull { path -> + try { + val process = ProcessBuilder(path, "version").start() + if (process.waitFor() == 0) { + return@firstOrNull true + } + } catch (e: Exception) { + println(e) + } + return@firstOrNull false } } From 405ee05a32f871d698353fa603e26573c5531bc9 Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Tue, 2 Sep 2025 16:57:29 -0400 Subject: [PATCH 4/9] Use telephoto library for zooming --- gradle/libs.versions.toml | 6 +++- workflow-trace-viewer/build.gradle.kts | 10 +++++++ .../workflow1/traceviewer/FileTraceViewer.kt | 2 -- .../traceviewer/util/SandboxBackground.kt | 29 +++++++------------ 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 485f8baf3b..d9a95d9e4d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,6 +92,8 @@ squareup-retrofit = "2.9.0" squareup-seismic = "1.0.3" squareup-workflow = "1.0.0" +skiko = "0.9.4" +telephoto = "0.16.0" timber = "5.0.1" truth = "1.4.4" turbine = "1.0.0" @@ -203,7 +205,7 @@ google-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin hamcrest = "org.hamcrest:hamcrest-core:2.2" java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "java-diff-utils" } - +telephoto = { module = "me.saket.telephoto:zoomable", version.ref = "telephoto" } jetbrains-annotations = "org.jetbrains:annotations:24.0.1" junit = { module = "junit:junit", version.ref = "jUnit" } @@ -251,6 +253,8 @@ robolectric-annotations = { module = "org.robolectric:annotations", version.ref rxjava2-rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxjava2-android" } rxjava2-rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava2-core" } +skiko = { module = "org.jetbrains.skiko:skiko-awt-runtime-macos-arm64", version.ref = "skiko" } + squareup-curtains = { module = "com.squareup.curtains:curtains", version.ref = "squareup-curtains" } squareup-cycler = { module = "com.squareup.cycler:cycler", version.ref = "squareup-cycler" } diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index c4912c7235..3d5e50cf98 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -1,4 +1,6 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("kotlin-multiplatform") @@ -27,6 +29,8 @@ kotlin { implementation(libs.squareup.moshi.kotlin) implementation(libs.filekit.dialogs.compose) implementation(libs.java.diff.utils) + implementation(libs.telephoto) + implementation(libs.skiko) } } jvmTest { @@ -62,3 +66,9 @@ compose { tasks.named("jvmTest") { useJUnitPlatform() } + +tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt index c7b6f889c1..99124ae8cb 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt @@ -111,7 +111,6 @@ internal fun TraceViewerWindow( SearchBox( nodes = frameNodeLocations.keys.toList(), onSearch = { name -> - sandboxState.scale = 1f val node = frameNodeLocations.keys.first { it.name == name } val newX = (sandboxState.offset.x - frameNodeLocations.getValue(node).x + appWindowSize.width / 2) @@ -144,7 +143,6 @@ internal fun TraceViewerWindow( internal class SandboxState { var offset by mutableStateOf(Offset.Zero) - var scale by mutableFloatStateOf(1f) fun reset() { offset = Offset.Zero diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 283c502a59..1625fec3ae 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -8,12 +8,14 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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 +import androidx.compose.ui.layout.FixedScale import androidx.compose.ui.unit.IntSize import com.squareup.workflow1.traceviewer.SandboxState +import me.saket.telephoto.zoomable.rememberZoomableState +import me.saket.telephoto.zoomable.zoomable /** * This is the backdrop for the whole app. Since there can be hundreds of modules at a time, there @@ -29,9 +31,12 @@ internal fun SandboxBackground( modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { + val zoomableState = rememberZoomableState() + Box( modifier .fillMaxSize() + .zoomable(state = zoomableState) .pointerInput(Unit) { // Panning capabilities: watches for drag gestures and applies the translation detectDragGestures { _, translation -> @@ -45,28 +50,16 @@ internal fun SandboxBackground( val event = awaitPointerEvent() if (event.type == PointerEventType.Scroll) { val pointerInput = event.changes.first() - val pointerOffsetToCenter = Offset( - // For some reason using 1.5 made zooming more natural than 2 - x = pointerInput.position.x - appWindowSize.width / (3 / 2), - y = pointerInput.position.y - appWindowSize.height / 2 - ) - val scrollDelta = pointerInput.scrollDelta.y // Applies zoom factor based on the actual delta change rather than just the act of scrolling // This helps to normalize mouse scrolling and touchpad scrolling, since touchpad will // fire a lot more scroll events. - val factor = 1f + (-scrollDelta * 0.1f) - val minWindowSize = 0.1f + val factor = 1f + (-pointerInput.scrollDelta.y * 0.1f) + val minWindowSize = 0.3f val maxWindowSize = 2f - val oldScale = sandboxState.scale + val oldScale = (zoomableState.contentScale as? FixedScale)?.value ?: 1.0f val newScale = (oldScale * factor).coerceIn(minWindowSize, maxWindowSize) - val scaleRatio = newScale / oldScale - - val newOrigin = sandboxState.offset - pointerOffsetToCenter - val scaledView = newOrigin * scaleRatio - val resetViewOffset = scaledView + pointerOffsetToCenter - sandboxState.offset = resetViewOffset - sandboxState.scale = newScale + zoomableState.contentScale = FixedScale(newScale) event.changes.forEach { it.consume() } } } @@ -78,8 +71,6 @@ internal fun SandboxBackground( .graphicsLayer { translationX = sandboxState.offset.x translationY = sandboxState.offset.y - scaleX = sandboxState.scale - scaleY = sandboxState.scale } ) { content() From ebb153926a6d2cbc1bfe0e38a7e15840ca1a9d9a Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Tue, 2 Sep 2025 18:19:59 -0400 Subject: [PATCH 5/9] Improve connection error message --- .../traceviewer/util/parser/TraceParser.kt | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/parser/TraceParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/parser/TraceParser.kt index 3cd9849bb7..58a540bdc7 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/parser/TraceParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/parser/TraceParser.kt @@ -1,6 +1,6 @@ package com.squareup.workflow1.traceviewer.util.parser -import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -12,9 +12,9 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.squareup.moshi.JsonAdapter @@ -24,6 +24,11 @@ import com.squareup.workflow1.traceviewer.model.NodeUpdate import com.squareup.workflow1.traceviewer.ui.DrawTree import com.squareup.workflow1.traceviewer.util.streamRenderPassesFromDevice +private const val ERROR_MESSAGE = + "\nEnsure app on device is logged in and running before connecting." + + "\n\nNote: Only one live connection per session is currently supported, " + + "\nif you have previously connected restart app on device and try again." + /** * Handles parsing the trace's after JsonParser has turned all render passes into frames. Also calls * the UI composables to render the full trace. @@ -39,7 +44,6 @@ internal fun RenderTrace( onNewFrame: () -> Unit, onNewData: (String) -> Unit, storeNodeLocation: (Node, Offset) -> Unit, - modifier: Modifier = Modifier ) { key(traceSource) { var isLoading by remember { mutableStateOf(true) } @@ -106,7 +110,7 @@ internal fun RenderTrace( val parseResult = parseLiveTrace(rawRenderPass, adapter, currentTree) handleParseResult(parseResult, rawRenderPass, onNewFrame) } - error = "Socket has already been closed or is not available." + error = ERROR_MESSAGE } } } @@ -115,7 +119,7 @@ internal fun RenderTrace( // the lambda call to parse the data was immediately cancelled, meaning handleParseResult was never // called to set isLoading to false. if (isLoading && error != null) { - Text("Socket Error: $error") + Error("Device Connection Failed:\n$error") return } @@ -123,7 +127,7 @@ internal fun RenderTrace( // handleParseResult method. Since there is no parsed data, this likely means there was a moshi // parsing error. if (error != null && frames.isEmpty()) { - Text("Malformed File: $error") + Error("Malformed File:\n$error") return } @@ -140,13 +144,18 @@ internal fun RenderTrace( // This error happens when there has already been previous data parsed, but some exception bubbled // up again, meaning it has to be a socket closure in Live mode. - error?.let { - Text( - text = "Socket closed: $error", - fontSize = 20.sp, - modifier = modifier.background(Color.White).padding(20.dp) - ) - } + error?.let { Error("Lost Connection:\n$error") } } } } + +@Composable +fun Error(message: String) { + Box { + Text( + text = message, + fontSize = 20.sp, + modifier = Modifier.align(Alignment.Center).padding(200.dp) + ) + } +} From 10a52cfd6ee4ac038651954c313f120f10d952a5 Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Tue, 2 Sep 2025 19:03:24 -0400 Subject: [PATCH 6/9] Automatically open info panel when a node is selected --- .../workflow1/traceviewer/FileTraceViewer.kt | 13 +++++++------ .../workflow1/traceviewer/ui/WorkflowInfoPanel.kt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt index 99124ae8cb..5db700f06d 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf @@ -133,11 +133,12 @@ internal fun TraceViewerWindow( modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp) ) } - - RightInfoPanel( - selectedNode = selectedNode, - modifier = Modifier.align(Alignment.TopEnd) - ) + key(selectedNode) { + RightInfoPanel( + selectedNode = selectedNode, + modifier = Modifier.align(Alignment.TopEnd) + ) + } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 4bdad801cd..bb33a79ca9 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -53,7 +53,7 @@ internal fun RightInfoPanel( Row( modifier = modifier ) { - var panelOpen by remember { mutableStateOf(false) } + var panelOpen by remember { mutableStateOf(selectedNode != null) } IconButton( onClick = { panelOpen = !panelOpen }, From a40f8a105e5af42ed051646c0b386b2d0895cb21 Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Tue, 2 Sep 2025 19:03:33 -0400 Subject: [PATCH 7/9] Disable proguard in release builds --- workflow-trace-viewer/build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 3d5e50cf98..b759fe3adc 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -59,6 +59,10 @@ compose { bundleID = "com.squareup.workflow1.traceviewer" } } + + buildTypes.release.proguard { + isEnabled.set(false) + } } } } From 84cffb08b5405b73f187c87c74d3f797baaad897 Mon Sep 17 00:00:00 2001 From: Jacob Applin Date: Tue, 9 Sep 2025 10:36:04 -0400 Subject: [PATCH 8/9] Move device polling into DisplayDevices composable --- .../workflow1/traceviewer/LandingWindow.kt | 50 ----------------- .../traceviewer/ui/control/DisplayDevices.kt | 54 ++++++++++++++++++- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt index 2f5f7d3dd8..60eddfc02a 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt @@ -29,13 +29,6 @@ internal fun LandingWindow( onFileSelected: (PlatformFile) -> Unit, onDeviceSelected: (String) -> Unit ) { - val devices by produceState(initialValue = listDevices()) { - while (true) { - delay(3000L) - value = listDevices() - } - } - Box(modifier = modifier.fillMaxSize()) { Column( modifier = Modifier @@ -75,50 +68,7 @@ internal fun LandingWindow( DisplayDevices( onDeviceSelect = onDeviceSelected, - devices = devices ) } } } - -/** - * Allows users to select from multiple devices that are currently running. - */ -private fun listDevices(): List { - if (adb == null) return emptyList() - val process = ProcessBuilder(adb, "devices", "-l").start() - process.waitFor() - // We drop the header "List of devices attached" - val devices = process.inputStream.use { - it.bufferedReader().readLines().drop(1).dropLast(1) - } - - return devices.mapNotNull { device -> - if (device.isBlank()) return@mapNotNull null - val deviceId = device.split(' ').first() - val deviceName = ProcessBuilder(adb, "-s", deviceId, "emu", "avd", "name").start() - deviceName.waitFor() - "$deviceId " + deviceName.inputStream.use { - it.bufferedReader().readLines().firstOrNull() ?: "" - } - } -} - -val adb: String? by lazy { - listOfNotNull( - System.getenv("ANDROID_HOME")?.let { "$it/platform-tools/adb"}, - // Common macOS Android SDK locations - "${System.getProperty("user.home")}/Library/Android/sdk/platform-tools/adb", - "/Users/${System.getProperty("user.name")}/Library/Android/sdk/platform-tools/adb", - ).firstOrNull { path -> - try { - val process = ProcessBuilder(path, "version").start() - if (process.waitFor() == 0) { - return@firstOrNull true - } - } catch (e: Exception) { - println(e) - } - return@firstOrNull false - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt index ada200b435..ce4e93d152 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/control/DisplayDevices.kt @@ -11,24 +11,33 @@ import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.key +import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay /** * Only give back the specific emulator device, i.e. "emulator-5554" */ private val emulatorRegex = Regex("""\bemulator-\d+\b""") +private const val ADB_DEVICE_LIST_POLLING_INTERVAL_MS = 3000L @OptIn(ExperimentalMaterialApi::class) @Composable internal fun DisplayDevices( onDeviceSelect: (String) -> Unit, - devices: List, modifier: Modifier = Modifier, ) { + val devices by produceState(initialValue = listDevices()) { + while (true) { + delay(ADB_DEVICE_LIST_POLLING_INTERVAL_MS) + value = listDevices() + } + } Box( modifier = modifier .fillMaxWidth(), @@ -67,3 +76,46 @@ internal fun DisplayDevices( } } } + +/** + * Allows users to select from multiple devices that are currently running. + */ +private fun listDevices(): List { + if (adb == null) return emptyList() + val process = ProcessBuilder(adb, "devices", "-l").start() + process.waitFor() + // We drop the header "List of devices attached" + val devices = process.inputStream.use { + it.bufferedReader().readLines().drop(1).dropLast(1) + } + + return devices.mapNotNull { device -> + if (device.isBlank()) return@mapNotNull null + val deviceId = device.split(' ').first() + val deviceName = ProcessBuilder(adb, "-s", deviceId, "emu", "avd", "name").start() + deviceName.waitFor() + "$deviceId " + deviceName.inputStream.use { + it.bufferedReader().readLines().firstOrNull() ?: "" + } + } +} + +val adb: String? by lazy { + listOfNotNull( + System.getenv("ANDROID_HOME")?.let { "$it/platform-tools/adb"}, + // Common macOS Android SDK locations + "${System.getProperty("user.home")}/Library/Android/sdk/platform-tools/adb", + "/Users/${System.getProperty("user.name")}/Library/Android/sdk/platform-tools/adb", + ).firstOrNull { path -> + try { + val process = ProcessBuilder(path, "version").start() + if (process.waitFor() == 0) { + return@firstOrNull true + } + } catch (e: Exception) { + println(e) + } + return@firstOrNull false + } +} + From 80ea45f018c545f5b79c45a485defa92591b1236 Mon Sep 17 00:00:00 2001 From: japplin <1403243+japplin@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:48:08 +0000 Subject: [PATCH 9/9] Apply changes from apiDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../api/workflow-trace-viewer.api | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index 475066e2f9..4bc6cf1232 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -1,7 +1,7 @@ public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainKt { public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$MainKt; public fun ()V - public final fun getLambda$468449326$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-793818668$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } public final class com/squareup/workflow1/traceviewer/MainKt { @@ -9,10 +9,37 @@ public final class com/squareup/workflow1/traceviewer/MainKt { public static synthetic fun main ([Ljava/lang/String;)V } +public abstract interface class com/squareup/workflow1/traceviewer/TraceWindow { +} + +public final class com/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow : com/squareup/workflow1/traceviewer/TraceWindow { + public static final field $stable I + public fun (Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow; + public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow; + public fun equals (Ljava/lang/Object;)Z + public final fun getDevice ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/traceviewer/TraceWindow$FileWindow : com/squareup/workflow1/traceviewer/TraceWindow { + public static final field $stable I + public fun (Lio/github/vinceglb/filekit/PlatformFile;)V + public final fun component1 ()Lio/github/vinceglb/filekit/PlatformFile; + public final fun copy (Lio/github/vinceglb/filekit/PlatformFile;)Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow; + public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow;Lio/github/vinceglb/filekit/PlatformFile;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow; + public fun equals (Ljava/lang/Object;)Z + public final fun getFile ()Lio/github/vinceglb/filekit/PlatformFile; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt { public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt; public fun ()V - public final fun getLambda$-1653175968$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; + public final fun getLambda$-1925612255$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } public final class com/squareup/workflow1/traceviewer/ui/control/ComposableSingletons$SearchBoxKt { @@ -28,3 +55,11 @@ public final class com/squareup/workflow1/traceviewer/ui/control/ComposableSingl public final fun getLambda$-1248702605$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } +public final class com/squareup/workflow1/traceviewer/ui/control/DisplayDevicesKt { + public static final fun getAdb ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/traceviewer/util/parser/TraceParserKt { + public static final fun Error (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V +} +