feat(app): keep workspace tabs alive in panes
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
deriveWorkspacePaneState,
|
deriveWorkspacePaneState,
|
||||||
getWorkspacePaneDescriptors,
|
getWorkspacePaneDescriptors,
|
||||||
} from "@/screens/workspace/workspace-pane-state";
|
} from "@/screens/workspace/workspace-pane-state";
|
||||||
|
import { useMountedTabSet } from "@/screens/workspace/use-mounted-tab-set";
|
||||||
import {
|
import {
|
||||||
WorkspacePaneContent,
|
WorkspacePaneContent,
|
||||||
type WorkspacePaneContentModel,
|
type WorkspacePaneContentModel,
|
||||||
@@ -63,6 +65,7 @@ import {
|
|||||||
type WorkspaceLayout,
|
type WorkspaceLayout,
|
||||||
} from "@/stores/workspace-layout-store";
|
} from "@/stores/workspace-layout-store";
|
||||||
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
|
||||||
|
import { workspaceTabTargetsEqual } from "@/utils/workspace-tab-identity";
|
||||||
|
|
||||||
interface SplitContainerProps {
|
interface SplitContainerProps {
|
||||||
layout: WorkspaceLayout;
|
layout: WorkspaceLayout;
|
||||||
@@ -150,6 +153,68 @@ interface SplitPaneViewProps
|
|||||||
tabDropPreview: TabDropPreview | null;
|
tabDropPreview: TabDropPreview | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MountedTabSlotProps {
|
||||||
|
tabDescriptor: WorkspaceTabDescriptor;
|
||||||
|
isVisible: boolean;
|
||||||
|
isPaneFocused: boolean;
|
||||||
|
paneId: string;
|
||||||
|
buildPaneContentModel: (input: {
|
||||||
|
paneId: string;
|
||||||
|
isPaneFocused: boolean;
|
||||||
|
tab: WorkspaceTabDescriptor;
|
||||||
|
}) => WorkspacePaneContentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MountedTabSlot = memo(function MountedTabSlot({
|
||||||
|
tabDescriptor,
|
||||||
|
isVisible,
|
||||||
|
isPaneFocused,
|
||||||
|
paneId,
|
||||||
|
buildPaneContentModel,
|
||||||
|
}: MountedTabSlotProps) {
|
||||||
|
const content = useMemo(
|
||||||
|
() =>
|
||||||
|
buildPaneContentModel({
|
||||||
|
paneId,
|
||||||
|
isPaneFocused,
|
||||||
|
tab: tabDescriptor,
|
||||||
|
}),
|
||||||
|
[buildPaneContentModel, isPaneFocused, paneId, tabDescriptor],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ display: isVisible ? "flex" : "none", flex: 1 }}>
|
||||||
|
<WorkspacePaneContent content={content} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) {
|
||||||
|
const cacheRef = useRef(new Map<string, WorkspaceTabDescriptor>());
|
||||||
|
const tabDescriptorMap = useMemo(() => {
|
||||||
|
const next = new Map<string, WorkspaceTabDescriptor>();
|
||||||
|
for (const tabDescriptor of tabDescriptors) {
|
||||||
|
const cachedDescriptor = cacheRef.current.get(tabDescriptor.tabId);
|
||||||
|
if (
|
||||||
|
cachedDescriptor &&
|
||||||
|
cachedDescriptor.key === tabDescriptor.key &&
|
||||||
|
cachedDescriptor.kind === tabDescriptor.kind &&
|
||||||
|
workspaceTabTargetsEqual(cachedDescriptor.target, tabDescriptor.target)
|
||||||
|
) {
|
||||||
|
next.set(tabDescriptor.tabId, cachedDescriptor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next.set(tabDescriptor.tabId, tabDescriptor);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}, [tabDescriptors]);
|
||||||
|
useEffect(() => {
|
||||||
|
cacheRef.current = tabDescriptorMap;
|
||||||
|
}, [tabDescriptorMap]);
|
||||||
|
|
||||||
|
return tabDescriptorMap;
|
||||||
|
}
|
||||||
|
|
||||||
const dropCollisionDetection: CollisionDetection = (args) => {
|
const dropCollisionDetection: CollisionDetection = (args) => {
|
||||||
const pointerHits = pointerWithin(args);
|
const pointerHits = pointerWithin(args);
|
||||||
const tabHits = pointerHits.filter(
|
const tabHits = pointerHits.filter(
|
||||||
@@ -725,7 +790,18 @@ function SplitPaneView({
|
|||||||
[pane, uiTabs],
|
[pane, uiTabs],
|
||||||
);
|
);
|
||||||
const paneTabs = useMemo(() => paneState.tabs.map((tab) => tab.descriptor), [paneState.tabs]);
|
const paneTabs = useMemo(() => paneState.tabs.map((tab) => tab.descriptor), [paneState.tabs]);
|
||||||
|
const paneTabIds = useMemo(() => paneTabs.map((tab) => tab.tabId), [paneTabs]);
|
||||||
|
const tabDescriptorMap = useStableTabDescriptorMap(paneTabs);
|
||||||
const activeTabDescriptor = paneState.activeTab?.descriptor ?? null;
|
const activeTabDescriptor = paneState.activeTab?.descriptor ?? null;
|
||||||
|
const { mountedTabIds } = useMountedTabSet({
|
||||||
|
activeTabId: activeTabDescriptor?.tabId ?? null,
|
||||||
|
allTabIds: paneTabIds,
|
||||||
|
cap: 3,
|
||||||
|
});
|
||||||
|
const mountedPaneTabIds = useMemo(
|
||||||
|
() => paneTabIds.filter((tabId) => mountedTabIds.has(tabId)),
|
||||||
|
[mountedTabIds, paneTabIds],
|
||||||
|
);
|
||||||
const desktopTabRowItems = useMemo<WorkspaceDesktopTabRowItem[]>(
|
const desktopTabRowItems = useMemo<WorkspaceDesktopTabRowItem[]>(
|
||||||
() =>
|
() =>
|
||||||
paneTabs.map((tab) => ({
|
paneTabs.map((tab) => ({
|
||||||
@@ -736,17 +812,6 @@ function SplitPaneView({
|
|||||||
})),
|
})),
|
||||||
[activeTabDescriptor?.key, closingTabIds, hoveredCloseTabKey, paneTabs],
|
[activeTabDescriptor?.key, closingTabIds, hoveredCloseTabKey, paneTabs],
|
||||||
);
|
);
|
||||||
const paneContent = useMemo(
|
|
||||||
() =>
|
|
||||||
activeTabDescriptor
|
|
||||||
? buildPaneContentModel({
|
|
||||||
paneId: pane.id,
|
|
||||||
isPaneFocused: isFocused,
|
|
||||||
tab: activeTabDescriptor,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
[activeTabDescriptor, buildPaneContentModel, isFocused, pane.id],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web") {
|
if (Platform.OS !== "web") {
|
||||||
@@ -823,8 +888,24 @@ function SplitPaneView({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.paneContent}>
|
<View style={styles.paneContent}>
|
||||||
{paneContent ? (
|
{mountedPaneTabIds.length > 0 ? (
|
||||||
<WorkspacePaneContent content={paneContent} />
|
mountedPaneTabIds.map((tabId) => {
|
||||||
|
const tabDescriptor = tabDescriptorMap.get(tabId);
|
||||||
|
if (!tabDescriptor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MountedTabSlot
|
||||||
|
key={tabId}
|
||||||
|
tabDescriptor={tabDescriptor}
|
||||||
|
isVisible={tabId === activeTabDescriptor?.tabId}
|
||||||
|
isPaneFocused={isFocused && tabId === activeTabDescriptor?.tabId}
|
||||||
|
paneId={pane.id}
|
||||||
|
buildPaneContentModel={buildPaneContentModel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
(renderPaneEmptyState?.() ?? null)
|
(renderPaneEmptyState?.() ?? null)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface UseMountedTabSetInput {
|
||||||
|
activeTabId: string | null;
|
||||||
|
allTabIds: string[];
|
||||||
|
cap: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseMountedTabSetResult {
|
||||||
|
mountedTabIds: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialMountedTabIds(input: UseMountedTabSetInput): Set<string> {
|
||||||
|
if (!input.activeTabId || !input.allTabIds.includes(input.activeTabId)) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
return new Set<string>([input.activeTabId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setsEqual(left: Set<string>, right: Set<string>): boolean {
|
||||||
|
if (left.size !== right.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const value of left) {
|
||||||
|
if (!right.has(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMountedTabSet(input: UseMountedTabSetInput): UseMountedTabSetResult {
|
||||||
|
const { activeTabId, allTabIds, cap } = input;
|
||||||
|
const allTabIdsKey = allTabIds.join("\u0000");
|
||||||
|
const availableTabIds = useMemo(() => new Set(allTabIds), [allTabIdsKey]);
|
||||||
|
const [mountedTabIds, setMountedTabIds] = useState(() => createInitialMountedTabIds(input));
|
||||||
|
const lruRef = useRef(
|
||||||
|
activeTabId && allTabIds.includes(activeTabId) ? [activeTabId] : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const nextLru = lruRef.current.filter((tabId) => availableTabIds.has(tabId));
|
||||||
|
if (activeTabId && availableTabIds.has(activeTabId)) {
|
||||||
|
const existingIndex = nextLru.indexOf(activeTabId);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
nextLru.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
|
nextLru.unshift(activeTabId);
|
||||||
|
}
|
||||||
|
if (nextLru.length > cap) {
|
||||||
|
nextLru.length = cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
lruRef.current = nextLru;
|
||||||
|
setMountedTabIds((previousMountedTabIds) => {
|
||||||
|
const nextMountedTabIds = new Set(nextLru);
|
||||||
|
return setsEqual(previousMountedTabIds, nextMountedTabIds)
|
||||||
|
? previousMountedTabIds
|
||||||
|
: nextMountedTabIds;
|
||||||
|
});
|
||||||
|
}, [activeTabId, availableTabIds, cap]);
|
||||||
|
|
||||||
|
return { mountedTabIds };
|
||||||
|
}
|
||||||
@@ -97,7 +97,9 @@ import { deriveWorkspacePaneState } from "@/screens/workspace/workspace-pane-sta
|
|||||||
import {
|
import {
|
||||||
buildWorkspacePaneContentModel,
|
buildWorkspacePaneContentModel,
|
||||||
WorkspacePaneContent,
|
WorkspacePaneContent,
|
||||||
|
type WorkspacePaneContentModel,
|
||||||
} from "@/screens/workspace/workspace-pane-content";
|
} from "@/screens/workspace/workspace-pane-content";
|
||||||
|
import { useMountedTabSet } from "@/screens/workspace/use-mounted-tab-set";
|
||||||
import {
|
import {
|
||||||
buildBulkCloseConfirmationMessage,
|
buildBulkCloseConfirmationMessage,
|
||||||
classifyBulkClosableTabs,
|
classifyBulkClosableTabs,
|
||||||
@@ -438,6 +440,68 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface MobileMountedTabSlotProps {
|
||||||
|
tabDescriptor: WorkspaceTabDescriptor;
|
||||||
|
isVisible: boolean;
|
||||||
|
isPaneFocused: boolean;
|
||||||
|
paneId: string | null;
|
||||||
|
buildPaneContentModel: (input: {
|
||||||
|
paneId: string | null;
|
||||||
|
isPaneFocused: boolean;
|
||||||
|
tab: WorkspaceTabDescriptor;
|
||||||
|
}) => WorkspacePaneContentModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileMountedTabSlot = memo(function MobileMountedTabSlot({
|
||||||
|
tabDescriptor,
|
||||||
|
isVisible,
|
||||||
|
isPaneFocused,
|
||||||
|
paneId,
|
||||||
|
buildPaneContentModel,
|
||||||
|
}: MobileMountedTabSlotProps) {
|
||||||
|
const content = useMemo(
|
||||||
|
() =>
|
||||||
|
buildPaneContentModel({
|
||||||
|
paneId,
|
||||||
|
isPaneFocused,
|
||||||
|
tab: tabDescriptor,
|
||||||
|
}),
|
||||||
|
[buildPaneContentModel, isPaneFocused, paneId, tabDescriptor],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ display: isVisible ? "flex" : "none", flex: 1 }}>
|
||||||
|
<WorkspacePaneContent content={content} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function useStableTabDescriptorMap(tabDescriptors: WorkspaceTabDescriptor[]) {
|
||||||
|
const cacheRef = useRef(new Map<string, WorkspaceTabDescriptor>());
|
||||||
|
const tabDescriptorMap = useMemo(() => {
|
||||||
|
const next = new Map<string, WorkspaceTabDescriptor>();
|
||||||
|
for (const tabDescriptor of tabDescriptors) {
|
||||||
|
const cachedDescriptor = cacheRef.current.get(tabDescriptor.tabId);
|
||||||
|
if (
|
||||||
|
cachedDescriptor &&
|
||||||
|
cachedDescriptor.key === tabDescriptor.key &&
|
||||||
|
cachedDescriptor.kind === tabDescriptor.kind &&
|
||||||
|
workspaceTabTargetsEqual(cachedDescriptor.target, tabDescriptor.target)
|
||||||
|
) {
|
||||||
|
next.set(tabDescriptor.tabId, cachedDescriptor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next.set(tabDescriptor.tabId, tabDescriptor);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}, [tabDescriptors]);
|
||||||
|
useEffect(() => {
|
||||||
|
cacheRef.current = tabDescriptorMap;
|
||||||
|
}, [tabDescriptorMap]);
|
||||||
|
|
||||||
|
return tabDescriptorMap;
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkspaceScreen({ serverId, workspaceId }: WorkspaceScreenProps) {
|
export function WorkspaceScreen({ serverId, workspaceId }: WorkspaceScreenProps) {
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
|
|
||||||
@@ -1726,37 +1790,33 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
retargetWorkspaceTab,
|
retargetWorkspaceTab,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const activePaneContent = useMemo(
|
const focusedPaneId = focusedPaneTabState.pane?.id ?? null;
|
||||||
() =>
|
const focusedPaneTabIds = useMemo(() => tabs.map((tab) => tab.tabId), [tabs]);
|
||||||
activeTabDescriptor
|
const focusedPaneTabDescriptorMap = useStableTabDescriptorMap(tabs);
|
||||||
? buildPaneContentModel({
|
const { mountedTabIds: mountedFocusedPaneTabIdsSet } = useMountedTabSet({
|
||||||
tab: activeTabDescriptor,
|
activeTabId: activeTabDescriptor?.tabId ?? null,
|
||||||
paneId: focusedPaneTabState.pane?.id ?? null,
|
allTabIds: focusedPaneTabIds,
|
||||||
isPaneFocused: true,
|
cap: 3,
|
||||||
focusPaneBeforeOpen: false,
|
});
|
||||||
})
|
const mountedFocusedPaneTabIds = useMemo(
|
||||||
: null,
|
() => focusedPaneTabIds.filter((tabId) => mountedFocusedPaneTabIdsSet.has(tabId)),
|
||||||
[activeTabDescriptor, buildPaneContentModel, focusedPaneTabState.pane?.id],
|
[focusedPaneTabIds, mountedFocusedPaneTabIdsSet],
|
||||||
|
);
|
||||||
|
const buildMobilePaneContentModel = useCallback(
|
||||||
|
function buildMobilePaneContentModel(input: {
|
||||||
|
paneId: string | null;
|
||||||
|
tab: WorkspaceTabDescriptor;
|
||||||
|
isPaneFocused: boolean;
|
||||||
|
}) {
|
||||||
|
return buildPaneContentModel({
|
||||||
|
tab: input.tab,
|
||||||
|
paneId: input.paneId,
|
||||||
|
isPaneFocused: input.isPaneFocused,
|
||||||
|
focusPaneBeforeOpen: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[buildPaneContentModel],
|
||||||
);
|
);
|
||||||
const prevActivePaneDeps = useRef({
|
|
||||||
activeTabDescriptor,
|
|
||||||
buildPaneContentModel,
|
|
||||||
paneId: focusedPaneTabState.pane?.id,
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
const prev = prevActivePaneDeps.current;
|
|
||||||
if (prev.activeTabDescriptor !== activeTabDescriptor)
|
|
||||||
console.log("[activePaneContent] activeTabDescriptor changed");
|
|
||||||
if (prev.buildPaneContentModel !== buildPaneContentModel)
|
|
||||||
console.log("[activePaneContent] buildPaneContentModel changed");
|
|
||||||
if (prev.paneId !== focusedPaneTabState.pane?.id)
|
|
||||||
console.log("[activePaneContent] paneId changed");
|
|
||||||
prevActivePaneDeps.current = {
|
|
||||||
activeTabDescriptor,
|
|
||||||
buildPaneContentModel,
|
|
||||||
paneId: focusedPaneTabState.pane?.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const content = shouldRenderMissingWorkspaceDescriptor({
|
const content = shouldRenderMissingWorkspaceDescriptor({
|
||||||
workspace: workspaceDescriptor,
|
workspace: workspaceDescriptor,
|
||||||
hasHydratedWorkspaces,
|
hasHydratedWorkspaces,
|
||||||
@@ -1777,7 +1837,23 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<WorkspacePaneContent content={activePaneContent!} />
|
mountedFocusedPaneTabIds.map((tabId) => {
|
||||||
|
const tabDescriptor = focusedPaneTabDescriptorMap.get(tabId);
|
||||||
|
if (!tabDescriptor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MobileMountedTabSlot
|
||||||
|
key={tabId}
|
||||||
|
tabDescriptor={tabDescriptor}
|
||||||
|
isVisible={tabId === activeTabDescriptor.tabId}
|
||||||
|
isPaneFocused={tabId === activeTabDescriptor.tabId}
|
||||||
|
paneId={focusedPaneId}
|
||||||
|
buildPaneContentModel={buildMobilePaneContentModel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildDesktopPaneContentModel = useCallback(
|
const buildDesktopPaneContentModel = useCallback(
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
|
import { ImageAddon } from "@xterm/addon-image";
|
||||||
|
import { LigaturesAddon } from "@xterm/addon-ligatures";
|
||||||
|
import { SearchAddon } from "@xterm/addon-search";
|
||||||
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
import { Unicode11Addon } from "@xterm/addon-unicode11";
|
||||||
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
||||||
import { WebglAddon } from "@xterm/addon-webgl";
|
import { WebglAddon } from "@xterm/addon-webgl";
|
||||||
import { Terminal, type ITheme } from "@xterm/xterm";
|
import { Terminal, type ITheme } from "@xterm/xterm";
|
||||||
import type { TerminalState } from "@server/shared/messages";
|
import type { TerminalState } from "@server/shared/messages";
|
||||||
@@ -149,6 +154,9 @@ export class TerminalEmulatorRuntime {
|
|||||||
fontFamily: DEFAULT_TERMINAL_FONT_FAMILY,
|
fontFamily: DEFAULT_TERMINAL_FONT_FAMILY,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
lineHeight: 1.0,
|
lineHeight: 1.0,
|
||||||
|
macOptionIsMeta: true,
|
||||||
|
minimumContrastRatio: 1,
|
||||||
|
rescaleOverlappingGlyphs: true,
|
||||||
overviewRuler: {
|
overviewRuler: {
|
||||||
width: 8,
|
width: 8,
|
||||||
},
|
},
|
||||||
@@ -160,6 +168,14 @@ export class TerminalEmulatorRuntime {
|
|||||||
let webglAddon: WebglAddon | null = null;
|
let webglAddon: WebglAddon | null = null;
|
||||||
terminal.loadAddon(fitAddon);
|
terminal.loadAddon(fitAddon);
|
||||||
terminal.loadAddon(unicode11Addon);
|
terminal.loadAddon(unicode11Addon);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.loadAddon(new SearchAddon({ highlightLimit: 20_000 }));
|
||||||
|
terminal.loadAddon(new ClipboardAddon());
|
||||||
|
try {
|
||||||
|
terminal.loadAddon(new LigaturesAddon());
|
||||||
|
} catch {
|
||||||
|
// Ligatures require Font Access API or compatible environment
|
||||||
|
}
|
||||||
terminal.open(input.host);
|
terminal.open(input.host);
|
||||||
try {
|
try {
|
||||||
terminal.unicode.activeVersion = "11";
|
terminal.unicode.activeVersion = "11";
|
||||||
@@ -193,6 +209,8 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
terminal.loadAddon(new ImageAddon());
|
||||||
|
|
||||||
const restoreDocumentStyles = this.applyDocumentBoundsStyles({
|
const restoreDocumentStyles = this.applyDocumentBoundsStyles({
|
||||||
root: input.root,
|
root: input.root,
|
||||||
});
|
});
|
||||||
@@ -211,6 +229,10 @@ export class TerminalEmulatorRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.root.offsetWidth === 0 || input.root.offsetHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentFitAddon.fit();
|
currentFitAddon.fit();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -657,7 +679,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
|
|
||||||
private applyViewportTouchStyles(input: { host: HTMLDivElement }): () => void {
|
private applyViewportTouchStyles(input: { host: HTMLDivElement }): () => void {
|
||||||
const viewportElement = input.host.querySelector<HTMLElement>(".xterm-viewport");
|
const viewportElement = input.host.querySelector<HTMLElement>(".xterm-viewport");
|
||||||
const screenElement = input.host.querySelector<HTMLElement>(".xterm-screen");
|
|
||||||
|
|
||||||
const previousViewportOverscroll = viewportElement?.style.overscrollBehavior ?? "";
|
const previousViewportOverscroll = viewportElement?.style.overscrollBehavior ?? "";
|
||||||
const previousViewportTouchAction = viewportElement?.style.touchAction ?? "";
|
const previousViewportTouchAction = viewportElement?.style.touchAction ?? "";
|
||||||
@@ -666,8 +687,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
const previousViewportPointerEvents = viewportElement?.style.pointerEvents ?? "";
|
const previousViewportPointerEvents = viewportElement?.style.pointerEvents ?? "";
|
||||||
const previousViewportWebkitOverflowScrolling =
|
const previousViewportWebkitOverflowScrolling =
|
||||||
viewportElement?.style.getPropertyValue("-webkit-overflow-scrolling") ?? "";
|
viewportElement?.style.getPropertyValue("-webkit-overflow-scrolling") ?? "";
|
||||||
const previousScreenPointerEvents = screenElement?.style.pointerEvents ?? "";
|
|
||||||
|
|
||||||
if (viewportElement) {
|
if (viewportElement) {
|
||||||
viewportElement.style.overscrollBehavior = "none";
|
viewportElement.style.overscrollBehavior = "none";
|
||||||
viewportElement.style.touchAction = "pan-y";
|
viewportElement.style.touchAction = "pan-y";
|
||||||
@@ -676,9 +695,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
viewportElement.style.pointerEvents = "auto";
|
viewportElement.style.pointerEvents = "auto";
|
||||||
viewportElement.style.setProperty("-webkit-overflow-scrolling", "touch");
|
viewportElement.style.setProperty("-webkit-overflow-scrolling", "touch");
|
||||||
}
|
}
|
||||||
if (screenElement) {
|
|
||||||
screenElement.style.pointerEvents = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (viewportElement) {
|
if (viewportElement) {
|
||||||
@@ -692,9 +708,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
previousViewportWebkitOverflowScrolling,
|
previousViewportWebkitOverflowScrolling,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (screenElement) {
|
|
||||||
screenElement.style.pointerEvents = previousScreenPointerEvents;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user