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/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 +} + diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 6bfd41a3ef..b759fe3adc 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 { @@ -50,11 +54,15 @@ 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" } } + + buildTypes.release.proguard { + isEnabled.set(false) + } } } } @@ -62,3 +70,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/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/FileTraceViewer.kt similarity index 66% 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..5db700f06d 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,12 +2,12 @@ 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 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 @@ -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 @@ -124,7 +111,6 @@ internal fun App( 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) @@ -141,63 +127,23 @@ 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) + ) + } + key(selectedNode) { + RightInfoPanel( + selectedNode = selectedNode, + modifier = Modifier.align(Alignment.TopEnd) ) } - - RightInfoPanel( - selectedNode = selectedNode, - modifier = Modifier.align(Alignment.TopEnd) - ) } } internal class SandboxState { var offset by mutableStateOf(Offset.Zero) - var scale by mutableFloatStateOf(1f) fun reset() { offset = Offset.Zero @@ -218,13 +164,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..60eddfc02a --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/LandingWindow.kt @@ -0,0 +1,74 @@ +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.getValue +import androidx.compose.runtime.produceState +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 +) { + 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, + ) + } + } +} 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/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 }, 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..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 @@ -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 @@ -10,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(), @@ -52,12 +62,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) ) } } @@ -65,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 + } +} + 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 ) } } 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() 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) + ) + } +}