Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-tooltips-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@codegraphy-dev/extension": patch
---

Stop stationary Node tooltips and hidden Graph Views from running graph frames.
5 changes: 5 additions & 0 deletions packages/extension/src/extension/graphView/editorPanel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import { publishGraphViewVisibility } from './webview/visibility';

interface OpenGraphViewInEditorOptions {
viewType: string;
Expand Down Expand Up @@ -46,7 +47,11 @@ export function openGraphViewInEditor({
};
setWebviewMessageListener(panel.webview);
panel.webview.html = getHtmlForWebview(panel.webview);
publishGraphViewVisibility(panel.webview, panel.visible);
registerPanel(panel);
panel.onDidChangeViewState(({ webviewPanel }) => {
publishGraphViewVisibility(webviewPanel.webview, webviewPanel.visible);
});
panel.onDidDispose(() => {
unregisterPanel(panel);
});
Expand Down
5 changes: 5 additions & 0 deletions packages/extension/src/extension/graphView/webview/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { publishGraphViewVisibility } from './visibility';

interface GraphViewWebviewLike {
options: {
enableScripts?: boolean;
localResourceRoots?: readonly unknown[];
retainContextWhenHidden?: boolean;
};
html: string;
postMessage(message: unknown): unknown;
}

interface GraphViewWebviewViewLike {
Expand Down Expand Up @@ -42,8 +45,10 @@ export function resolveGraphViewWebviewView(
webviewView.webview.html = html;

void executeCommand('setContext', 'codegraphy.viewVisible', webviewView.visible);
publishGraphViewVisibility(webviewView.webview, webviewView.visible);

webviewView.onDidChangeVisibility(() => {
void executeCommand('setContext', 'codegraphy.viewVisible', webviewView.visible);
publishGraphViewVisibility(webviewView.webview, webviewView.visible);
});
}
13 changes: 13 additions & 0 deletions packages/extension/src/extension/graphView/webview/visibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface VisibilityWebview {
postMessage(message: unknown): unknown;
}

export function publishGraphViewVisibility(
webview: VisibilityWebview,
visible: boolean,
): void {
void webview.postMessage({
type: 'GRAPH_VIEW_VISIBILITY_UPDATED',
payload: { visible },
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface IGraphNodeMetricsUpdate {

export type ExtensionToWebviewMessage =
| { type: 'GRAPH_DATA_UPDATED'; payload: IGraphData }
| { type: 'GRAPH_VIEW_VISIBILITY_UPDATED'; payload: { visible: boolean } }
| { type: 'GRAPH_NODE_METRICS_UPDATED'; payload: { nodes: IGraphNodeMetricsUpdate[] } }
| { type: 'APP_BOOTSTRAP_COMPLETE' }
| {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface GraphContextMenuOpeningOptions {
>;
setContextSelection: Dispatch<SetStateAction<GraphContextSelection>>;
setTooltipData: GraphContextMenuRuntimeDependencies<FGNode>['setTooltipData'];
stopTooltipTracking: GraphContextMenuRuntimeDependencies<FGNode>['stopTooltipTracking'];
clearTooltipAnchor: GraphContextMenuRuntimeDependencies<FGNode>['clearTooltipAnchor'];
tooltipTimeoutRef: GraphContextMenuRuntimeDependencies<FGNode>['tooltipTimeoutRef'];
}

Expand All @@ -75,7 +75,7 @@ function createGraphContextMenuOpeningDependencies({
refs,
setContextSelection,
setTooltipData,
stopTooltipTracking,
clearTooltipAnchor,
tooltipTimeoutRef,
}: Omit<GraphContextMenuOpeningOptions, 'getActionContext'>): GraphContextMenuRuntimeDependencies<FGNode> {
return {
Expand All @@ -99,7 +99,7 @@ function createGraphContextMenuOpeningDependencies({
},
setContextSelection,
setTooltipData,
stopTooltipTracking,
clearTooltipAnchor,
toggleFavoritesOptimistically: paths =>
graphStore.getState().toggleFavoritesOptimistically(paths),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface GraphContextMenuRuntimeDependencies<THoveredNode = unknown> {
refreshContextSelection?(): void;
setContextSelection(selection: GraphContextSelection): void;
setTooltipData(updater: (previousState: GraphTooltipState) => GraphTooltipState): void;
stopTooltipTracking(): void;
clearTooltipAnchor(): void;
toggleFavoritesOptimistically?(paths: readonly string[]): void;
now?(): number;
fallbackDelayMs?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type GraphContextMenuTooltipDependencies = Pick<
| 'tooltipTimeoutRef'
| 'setContextSelection'
| 'setTooltipData'
| 'stopTooltipTracking'
| 'clearTooltipAnchor'
> & Partial<Pick<
GraphContextMenuRuntimeDependencies,
| 'now'
Expand Down Expand Up @@ -42,7 +42,7 @@ export function createContextMenuTooltipRuntime(
}

dependencies.hoveredNodeRef.current = null;
dependencies.stopTooltipTracking();
dependencies.clearTooltipAnchor();
dependencies.setTooltipData(hideGraphTooltipState);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ export type OwnedGraphFrameLoopRuntime = Omit<

export interface OwnedGraphFrameLoop {
dispose(): void;
setHostVisible(visible: boolean): void;
}

function canScheduleFrame(runtime: OwnedGraphFrameLoopRuntime, active: boolean): boolean {
function canScheduleFrame(
runtime: OwnedGraphFrameLoopRuntime,
active: boolean,
visible: boolean,
): boolean {
const renderer = runtime.gpuRendererRef.current;
return active
&& visible
&& runtime.animationFrameRef.current === null
&& (!renderer || renderer.canRender());
}
Expand All @@ -36,6 +42,9 @@ export function startOwnedGraphFrameLoop(
controlsRef: MutableRefObject<OwnedGraph2dControls | undefined>,
): OwnedGraphFrameLoop {
let active = true;
let hostVisible = runtime.propsRef.current.graphViewVisible;
let documentVisible = document.visibilityState !== 'hidden';
const isVisible = (): boolean => hostVisible && documentVisible;
const frameRuntime: OwnedGraphFrameRuntime = {
...runtime,
recordRenderedFrame(submissionId, timestamp, simulationMs, renderMs) {
Expand All @@ -54,17 +63,34 @@ export function startOwnedGraphFrameLoop(

const renderFrame = (timestamp: number): void => {
runtime.animationFrameRef.current = null;
if (!active || !isVisible()) return;
runtime.frameRequestedRef.current = false;
if (!active) return;
renderOwnedGraphFrame(frameRuntime, canvas, timestamp);
};

const scheduleRequestedFrame = (): void => {
if (canScheduleFrame(runtime, active, isVisible())) {
runtime.animationFrameRef.current = window.requestAnimationFrame(renderFrame);
}
};

runtime.requestFrameRef.current = () => {
runtime.frameRequestedRef.current = true;
if (canScheduleFrame(runtime, active)) {
runtime.animationFrameRef.current = window.requestAnimationFrame(renderFrame);
scheduleRequestedFrame();
};

const applyVisibility = (): void => {
if (!isVisible() && runtime.animationFrameRef.current !== null) {
window.cancelAnimationFrame(runtime.animationFrameRef.current);
runtime.animationFrameRef.current = null;
}
if (isVisible() && runtime.frameRequestedRef.current) scheduleRequestedFrame();
};
const handleVisibilityChange = (): void => {
documentVisible = document.visibilityState !== 'hidden';
applyVisibility();
};
document.addEventListener('visibilitychange', handleVisibilityChange);

const controls = createOwnedGraphControls(runtime, canvas);
controlsRef.current = controls;
Expand All @@ -76,8 +102,13 @@ export function startOwnedGraphFrameLoop(
runtime.requestFrameRef.current();

return {
setHostVisible: (visible) => {
hostVisible = visible;
applyVisibility();
},
dispose: () => {
active = false;
document.removeEventListener('visibilitychange', handleVisibilityChange);
resizeObserver.disconnect();
stopObservingDevicePixelRatio();
if (runtime.animationFrameRef.current !== null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface Surface2dProps {
directionMode: DirectionMode;
fg2dRef: MutableRefObject<OwnedGraph2dControls | undefined>;
graphViewContributions?: ExtensionGraphViewContributionSet;
graphViewVisible: boolean;
getBaseLinkColor: (this: void, link: FGLink) => string;
getBaseLinkOpacity: (this: void, link: FGLink) => number;
getBaseLinkWidth: (this: void, link: FGLink) => number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
type MutableRefObject,
Expand Down Expand Up @@ -155,6 +156,7 @@ export function OwnedGraphSurface2d(props: Surface2dProps): ReactElement {
createOwnedGraphPerformanceMonitor,
);
const requestFrameRef = useRef<() => void>(NOOP);
const frameLoopRef = useRef<ReturnType<typeof startOwnedGraphFrameLoop> | null>(null);
const contextGestureSessionRef = useRef<ContextGestureSession | null>(null);
const pointerSessionRef = useRef<PointerSession | null>(null);
const hoveredNodeRef = useRef<FGNode | null>(null);
Expand Down Expand Up @@ -254,7 +256,11 @@ export function OwnedGraphSurface2d(props: Surface2dProps): ReactElement {
},
};
const frameLoop = startOwnedGraphFrameLoop(frameLoopRuntime, canvas, props.fg2dRef);
return () => frameLoop.dispose();
frameLoopRef.current = frameLoop;
return () => {
frameLoopRef.current = null;
frameLoop.dispose();
};
}, [
cameraRef,
clearLinkHover,
Expand All @@ -266,6 +272,10 @@ export function OwnedGraphSurface2d(props: Surface2dProps): ReactElement {
simulationClockRef,
]);

useLayoutEffect(() => {
frameLoopRef.current?.setHostVisible(props.graphViewVisible);
}, [props.graphViewVisible]);

useEffect(() => {
clearLinkHover();
reconcileOwnedGraphRuntime(layoutRuntime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ export interface TooltipHoverOptions {
pluginHost?: WebviewPluginHost;
postMessage(this: void, message: WebviewToExtensionMessage): void;
setTooltipData: React.Dispatch<React.SetStateAction<GraphTooltipState>>;
startTracking(this: void): void;
stopTracking(this: void): void;
initializeAnchorSnapshot(this: void): void;
clearAnchorSnapshot(this: void): void;
tooltipTimeoutRef: MutableRefObject<ReturnType<typeof setTimeout> | null>;
}

function clearTooltipHoverState(
hoveredNodeRef: MutableRefObject<FGNode | null>,
interactionHandlers: GraphTooltipInteractionDependencies,
setTooltipData: React.Dispatch<React.SetStateAction<GraphTooltipState>>,
stopTracking: () => void,
clearAnchorSnapshot: () => void,
): void {
interactionHandlers.setGraphCursor('default');
hoveredNodeRef.current = null;
stopTracking();
clearAnchorSnapshot();
setTooltipData(hideGraphTooltipState);
}

Expand All @@ -52,8 +52,8 @@ export function handleTooltipNodeHover(
pluginHost,
postMessage,
setTooltipData,
startTracking,
stopTracking,
initializeAnchorSnapshot,
clearAnchorSnapshot,
tooltipTimeoutRef,
}: TooltipHoverOptions,
): void {
Expand All @@ -64,7 +64,7 @@ export function handleTooltipNodeHover(
hoveredNodeRef,
interactionHandlers,
setTooltipData,
stopTracking,
clearAnchorSnapshot,
);
return;
}
Expand All @@ -80,7 +80,7 @@ export function handleTooltipNodeHover(
pluginHost,
postMessage,
setTooltipData,
startTracking,
initializeAnchorSnapshot,
tooltipTimeoutRef,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function shouldRequestFileInfo(node: FGNode): boolean {

export function scheduleTooltipHover(node: FGNode, options: Pick<TooltipHoverOptions,
'dataRef' | 'fileInfoCacheRef' | 'getNodeRect' | 'pluginHost' | 'postMessage'
| 'legends' | 'setTooltipData' | 'startTracking' | 'tooltipTimeoutRef'>): void {
| 'legends' | 'setTooltipData' | 'initializeAnchorSnapshot' | 'tooltipTimeoutRef'>): void {
options.tooltipTimeoutRef.current = setTimeout(() => {
const pluginTooltip = options.pluginHost?.getTooltipContent(buildGraphTooltipContext({ node, snapshot: options.dataRef.current }));
const tooltipState = buildGraphTooltipState({
Expand All @@ -23,6 +23,6 @@ export function scheduleTooltipHover(node: FGNode, options: Pick<TooltipHoverOpt
if (tooltipState.shouldRequestFileInfo && shouldRequestFileInfo(node)) {
options.postMessage({ type: 'GET_FILE_INFO', payload: { path: node.id } });
}
options.startTracking();
options.initializeAnchorSnapshot();
}, 500);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,51 @@ import type { FGNode } from '../../model/build';
interface GraphTooltipTrackingOptions {
getNodeRect(this: void, node: FGNode): GraphTooltipRect | null;
hoveredNodeRef: MutableRefObject<FGNode | null>;
setTooltipData: React.Dispatch<React.SetStateAction<GraphTooltipState>>;
tooltipRafRef: MutableRefObject<number | null>;
tooltipRectRef: MutableRefObject<GraphTooltipRect | null>;
}

export function stopTooltipTracking(
tooltipRafRef: MutableRefObject<number | null>,
export function clearTooltipAnchorSnapshot(
tooltipRectRef: MutableRefObject<GraphTooltipRect | null>,
): void {
if (tooltipRafRef.current !== null) {
cancelAnimationFrame(tooltipRafRef.current);
tooltipRafRef.current = null;
}
tooltipRectRef.current = null;
}

export function startTooltipTracking({
export function initializeTooltipAnchorSnapshot({
getNodeRect,
hoveredNodeRef,
setTooltipData,
tooltipRafRef,
tooltipRectRef,
}: GraphTooltipTrackingOptions): void {
const tick = (): void => {
const node = hoveredNodeRef.current;
if (!node) return;
const node = hoveredNodeRef.current;
tooltipRectRef.current = node ? getNodeRect(node) : null;
}

const rect = getNodeRect(node);
if (rect) {
setTooltipData(previous => previous.visible ? { ...previous, nodeRect: rect } : previous);
}
function rectsEqual(
first: GraphTooltipRect,
second: GraphTooltipRect,
): boolean {
return first.x === second.x
&& first.y === second.y
&& first.radius === second.radius;
}

tooltipRafRef.current = requestAnimationFrame(tick);
};
export function updateTooltipAnchorSnapshot({
getNodeRect,
hoveredNodeRef,
setTooltipData,
tooltipRectRef,
}: GraphTooltipTrackingOptions & {
setTooltipData: React.Dispatch<React.SetStateAction<GraphTooltipState>>;
}): void {
const node = hoveredNodeRef.current;
const rect = node ? getNodeRect(node) : null;
if (!rect) {
clearTooltipAnchorSnapshot(tooltipRectRef);
return;
}
if (tooltipRectRef.current && rectsEqual(tooltipRectRef.current, rect)) return;

tooltipRafRef.current = requestAnimationFrame(tick);
tooltipRectRef.current = rect;
setTooltipData(previous => previous.visible
? { ...previous, nodeRect: rect }
: previous);
}
Loading