Focus mobile terminal only on tap
This commit is contained in:
@@ -8,7 +8,14 @@ import {
|
||||
type ComponentProps,
|
||||
type Ref,
|
||||
} from "react";
|
||||
import { Keyboard, StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import {
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
View,
|
||||
type GestureResponderEvent,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { WebView, type WebViewMessageEvent } from "react-native-webview";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import type { TerminalState } from "@server/shared/messages";
|
||||
@@ -131,7 +138,13 @@ const TERMINAL_WEBVIEW_SOURCE = { html: terminalEmulatorWebViewHtml };
|
||||
const TERMINAL_WEBVIEW_ORIGIN_WHITELIST = ["*"];
|
||||
const BRIDGE_READY_TIMEOUT_MS = 2_500;
|
||||
const RENDERER_READY_TIMEOUT_MS = 2_500;
|
||||
const TERMINAL_TAP_MOVE_TOLERANCE_PX = 8;
|
||||
type WebViewProps = ComponentProps<typeof WebView>;
|
||||
interface PendingTerminalTap {
|
||||
startX: number;
|
||||
startY: number;
|
||||
moved: boolean;
|
||||
}
|
||||
|
||||
function buildThemeKey(theme: ITheme): string {
|
||||
return JSON.stringify(theme);
|
||||
@@ -197,6 +210,7 @@ export default function TerminalEmulator({
|
||||
const mountedStreamKeyRef = useRef<string | null>(null);
|
||||
const bridgeReadyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const rendererReadyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingTapRef = useRef<PendingTerminalTap | null>(null);
|
||||
const mountConfigRef = useRef({
|
||||
streamKey,
|
||||
initialSnapshot,
|
||||
@@ -550,11 +564,41 @@ export default function TerminalEmulator({
|
||||
resetWebViewDocument();
|
||||
}, [resetWebViewDocument]);
|
||||
|
||||
const handleWebViewTouchStart = useCallback(() => {
|
||||
const handleWebViewTouchStart = useCallback((event: GestureResponderEvent) => {
|
||||
pendingTapRef.current = {
|
||||
startX: event.nativeEvent.pageX,
|
||||
startY: event.nativeEvent.pageY,
|
||||
moved: false,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleWebViewTouchMove = useCallback((event: GestureResponderEvent) => {
|
||||
const pendingTap = pendingTapRef.current;
|
||||
if (!pendingTap) return;
|
||||
const dx = event.nativeEvent.pageX - pendingTap.startX;
|
||||
const dy = event.nativeEvent.pageY - pendingTap.startY;
|
||||
if (
|
||||
Math.abs(dx) > TERMINAL_TAP_MOVE_TOLERANCE_PX ||
|
||||
Math.abs(dy) > TERMINAL_TAP_MOVE_TOLERANCE_PX
|
||||
) {
|
||||
pendingTap.moved = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleWebViewTouchEnd = useCallback(() => {
|
||||
const pendingTap = pendingTapRef.current;
|
||||
pendingTapRef.current = null;
|
||||
if (!pendingTap || pendingTap.moved) {
|
||||
return;
|
||||
}
|
||||
webViewRef.current?.requestFocus();
|
||||
sendToWebView({ type: "focus", streamKey, forceRefocus: true });
|
||||
}, [sendToWebView, streamKey]);
|
||||
|
||||
const handleWebViewTouchCancel = useCallback(() => {
|
||||
pendingTapRef.current = null;
|
||||
}, []);
|
||||
|
||||
const backgroundColor = xtermTheme.background ?? "#0b0b0b";
|
||||
const rootStyle = useMemo<StyleProp<ViewStyle>>(
|
||||
() => [styles.root, { backgroundColor }],
|
||||
@@ -592,6 +636,9 @@ export default function TerminalEmulator({
|
||||
setDisplayZoomControls={false}
|
||||
textZoom={100}
|
||||
onTouchStart={handleWebViewTouchStart}
|
||||
onTouchMove={handleWebViewTouchMove}
|
||||
onTouchEnd={handleWebViewTouchEnd}
|
||||
onTouchCancel={handleWebViewTouchCancel}
|
||||
onMessage={handleMessage}
|
||||
onLoadStart={handleLoadStart}
|
||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||
|
||||
@@ -91,6 +91,7 @@ const sendToNative = (message: OutboundMessage): void => {
|
||||
|
||||
const TERMINAL_BACKGROUND_CSS_VAR = "--paseo-terminal-background";
|
||||
const DEFAULT_TERMINAL_BACKGROUND = "#0b0b0b";
|
||||
const TERMINAL_TAP_MOVE_TOLERANCE_PX = 8;
|
||||
|
||||
function getTerminalBackground(theme: ITheme): string {
|
||||
return theme.background ?? DEFAULT_TERMINAL_BACKGROUND;
|
||||
@@ -170,6 +171,10 @@ class TerminalWebViewBridge {
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private firedSwipe = false;
|
||||
private tapTouchIdentifier: number | null = null;
|
||||
private tapStartX = 0;
|
||||
private tapStartY = 0;
|
||||
private tapMoved = false;
|
||||
|
||||
constructor(
|
||||
private readonly root: HTMLDivElement,
|
||||
@@ -177,6 +182,9 @@ class TerminalWebViewBridge {
|
||||
) {
|
||||
this.root.addEventListener("pointerdown", this.handlePointerDown, { passive: true });
|
||||
this.root.addEventListener("touchstart", this.handleTouchStart, { passive: true });
|
||||
this.root.addEventListener("touchmove", this.handleTouchMove, { passive: true });
|
||||
this.root.addEventListener("touchend", this.handleTouchEnd, { passive: true });
|
||||
this.root.addEventListener("touchcancel", this.handleTouchCancel, { passive: true });
|
||||
this.root.addEventListener("pointermove", this.handlePointerMove, { passive: false });
|
||||
this.root.addEventListener("pointerup", this.handlePointerUp, { passive: true });
|
||||
this.root.addEventListener("pointercancel", this.handlePointerUp, { passive: true });
|
||||
@@ -370,7 +378,6 @@ class TerminalWebViewBridge {
|
||||
if (!event.isPrimary) {
|
||||
return;
|
||||
}
|
||||
this.runtime?.focus({ forceRefocus: true });
|
||||
if (!this.swipeGesturesEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -382,11 +389,44 @@ class TerminalWebViewBridge {
|
||||
};
|
||||
|
||||
private handleTouchStart = (event: TouchEvent): void => {
|
||||
if (event.touches.length === 1) {
|
||||
if (event.touches.length !== 1) {
|
||||
this.resetTap();
|
||||
return;
|
||||
}
|
||||
const touch = event.touches[0];
|
||||
this.tapTouchIdentifier = touch.identifier;
|
||||
this.tapStartX = touch.clientX;
|
||||
this.tapStartY = touch.clientY;
|
||||
this.tapMoved = false;
|
||||
};
|
||||
|
||||
private handleTouchMove = (event: TouchEvent): void => {
|
||||
const touch = this.findTrackedTouch(event.touches);
|
||||
if (!touch) {
|
||||
return;
|
||||
}
|
||||
const dx = touch.clientX - this.tapStartX;
|
||||
const dy = touch.clientY - this.tapStartY;
|
||||
if (
|
||||
Math.abs(dx) > TERMINAL_TAP_MOVE_TOLERANCE_PX ||
|
||||
Math.abs(dy) > TERMINAL_TAP_MOVE_TOLERANCE_PX
|
||||
) {
|
||||
this.tapMoved = true;
|
||||
}
|
||||
};
|
||||
|
||||
private handleTouchEnd = (event: TouchEvent): void => {
|
||||
const completedTap = Boolean(this.findTrackedTouch(event.changedTouches) && !this.tapMoved);
|
||||
this.resetTap();
|
||||
if (completedTap) {
|
||||
this.runtime?.focus({ forceRefocus: true });
|
||||
}
|
||||
};
|
||||
|
||||
private handleTouchCancel = (): void => {
|
||||
this.resetTap();
|
||||
};
|
||||
|
||||
private handlePointerMove = (event: PointerEvent): void => {
|
||||
if (!this.trackingSwipe || this.firedSwipe || !this.streamKey) {
|
||||
return;
|
||||
@@ -426,6 +466,26 @@ class TerminalWebViewBridge {
|
||||
this.startY = 0;
|
||||
this.firedSwipe = false;
|
||||
}
|
||||
|
||||
private findTrackedTouch(touches: TouchList): Touch | null {
|
||||
if (this.tapTouchIdentifier === null) {
|
||||
return null;
|
||||
}
|
||||
for (let index = 0; index < touches.length; index += 1) {
|
||||
const touch = touches.item(index);
|
||||
if (touch?.identifier === this.tapTouchIdentifier) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private resetTap(): void {
|
||||
this.tapTouchIdentifier = null;
|
||||
this.tapStartX = 0;
|
||||
this.tapStartY = 0;
|
||||
this.tapMoved = false;
|
||||
}
|
||||
}
|
||||
|
||||
installStyles();
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user