Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@ package com.squareup.workflow1.traceviewer

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
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.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.model.NodeUpdate
Expand All @@ -31,6 +30,10 @@ import com.squareup.workflow1.traceviewer.ui.control.SearchBox
import com.squareup.workflow1.traceviewer.util.SandboxBackground
import com.squareup.workflow1.traceviewer.util.parser.RenderTrace
import io.github.vinceglb.filekit.PlatformFile
import kotlinx.coroutines.launch
import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.rememberZoomableState
import kotlin.also

/**
* Main composable that provides the different layers of UI.
Expand All @@ -40,53 +43,52 @@ internal fun TraceViewerWindow(
modifier: Modifier = Modifier,
traceMode: TraceMode,
) {
var appWindowSize by remember { mutableStateOf(IntSize(0, 0)) }
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
var frameSize by remember { mutableIntStateOf(0) }
var rawRenderPass by remember { mutableStateOf("") }
var frameIndex by remember { mutableIntStateOf(if (traceMode is TraceMode.Live) -1 else 0) }
val sandboxState = remember { SandboxState() }
val nodeLocations = remember { mutableStateListOf<SnapshotStateMap<Node, Offset>>() }

// Default to File mode, and can be toggled to be in Live mode.
var active by remember { mutableStateOf(false) }
// 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

LaunchedEffect(sandboxState) {
snapshotFlow { frameIndex }.collect {
sandboxState.reset()
}
}
Box(modifier) {
val zoomableState = key(frameInd) {
rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 1f)
).also {
it.contentScale = ContentScale.Fit

Box(
modifier = modifier.onSizeChanged {
appWindowSize = it
// TODO: do we want to draw behind the search bar?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think drawing behind the search looks correct, which is what it's doing already, if we didn't allow it there would be a jarring cutoff.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In that case, do you want to synchronize the content padding below with the search box's height?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure, I think it was looking good how it was now.

val searchBarHeight = 80.dp
it.contentPadding = PaddingValues(horizontal = 24.dp, vertical = searchBarHeight)
}
}
) {

// Main content
SandboxBackground(
appWindowSize = appWindowSize,
sandboxState = sandboxState,
) {
// if there is not a file selected and trace mode is live, then don't render anything.
val readyForFileTrace = TraceMode.validateFileMode(traceMode)
val readyForLiveTrace = TraceMode.validateLiveMode(traceMode)
zoomableState = zoomableState,
content = {
// if there is not a file selected and trace mode is live, then don't render anything.
val readyForFileTrace = TraceMode.validateFileMode(traceMode)
val readyForLiveTrace = TraceMode.validateLiveMode(traceMode)

if (readyForFileTrace || readyForLiveTrace) {
active = true
RenderTrace(
traceSource = traceMode,
frameInd = frameIndex,
onFileParse = { frameSize += it },
onNodeSelect = { selectedNode = it },
onNewFrame = { frameIndex += 1 },
onNewData = { rawRenderPass += "$it," },
storeNodeLocation = { node, loc -> nodeLocations[frameInd] += (node to loc) }
)
}
}
if (readyForFileTrace || readyForLiveTrace) {
active = true
RenderTrace(
traceSource = traceMode,
frameInd = frameIndex,
onFileParse = { frameSize += it },
onNodeSelect = { selectedNode = it },
onNewFrame = { frameIndex += 1 },
onNewData = { rawRenderPass += "$it," },
storeNodeLocation = { node, loc -> nodeLocations[frameInd] += (node to loc) }
)
}
},
)

Row(
modifier = Modifier
Expand All @@ -108,15 +110,19 @@ internal fun TraceViewerWindow(
}

val frameNodeLocations = nodeLocations[frameInd]
val scope = rememberCoroutineScope()
SearchBox(
nodes = frameNodeLocations.keys.toList(),
onSearch = { name ->
val node = frameNodeLocations.keys.first { it.name == name }
val newX = (sandboxState.offset.x - frameNodeLocations.getValue(node).x
+ appWindowSize.width / 2)
val newY = (sandboxState.offset.y - frameNodeLocations.getValue(node).y
+ appWindowSize.height / 2)
sandboxState.offset = Offset(x = newX, y = newY)
scope.launch {
// TODO: this doesn't work super well.
// Probably because of https://github.com/saket/telephoto/issues/135?
zoomableState.zoomTo(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From local testing this isn't adjusting the pan at all when selecting a node.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be fixed by saket/telephoto@68fcaef. Do you mind if I update telephoto to its latest snapshot version?

Copy link
Contributor Author

@japplin japplin Sep 12, 2025

Choose a reason for hiding this comment

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

Nope go for it

zoomFactor = zoomableState.zoomSpec.maximum.factor,
centroid = frameNodeLocations.getValue(node),
)
}
},
)

Expand All @@ -142,14 +148,6 @@ internal fun TraceViewerWindow(
}
}

internal class SandboxState {
var offset by mutableStateOf(Offset.Zero)

fun reset() {
offset = Offset.Zero
}
}

internal sealed interface TraceMode {
data class File(val file: PlatformFile?) : TraceMode
data class Live(val device: String? = null) : TraceMode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package com.squareup.workflow1.traceviewer.util

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
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 androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.toSize
import me.saket.telephoto.zoomable.ZoomableContentLocation
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.zoomable

/**
Expand All @@ -26,51 +28,29 @@ import me.saket.telephoto.zoomable.zoomable
*/
@Composable
internal fun SandboxBackground(
appWindowSize: IntSize,
sandboxState: SandboxState,
zoomableState: ZoomableState,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val zoomableState = rememberZoomableState()

val focusRequester = remember { FocusRequester() }
LaunchedEffect(zoomableState) {
// Request focus to receive keyboard shortcuts.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is turning out to be more complex than I expected. The content starts in a focused state but loses focus when the search box is clicked. We'll have to restore focus when a search result is selected. Let me try fixing this.

focusRequester.requestFocus()
}
Box(
modifier
.fillMaxSize()
.zoomable(state = zoomableState)
.pointerInput(Unit) {
// Panning capabilities: watches for drag gestures and applies the translation
detectDragGestures { _, translation ->
sandboxState.offset += translation
}
}
.pointerInput(appWindowSize) {
// Zooming capabilities: watches for any scroll events and immediately consumes changes.
// - This is AI generated.
awaitEachGesture {
val event = awaitPointerEvent()
if (event.type == PointerEventType.Scroll) {
val pointerInput = event.changes.first()
// 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 + (-pointerInput.scrollDelta.y * 0.1f)
val minWindowSize = 0.3f
val maxWindowSize = 2f
val oldScale = (zoomableState.contentScale as? FixedScale)?.value ?: 1.0f
val newScale = (oldScale * factor).coerceIn(minWindowSize, maxWindowSize)

zoomableState.contentScale = FixedScale(newScale)
event.changes.forEach { it.consume() }
}
}
}
.focusRequester(focusRequester)
.zoomable(zoomableState)
) {
Box(
modifier = Modifier
.wrapContentSize(unbounded = true, align = Alignment.Center)
.graphicsLayer {
translationX = sandboxState.offset.x
translationY = sandboxState.offset.y
.wrapContentSize(unbounded = true, align = AbsoluteAlignment.TopLeft)
.onSizeChanged {
// TODO(saket): Modifier.zoomable() should automatically use its child's size by default.
zoomableState.setContentLocation(
ZoomableContentLocation.unscaledAndTopLeftAligned(it.toSize())
)
}
) {
content()
Expand Down
Loading