diff --git a/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt b/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt index ec97bbd..2752997 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.calculateCentroid import androidx.compose.foundation.gestures.calculatePan import androidx.compose.foundation.gestures.calculateZoom -import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -258,60 +258,36 @@ internal fun TimelineGraph( .fillMaxWidth() .height(190.dp) .background(Color(0xFF0A0E18), RoundedCornerShape(10.dp)) - .pointerInput(activeCurve, viewport, curveLeft, curveRight) { + .pointerInput(viewport, selection, curveLeft, curveRight, activeCurve) { awaitPointerEventScope { while (true) { - val event = awaitPointerEvent(pass = PointerEventPass.Initial) - val pressedCount = event.changes.count { it.pressed } - if (pressedCount < 2) continue - - val zoom = event.calculateZoom() - val pan = event.calculatePan() - val centroid = event.calculateCentroid(useCurrent = true) - - if (zoom != 1f || pan.x != 0f || pan.y != 0f) { - val newViewport = viewport.applyZoomPan( - zoomChange = zoom, - panPx = pan.x, - widthPx = size.width.toFloat(), - centroidPx = centroid.x, - ) - onViewportChanged(newViewport) - } - - // Reserve all 2+ finger gestures for transform handling so they don't - // compete with one-finger point/handle drags. - event.changes.forEach { it.consume() } - draggingPointIndex = null - draggingCurveId = null - dragMode = HandleDrag.NONE - } - } - } - .pointerInput(viewport, selection, curveLeft, curveRight, activeCurve) { - detectDragGestures( - onDragStart = { start -> + val down = awaitFirstDown(requireUnconsumed = false) val w = size.width.toFloat() val h = size.height.toFloat() val startX = viewport.timeToX(selection.startSec, w) val endX = viewport.timeToX(selection.endSec, w) val hitPx = 32f + var activePointerId = down.id + var lastPos = down.position + var isTransform = false + var moved = false + val hits = buildList { - curveLeft.hitTestPoint(start, viewport, w, h)?.let { idx -> + curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx -> val point = curveLeft.points[idx] val x = viewport.timeToX(point.tSec, w) val y = valueToY(point.value01, h) - val dx = start.x - x - val dy = start.y - y + val dx = down.position.x - x + val dy = down.position.y - y add(Triple(leftId, idx, dx * dx + dy * dy)) } - curveRight.hitTestPoint(start, viewport, w, h)?.let { idx -> + curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx -> val point = curveRight.points[idx] val x = viewport.timeToX(point.tSec, w) val y = valueToY(point.value01, h) - val dx = start.x - x - val dy = start.y - y + val dx = down.position.x - x + val dy = down.position.y - y add(Triple(rightId, idx, dx * dx + dy * dy)) } } @@ -323,65 +299,99 @@ internal fun TimelineGraph( dragMode = HandleDrag.NONE onTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT) } else { - dragMode = when { - abs(start.x - startX) <= hitPx -> HandleDrag.START - abs(start.x - endX) <= hitPx -> HandleDrag.END - else -> HandleDrag.NONE - } draggingPointIndex = null draggingCurveId = null - } - }, - onDragEnd = { - draggingPointIndex = null - draggingCurveId = null - dragMode = HandleDrag.NONE - }, - onDragCancel = { - draggingPointIndex = null - draggingCurveId = null - dragMode = HandleDrag.NONE - }, - onDrag = { change, dragAmount -> - change.consume() - val w = size.width.toFloat() - val h = size.height.toFloat() - val pointIndex = draggingPointIndex - val curveId = draggingCurveId - if (pointIndex != null && curveId != null) { - val targetCurve = if (curveId == leftId) curveLeft else curveRight - val updated = targetCurve.movePointVertical(pointIndex, change.position.y, h) - onUpdateCurve(curveId, updated) - onCurveEditedHaptic() - return@detectDragGestures - } - - val t = viewport.xToTimeSec(change.position.x, w).toInt().coerceAtLeast(0) - when (dragMode) { - HandleDrag.START -> onSelectionChanged(TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)) - HandleDrag.END -> onSelectionChanged(TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))) - HandleDrag.NONE -> { - val newViewport = viewport.applyZoomPan( - zoomChange = 1f, - panPx = dragAmount.x, - widthPx = w, - centroidPx = w / 2f, - ) - onViewportChanged(newViewport) + dragMode = when { + abs(down.position.x - startX) <= hitPx -> HandleDrag.START + abs(down.position.x - endX) <= hitPx -> HandleDrag.END + else -> HandleDrag.NONE } } + + while (true) { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + val pressed = event.changes.filter { it.pressed } + if (pressed.isEmpty()) break + + if (pressed.size >= 2) { + isTransform = true + draggingPointIndex = null + draggingCurveId = null + dragMode = HandleDrag.NONE + } + + if (isTransform) { + val zoom = event.calculateZoom() + val pan = event.calculatePan() + val centroid = event.calculateCentroid(useCurrent = true) + if (zoom != 1f || pan.x != 0f || pan.y != 0f) { + moved = true + val newViewport = viewport.applyZoomPan( + zoomChange = zoom, + panPx = pan.x, + widthPx = size.width.toFloat(), + centroidPx = centroid.x, + ) + onViewportChanged(newViewport) + } + event.changes.forEach { it.consume() } + continue + } + + val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first() + activePointerId = primary.id + val delta = primary.position - lastPos + lastPos = primary.position + + if (delta.getDistanceSquared() > 0.5f) moved = true + + val pointIndex = draggingPointIndex + val curveId = draggingCurveId + if (pointIndex != null && curveId != null) { + val targetCurve = if (curveId == leftId) curveLeft else curveRight + val updated = targetCurve.movePointVertical(pointIndex, primary.position.y, h) + onUpdateCurve(curveId, updated) + onCurveEditedHaptic() + primary.consume() + continue + } + + val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0) + when (dragMode) { + HandleDrag.START -> { + onSelectionChanged(TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)) + primary.consume() + } + HandleDrag.END -> { + onSelectionChanged(TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))) + primary.consume() + } + HandleDrag.NONE -> { + if (delta.x != 0f) { + val newViewport = viewport.applyZoomPan( + zoomChange = 1f, + panPx = delta.x, + widthPx = w, + centroidPx = w / 2f, + ) + onViewportChanged(newViewport) + primary.consume() + } + } + } + } + + if (!moved && !isTransform) { + val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT + onTapSideSelect(side) + } + draggingPointIndex = null + draggingCurveId = null + dragMode = HandleDrag.NONE } - ) + } } - .pointerInput(activeCurve) { - detectTapGestures( - onTap = { offset -> - val side = if (offset.x < size.width / 2f) Side.LEFT else Side.RIGHT - onTapSideSelect(side) - } - ) - } - ) { + ) { Canvas(modifier = Modifier.fillMaxSize()) { drawGrid()