Land taps and the pointer on what is drawn under them (#389)

The compositor takes touch and pointer positions over its whole output and
removes the letterbox bars itself, but the session sent them already
measured from the picture's edge, so the bars were taken off twice. Any
session whose output is a different shape from the panel (16:9 on a 16:10
tablet or a foldable, a fixed resolution on a wide phone) had taps and the
touchpad pointer pulled away from the centre, by up to a bar's height at
the edges: on a 2400x1504 panel showing 1280x720, a tap near the top
reached the first row and one near the bottom landed 32 rows low. Stretch
to panel hid it because that output has no bars.

Direct touch, the touchpad and mouse pointer, and the second-screen pointer
now send positions over the whole view, so the compositor's own mapping
puts them on the pixel under the finger.
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-10-08 11:52:45 -04:00
1 parent a58b0ac3e0
commit b6c1d3c09c
3 files changed
+71 -19

No files matched your search

@@ -45,6 +45,7 @@ import com.droiddeck.launcher.gpu.ScreenEffects
import com.droiddeck.launcher.gpu.Lossless
import com.droiddeck.launcher.gpu.TurnipDriver
import com.droiddeck.launcher.input.EvdevKeys
import com.droiddeck.launcher.input.InputSpace
import com.droiddeck.launcher.input.KeyboardHost
import com.droiddeck.launcher.input.ControllerPrefs
import com.droiddeck.launcher.input.SessionClipboard
@@ -1486,18 +1487,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
private fun movePointer(x: Float, y: Float) {
val width = surfaceView.width.takeIf { it > 0 } ?: return
val height = surfaceView.height.takeIf { it > 0 } ?: return
val out = SessionState.outputSize
val scale = minOf(width / out.first.toFloat(), height / out.second.toFloat())
val drawnW = out.first * scale
val drawnH = out.second * scale
val left = (width - drawnW) / 2f
val top = (height - drawnH) / 2f
val px = ((x - left) / drawnW * 1920f).toInt().coerceIn(0, 1919)
val py = ((y - top) / drawnH * 1080f).toInt().coerceIn(0, 1079)
WaylandCompositor.nativeSendPointer(1, px, py)
showCursor(x.coerceIn(left, left + drawnW), y.coerceIn(top, top + drawnH))
val rect = drawnRect() ?: return
val cx = x.coerceIn(rect.left, rect.right)
val cy = y.coerceIn(rect.top, rect.bottom)
WaylandCompositor.nativeSendPointer(1, InputSpace.x(cx, surfaceView.width), InputSpace.y(cy, surfaceView.height))
showCursor(cx, cy)
}
/**
@@ -1644,10 +1638,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
if (width <= 0f || height <= 0f) return
val nx = (x / width).coerceIn(0f, 1f)
val ny = (y / height).coerceIn(0f, 1f)
WaylandCompositor.nativeSendPointer(1, (nx * 1919f).toInt(), (ny * 1079f).toInt())
drawnRect()?.let { rect ->
showCursor(rect.left + nx * rect.width(), rect.top + ny * rect.height())
}
val rect = drawnRect() ?: return
val px = rect.left + nx * rect.width()
val py = rect.top + ny * rect.height()
WaylandCompositor.nativeSendPointer(1, InputSpace.x(px, surfaceView.width), InputSpace.y(py, surfaceView.height))
showCursor(px, py)
}
private fun closeSecondScreen(reset: Boolean) {
@@ -1694,9 +1689,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
val rect = drawnRect() ?: return false
fun sendTouch(action: Int, index: Int) {
val x = ((event.getX(index) - rect.left) / rect.width()).coerceIn(0f, 1f)
val y = ((event.getY(index) - rect.top) / rect.height()).coerceIn(0f, 1f)
WaylandCompositor.nativeSendTouch(action, event.getPointerId(index), (x * 1919f).toInt(), (y * 1079f).toInt())
val x = event.getX(index).coerceIn(rect.left, rect.right)
val y = event.getY(index).coerceIn(rect.top, rect.bottom)
WaylandCompositor.nativeSendTouch(action, event.getPointerId(index),
InputSpace.x(x, surfaceView.width), InputSpace.y(y, surfaceView.height))
}
when (event.actionMasked) {
// A first finger starts a new gesture, and Android has ended every earlier one: whatever
@@ -0,0 +1,13 @@
package com.droiddeck.launcher.input
object InputSpace {
const val WIDTH = 1920
const val HEIGHT = 1080
fun x(position: Float, viewWidth: Int): Int = scale(position, viewWidth, WIDTH)
fun y(position: Float, viewHeight: Int): Int = scale(position, viewHeight, HEIGHT)
private fun scale(position: Float, extent: Int, size: Int): Int =
if (extent <= 0) 0 else (position / extent * size).toInt().coerceIn(0, size - 1)
}
@@ -0,0 +1,43 @@
package com.droiddeck.launcher.input
import org.junit.Assert.*
import org.junit.Test
class InputSpaceTest {
private fun sceneUnder(viewX: Float, viewY: Float, view: Pair<Int, Int>, scene: Pair<Int, Int>): Pair<Double, Double> {
val k = minOf(view.first.toDouble() / scene.first, view.second.toDouble() / scene.second)
val offX = (view.first - scene.first * k) / 2
val offY = (view.second - scene.second * k) / 2
val ox = InputSpace.x(viewX, view.first).toDouble() * view.first / InputSpace.WIDTH
val oy = InputSpace.y(viewY, view.second).toDouble() * view.second / InputSpace.HEIGHT
return ((ox - offX) / k).coerceIn(0.0, scene.first - 1.0) to ((oy - offY) / k).coerceIn(0.0, scene.second - 1.0)
}
private fun assertLandsUnderFinger(view: Pair<Int, Int>, scene: Pair<Int, Int>) {
val k = minOf(view.first.toFloat() / scene.first, view.second.toFloat() / scene.second)
val offX = (view.first - scene.first * k) / 2
val offY = (view.second - scene.second * k) / 2
val stepX = view.first.toDouble() / InputSpace.WIDTH / k
val stepY = view.second.toDouble() / InputSpace.HEIGHT / k
for (sx in listOf(10, scene.first / 4, scene.first / 2, scene.first - 10))
for (sy in listOf(10, scene.second / 4, scene.second / 2, scene.second - 10)) {
val (x, y) = sceneUnder(offX + sx * k, offY + sy * k, view, scene)
assertEquals("x at $sx,$sy on $view showing $scene", sx.toDouble(), x, stepX + 0.01)
assertEquals("y at $sx,$sy on $view showing $scene", sy.toDouble(), y, stepY + 0.01)
}
}
@Test fun aTouchReachesTheScenePixelUnderTheFinger() {
assertLandsUnderFinger(2400 to 1504, 1280 to 720)
assertLandsUnderFinger(2400 to 1080, 1280 to 720)
assertLandsUnderFinger(2400 to 1504, 1148 to 720)
assertLandsUnderFinger(1080 to 2400, 1280 to 720)
}
@Test fun positionsOutsideTheViewStayInRange() {
assertEquals(0, InputSpace.x(-50f, 2400))
assertEquals(InputSpace.WIDTH - 1, InputSpace.x(2400f, 2400))
assertEquals(InputSpace.HEIGHT - 1, InputSpace.y(5000f, 1504))
assertEquals(0, InputSpace.y(10f, 0))
}
}