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