Touchpad: two-finger scroll and pinch zoom, fix selection row offset; v0.1.1

This commit is contained in:
WHC
2026-10-06 14:24:43 +08:00
parent e00bb637f9
commit a623c93b76
2 changed files with 55 additions and 6 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId "xyz.whcself.whcshell"
minSdk 26
targetSdk 35
versionCode 1
versionName "0.1.0"
versionCode 2
versionName "0.1.1"
}
buildTypes {
@@ -284,10 +284,15 @@ public final class TermView extends View {
return Math.max(0, Math.min(cols - 1, c));
}
/** Top of the first text row; the renderer starts rows below the padding by this offset. */
private int textTop() {
return getPaddingTop() + renderer.getFontLineSpacingAndAscent();
}
/** Visible row, 0-based. */
private int row(MotionEvent ev) {
int rows = emulator().mRows;
int r = (int) Math.floor((ev.getY() - getPaddingTop()) / renderer.getFontLineSpacing());
int r = (int) Math.floor((ev.getY() - textTop()) / renderer.getFontLineSpacing());
return Math.max(0, Math.min(rows - 1, r));
}
@@ -448,6 +453,49 @@ public final class TermView extends View {
return m;
}
private float gestureY, pinchDist;
/**
* Touchpad gestures arrive as mouse-source events with a finger tool and no buttons: a fake
* finger classified as a two-finger swipe for scrolling (the gesture axes stay 0, so the finger's
* movement is the distance), two fake fingers for a pinch, and stray single-finger strokes around
* taps. None of them are button presses.
*/
private boolean onTouchpadGesture(MotionEvent ev) {
int action = ev.getActionMasked();
if (ev.getPointerCount() >= 2) {
float d = (float) Math.hypot(ev.getX(0) - ev.getX(1), ev.getY(0) - ev.getY(1));
if (action == MotionEvent.ACTION_POINTER_DOWN) {
pinchDist = d;
} else if (action == MotionEvent.ACTION_MOVE && pinchDist > 0 && actions != null) {
if (d > pinchDist * 1.25f) {
actions.zoom(1);
pinchDist = d;
} else if (d < pinchDist / 1.25f) {
actions.zoom(-1);
pinchDist = d;
}
}
return true;
}
pinchDist = 0;
boolean swipe = android.os.Build.VERSION.SDK_INT >= 34
&& ev.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE;
if (action == MotionEvent.ACTION_DOWN) {
gestureY = ev.getY();
scrollAccum = 0;
} else if (action == MotionEvent.ACTION_MOVE && swipe) {
scrollAccum += (gestureY - ev.getY()) / renderer.getFontLineSpacing();
gestureY = ev.getY();
int lines = (int) scrollAccum;
if (lines != 0) {
scrollAccum -= lines;
scrollLines(lines, ev);
}
}
return true;
}
private static final int[][] BUTTONS = {
{MotionEvent.BUTTON_PRIMARY, TerminalEmulator.MOUSE_LEFT_BUTTON},
{MotionEvent.BUTTON_TERTIARY, 1},
@@ -459,11 +507,12 @@ public final class TermView extends View {
* button, so presses and releases are derived from changes in the button state.
*/
private boolean onMouse(MotionEvent ev) {
if (ev.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER) return onTouchpadGesture(ev);
TerminalEmulator e = emulator();
int action = ev.getActionMasked();
int state = ev.getButtonState()
& (MotionEvent.BUTTON_PRIMARY | MotionEvent.BUTTON_SECONDARY | MotionEvent.BUTTON_TERTIARY);
// Touchpad taps and injected events can come as DOWN/UP without any button state.
// Injected events (adb input mouse) come as DOWN/UP without any button state.
if (action == MotionEvent.ACTION_DOWN && state == 0) implicitPrimary = true;
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) implicitPrimary = false;
if (implicitPrimary) state |= MotionEvent.BUTTON_PRIMARY;
@@ -524,8 +573,8 @@ public final class TermView extends View {
} else if (selDragging && (state & MotionEvent.BUTTON_PRIMARY) != 0
&& action == MotionEvent.ACTION_MOVE) {
float y = ev.getY();
if (y < getPaddingTop()) scrollTranscript(-1);
else if (y > getHeight() - getPaddingBottom()) scrollTranscript(1);
if (y < textTop()) scrollTranscript(-1);
else if (y >= textTop() + emulator().mRows * renderer.getFontLineSpacing()) scrollTranscript(1);
extendSelection(col, row + topRow);
}
if ((released & MotionEvent.BUTTON_PRIMARY) != 0 && selDragging) {