Fix mobile terminal focus and background
This commit is contained in:
@@ -80,7 +80,7 @@ type BridgeInboundMessage =
|
|||||||
| { type: "restoreOutput"; streamKey: string; text: string }
|
| { type: "restoreOutput"; streamKey: string; text: string }
|
||||||
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
||||||
| { type: "clear"; streamKey: string }
|
| { type: "clear"; streamKey: string }
|
||||||
| { type: "focus"; streamKey: string }
|
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
||||||
| { type: "resize"; streamKey: string }
|
| { type: "resize"; streamKey: string }
|
||||||
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
||||||
| { type: "setScrollback"; streamKey: string; lines: number }
|
| { type: "setScrollback"; streamKey: string; lines: number }
|
||||||
@@ -543,19 +543,33 @@ export default function TerminalEmulator({
|
|||||||
resetWebViewDocument();
|
resetWebViewDocument();
|
||||||
}, [resetWebViewDocument]);
|
}, [resetWebViewDocument]);
|
||||||
|
|
||||||
|
const handleWebViewTouchStart = useCallback(() => {
|
||||||
|
webViewRef.current?.requestFocus();
|
||||||
|
sendToWebView({ type: "focus", streamKey, forceRefocus: true });
|
||||||
|
}, [sendToWebView, streamKey]);
|
||||||
|
|
||||||
|
const backgroundColor = xtermTheme.background ?? "#0b0b0b";
|
||||||
|
const rootStyle = useMemo<StyleProp<ViewStyle>>(
|
||||||
|
() => [styles.root, { backgroundColor }],
|
||||||
|
[backgroundColor],
|
||||||
|
);
|
||||||
const webViewStyle = useMemo<StyleProp<ViewStyle>>(
|
const webViewStyle = useMemo<StyleProp<ViewStyle>>(
|
||||||
() => [styles.webView, { backgroundColor: xtermTheme.background ?? "#0b0b0b" }],
|
() => [styles.webView, { backgroundColor }],
|
||||||
[xtermTheme.background],
|
[backgroundColor],
|
||||||
|
);
|
||||||
|
const webViewContainerStyle = useMemo<StyleProp<ViewStyle>>(
|
||||||
|
() => [styles.webViewContainer, { backgroundColor }],
|
||||||
|
[backgroundColor],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root} testID={testId}>
|
<View style={rootStyle} testID={testId}>
|
||||||
<WebView
|
<WebView
|
||||||
key={webViewEpoch}
|
key={webViewEpoch}
|
||||||
ref={webViewRef}
|
ref={webViewRef}
|
||||||
source={TERMINAL_WEBVIEW_SOURCE}
|
source={TERMINAL_WEBVIEW_SOURCE}
|
||||||
style={webViewStyle}
|
style={webViewStyle}
|
||||||
containerStyle={styles.webViewContainer}
|
containerStyle={webViewContainerStyle}
|
||||||
originWhitelist={TERMINAL_WEBVIEW_ORIGIN_WHITELIST}
|
originWhitelist={TERMINAL_WEBVIEW_ORIGIN_WHITELIST}
|
||||||
scrollEnabled
|
scrollEnabled
|
||||||
nestedScrollEnabled
|
nestedScrollEnabled
|
||||||
@@ -564,12 +578,13 @@ export default function TerminalEmulator({
|
|||||||
keyboardDisplayRequiresUserAction={false}
|
keyboardDisplayRequiresUserAction={false}
|
||||||
automaticallyAdjustContentInsets={false}
|
automaticallyAdjustContentInsets={false}
|
||||||
contentInsetAdjustmentBehavior="never"
|
contentInsetAdjustmentBehavior="never"
|
||||||
textInteractionEnabled={false}
|
textInteractionEnabled
|
||||||
allowsLinkPreview={false}
|
allowsLinkPreview={false}
|
||||||
setSupportMultipleWindows={false}
|
setSupportMultipleWindows={false}
|
||||||
setBuiltInZoomControls={false}
|
setBuiltInZoomControls={false}
|
||||||
setDisplayZoomControls={false}
|
setDisplayZoomControls={false}
|
||||||
textZoom={100}
|
textZoom={100}
|
||||||
|
onTouchStart={handleWebViewTouchStart}
|
||||||
onMessage={handleMessage}
|
onMessage={handleMessage}
|
||||||
onLoadStart={handleLoadStart}
|
onLoadStart={handleLoadStart}
|
||||||
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
onContentProcessDidTerminate={handleContentProcessDidTerminate}
|
||||||
@@ -585,14 +600,11 @@ const styles = StyleSheet.create({
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#0b0b0b",
|
|
||||||
},
|
},
|
||||||
webView: {
|
webView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#0b0b0b",
|
|
||||||
},
|
},
|
||||||
webViewContainer: {
|
webViewContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#0b0b0b",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
private suppressInput = false;
|
private suppressInput = false;
|
||||||
private readonly inputModeTracker = new TerminalInputModeTracker();
|
private readonly inputModeTracker = new TerminalInputModeTracker();
|
||||||
private lastInputModeState: TerminalInputModeState = this.inputModeTracker.getState();
|
private lastInputModeState: TerminalInputModeState = this.inputModeTracker.getState();
|
||||||
|
private themeBackgroundElements: HTMLElement[] = [];
|
||||||
|
|
||||||
private handleVisibilityRestore = (): void => {
|
private handleVisibilityRestore = (): void => {
|
||||||
if (typeof document !== "undefined" && document.visibilityState !== "visible") {
|
if (typeof document !== "undefined" && document.visibilityState !== "visible") {
|
||||||
@@ -252,6 +253,8 @@ export class TerminalEmulatorRuntime {
|
|||||||
// Ligatures require Font Access API or compatible environment
|
// Ligatures require Font Access API or compatible environment
|
||||||
}
|
}
|
||||||
terminal.open(input.host);
|
terminal.open(input.host);
|
||||||
|
this.themeBackgroundElements = this.collectThemeBackgroundElements(input);
|
||||||
|
this.applyThemeBackground(input.theme);
|
||||||
try {
|
try {
|
||||||
terminal.unicode.activeVersion = "11";
|
terminal.unicode.activeVersion = "11";
|
||||||
} catch {
|
} catch {
|
||||||
@@ -646,6 +649,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.applyThemeBackground(input.theme);
|
||||||
this.refreshVisibleRows();
|
this.refreshVisibleRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,8 +669,15 @@ export class TerminalEmulatorRuntime {
|
|||||||
this.refreshVisibleRows();
|
this.refreshVisibleRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
focus(): void {
|
focus(input?: { forceRefocus?: boolean }): void {
|
||||||
this.terminal?.focus();
|
const terminal = this.terminal;
|
||||||
|
if (!terminal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input?.forceRefocus) {
|
||||||
|
terminal.blur();
|
||||||
|
}
|
||||||
|
terminal.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshVisibleRows(): void {
|
private refreshVisibleRows(): void {
|
||||||
@@ -682,6 +693,26 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private collectThemeBackgroundElements(input: {
|
||||||
|
root: HTMLDivElement;
|
||||||
|
host: HTMLDivElement;
|
||||||
|
}): HTMLElement[] {
|
||||||
|
return [
|
||||||
|
input.root,
|
||||||
|
input.host,
|
||||||
|
...Array.from(
|
||||||
|
input.host.querySelectorAll<HTMLElement>(".xterm, .xterm-screen, .xterm-viewport"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyThemeBackground(theme: ITheme): void {
|
||||||
|
const background = theme.background ?? "#0b0b0b";
|
||||||
|
for (const element of this.themeBackgroundElements) {
|
||||||
|
element.style.backgroundColor = background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
unmount(): void {
|
unmount(): void {
|
||||||
this.clearInFlightOutputTimeout();
|
this.clearInFlightOutputTimeout();
|
||||||
const inFlightOperation = this.inFlightOutputOperation;
|
const inFlightOperation = this.inFlightOutputOperation;
|
||||||
@@ -703,6 +734,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
this.fitAddon = null;
|
this.fitAddon = null;
|
||||||
this.fitAndEmitResize = null;
|
this.fitAndEmitResize = null;
|
||||||
this.lastSize = null;
|
this.lastSize = null;
|
||||||
|
this.themeBackgroundElements = [];
|
||||||
this.suppressInput = false;
|
this.suppressInput = false;
|
||||||
this.inputModeDecoder.decode();
|
this.inputModeDecoder.decode();
|
||||||
this.inputModeTracker.reset();
|
this.inputModeTracker.reset();
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ type InboundMessage =
|
|||||||
| { type: "restoreOutput"; streamKey: string; text: string }
|
| { type: "restoreOutput"; streamKey: string; text: string }
|
||||||
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
||||||
| { type: "clear"; streamKey: string }
|
| { type: "clear"; streamKey: string }
|
||||||
| { type: "focus"; streamKey: string }
|
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
||||||
| { type: "resize"; streamKey: string }
|
| { type: "resize"; streamKey: string }
|
||||||
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
||||||
| { type: "setScrollback"; streamKey: string; lines: number }
|
| { type: "setScrollback"; streamKey: string; lines: number }
|
||||||
@@ -88,10 +88,20 @@ const sendToNative = (message: OutboundMessage): void => {
|
|||||||
window.ReactNativeWebView?.postMessage?.(JSON.stringify(message));
|
window.ReactNativeWebView?.postMessage?.(JSON.stringify(message));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TERMINAL_BACKGROUND_CSS_VAR = "--paseo-terminal-background";
|
||||||
|
const DEFAULT_TERMINAL_BACKGROUND = "#0b0b0b";
|
||||||
|
|
||||||
|
function getTerminalBackground(theme: ITheme): string {
|
||||||
|
return theme.background ?? DEFAULT_TERMINAL_BACKGROUND;
|
||||||
|
}
|
||||||
|
|
||||||
const installStyles = (): void => {
|
const installStyles = (): void => {
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
${xtermCss}
|
${xtermCss}
|
||||||
|
:root {
|
||||||
|
${TERMINAL_BACKGROUND_CSS_VAR}: transparent;
|
||||||
|
}
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#terminal-root {
|
#terminal-root {
|
||||||
@@ -101,7 +111,7 @@ body,
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
background: #0b0b0b;
|
background: var(${TERMINAL_BACKGROUND_CSS_VAR});
|
||||||
}
|
}
|
||||||
#terminal-root {
|
#terminal-root {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -115,6 +125,12 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: var(${TERMINAL_BACKGROUND_CSS_VAR});
|
||||||
|
}
|
||||||
|
#terminal-root .xterm,
|
||||||
|
#terminal-root .xterm-screen,
|
||||||
|
#terminal-root .xterm-viewport {
|
||||||
|
background-color: var(${TERMINAL_BACKGROUND_CSS_VAR}) !important;
|
||||||
}
|
}
|
||||||
[data-terminal-scrollbar-root="true"] .xterm-viewport {
|
[data-terminal-scrollbar-root="true"] .xterm-viewport {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -124,6 +140,17 @@ body,
|
|||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
#terminal-root .xterm .xterm-helper-textarea {
|
||||||
|
opacity: 0.01;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
min-width: 1px;
|
||||||
|
min-height: 1px;
|
||||||
|
color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
caret-color: transparent;
|
||||||
|
z-index: 5 !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
};
|
};
|
||||||
@@ -148,6 +175,7 @@ class TerminalWebViewBridge {
|
|||||||
private readonly host: HTMLDivElement,
|
private readonly host: HTMLDivElement,
|
||||||
) {
|
) {
|
||||||
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("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 });
|
||||||
@@ -204,12 +232,13 @@ class TerminalWebViewBridge {
|
|||||||
this.runtime?.clear();
|
this.runtime?.clear();
|
||||||
break;
|
break;
|
||||||
case "focus":
|
case "focus":
|
||||||
this.runtime?.focus();
|
this.runtime?.focus({ forceRefocus: message.forceRefocus });
|
||||||
break;
|
break;
|
||||||
case "resize":
|
case "resize":
|
||||||
this.runtime?.resize({ force: true });
|
this.runtime?.resize({ force: true });
|
||||||
break;
|
break;
|
||||||
case "setTheme":
|
case "setTheme":
|
||||||
|
this.applyThemeBackground(message.theme);
|
||||||
this.runtime?.setTheme({ theme: message.theme });
|
this.runtime?.setTheme({ theme: message.theme });
|
||||||
break;
|
break;
|
||||||
case "setScrollback":
|
case "setScrollback":
|
||||||
@@ -228,7 +257,7 @@ class TerminalWebViewBridge {
|
|||||||
this.unmount(this.streamKey);
|
this.unmount(this.streamKey);
|
||||||
this.streamKey = message.streamKey;
|
this.streamKey = message.streamKey;
|
||||||
this.swipeGesturesEnabled = message.swipeGesturesEnabled;
|
this.swipeGesturesEnabled = message.swipeGesturesEnabled;
|
||||||
document.body.style.backgroundColor = message.theme.background ?? "#0b0b0b";
|
this.applyThemeBackground(message.theme);
|
||||||
|
|
||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
@@ -266,6 +295,14 @@ class TerminalWebViewBridge {
|
|||||||
sendToNative({ type: "rendererReady", streamKey: message.streamKey, isReady: true });
|
sendToNative({ type: "rendererReady", streamKey: message.streamKey, isReady: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyThemeBackground(theme: ITheme): void {
|
||||||
|
const background = getTerminalBackground(theme);
|
||||||
|
document.documentElement.style.setProperty(TERMINAL_BACKGROUND_CSS_VAR, background);
|
||||||
|
document.body.style.backgroundColor = background;
|
||||||
|
this.root.style.backgroundColor = background;
|
||||||
|
this.host.style.backgroundColor = background;
|
||||||
|
}
|
||||||
|
|
||||||
private unmount(streamKey: string | null): void {
|
private unmount(streamKey: string | null): void {
|
||||||
if (!this.runtime) {
|
if (!this.runtime) {
|
||||||
return;
|
return;
|
||||||
@@ -325,7 +362,11 @@ class TerminalWebViewBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handlePointerDown = (event: PointerEvent): void => {
|
private handlePointerDown = (event: PointerEvent): void => {
|
||||||
if (!this.swipeGesturesEnabled || !event.isPrimary) {
|
if (!event.isPrimary) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.runtime?.focus({ forceRefocus: true });
|
||||||
|
if (!this.swipeGesturesEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.trackingSwipe = true;
|
this.trackingSwipe = true;
|
||||||
@@ -335,6 +376,12 @@ class TerminalWebViewBridge {
|
|||||||
this.startY = event.clientY;
|
this.startY = event.clientY;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleTouchStart = (event: TouchEvent): void => {
|
||||||
|
if (event.touches.length === 1) {
|
||||||
|
this.runtime?.focus({ forceRefocus: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user