@@ -3,6 +3,11 @@ package com.pureswift.swiftui
33import androidx.compose.foundation.background
44import androidx.compose.foundation.border
55import androidx.compose.foundation.clickable
6+ import androidx.compose.foundation.ExperimentalFoundationApi
7+ import androidx.compose.foundation.combinedClickable
8+ import androidx.compose.foundation.gestures.detectDragGestures
9+ import androidx.compose.ui.geometry.Offset
10+ import androidx.compose.ui.input.pointer.pointerInput
611import androidx.compose.foundation.rememberScrollState
712import androidx.compose.foundation.horizontalScroll
813import androidx.compose.foundation.verticalScroll
@@ -1062,6 +1067,7 @@ private fun fontWeightFor(name: String): FontWeight = when (name) {
10621067// / animation (explicit `withAnimation` tree, or this node's `.animation`
10631068// / modifier) applies, and `snap` otherwise.
10641069@Composable
1070+ @OptIn(ExperimentalFoundationApi ::class )
10651071internal fun ViewNode.composeModifiers (): Modifier {
10661072 val implicit = modifiers.firstOrNull { it.kind == " animation" && it.args[" curve" ] != null }
10671073 ?.let { AnimSpec (it.args.string(" curve" ) ? : " easeInOut" , (it.args.double(" durationMs" ) ? : 350.0 ).toInt()) }
@@ -1124,9 +1130,66 @@ internal fun ViewNode.composeModifiers(): Modifier {
11241130 modifier.clip(shape)
11251131 }
11261132
1133+ // Tap and long press share one detector: two competing pointer
1134+ // handlers on the same view would fight over the gesture.
11271135 " onTapGesture" -> {
11281136 val id = entry.args.long(" action" )
1129- if (id != null ) modifier.clickable { SwiftBridge .sink.invokeVoid(id) } else modifier
1137+ val longID = modifiers.firstOrNull { it.kind == " longPress" }?.args?.long(" action" )
1138+ when {
1139+ id == null -> modifier
1140+ longID == null -> modifier.clickable { SwiftBridge .sink.invokeVoid(id) }
1141+ else -> modifier.combinedClickable(
1142+ onClick = { SwiftBridge .sink.invokeVoid(id) },
1143+ onLongClick = { SwiftBridge .sink.invokeVoid(longID) },
1144+ )
1145+ }
1146+ }
1147+
1148+ " longPress" -> {
1149+ val id = entry.args.long(" action" )
1150+ // already wired above when this view also has a tap handler
1151+ val hasTap = modifiers.any { it.kind == " onTapGesture" }
1152+ if (id == null || hasTap) modifier
1153+ else modifier.combinedClickable(onClick = {}, onLongClick = { SwiftBridge .sink.invokeVoid(id) })
1154+ }
1155+
1156+ // Continuous drag: positions are reported in points, converted from
1157+ // the pointer's pixels, as "<phase>;<startX>,<startY>;<x>,<y>".
1158+ " drag" -> {
1159+ val id = entry.args.long(" action" )
1160+ if (id == null ) modifier else {
1161+ // The detector is a long-running coroutine, so it must be keyed
1162+ // by the node's STABLE id: keying by the callback id would tear
1163+ // the gesture down mid-drag, since every onChanged re-evaluates
1164+ // the tree and mints a fresh id. The lambda then reads the id
1165+ // from a holder so it never dispatches to a reclaimed callback.
1166+ val latest = remember(this @composeModifiers.id) { longArrayOf(id) }
1167+ latest[0 ] = id
1168+ modifier.pointerInput(this @composeModifiers.id) {
1169+ var start = Offset .Zero
1170+ // Accumulate per-event deltas rather than reading absolute
1171+ // positions: a view that offsets itself by the translation
1172+ // moves under the finger, and absolute readings would
1173+ // compensate for that motion and collapse toward zero.
1174+ var total = Offset .Zero
1175+ fun payload (phase : String ): String {
1176+ val end = start + total
1177+ return " $phase ;${start.x.toDp().value} ,${start.y.toDp().value} ;" +
1178+ " ${end.x.toDp().value} ,${end.y.toDp().value} "
1179+ }
1180+ detectDragGestures(
1181+ onDragStart = { start = it; total = Offset .Zero },
1182+ onDrag = { change, dragAmount ->
1183+ change.consume()
1184+ total + = dragAmount
1185+ SwiftBridge .sink.invokeString(latest[0 ], payload(" changed" ))
1186+ },
1187+ // ends carrying the final translation, as SwiftUI does
1188+ onDragEnd = { SwiftBridge .sink.invokeString(latest[0 ], payload(" ended" )) },
1189+ onDragCancel = { SwiftBridge .sink.invokeString(latest[0 ], payload(" ended" )) },
1190+ )
1191+ }
1192+ }
11301193 }
11311194
11321195 // Dim disabled content; controls also drop interactivity via their
@@ -1155,7 +1218,7 @@ private val KNOWN_MODIFIER_KINDS = setOf(
11551218 " scale" , " opacity" , " border" , " shadow" , " clipShape" , " onTapGesture" , " disabled" ,
11561219 " font" , " fontWeight" , " italic" , " foregroundColor" , " lineLimit" , " multilineTextAlignment" ,
11571220 " tint" , " onAppear" , " onDisappear" , " task" , " onChange" , " animation" , " tag" , " tabItem" ,
1158- " transition" , " focused" ,
1221+ " transition" , " focused" , " longPress " , " drag " ,
11591222)
11601223
11611224// Folds a frame entry: fixed size, fill (maxWidth/Height .infinity), bounded
0 commit comments