diff --git a/src/index.ts b/src/index.ts index 1f68bfb7..8867f291 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,7 @@ export const scan = ( `font-weight:bold;font-size:14px;font-weight:bold;font-family:${MONO_FONT}`, ); - const ctx = createFullscreenCanvas(); + const canvasInstance = createFullscreenCanvas(); const status = createStatus(); const handleCommitFiberRoot = (_rendererID: number, root: FiberRoot) => { @@ -114,7 +114,7 @@ export const scan = ( ...outlines, ]); - if (nextPendingOutlines.length && ctx) { + if (nextPendingOutlines.length && canvasInstance.ctx) { for (let i = 0, len = nextPendingOutlines.length; i < len; i++) { const outline = nextPendingOutlines[i]; totalSelfTime += outline.selfTime; @@ -123,7 +123,7 @@ export const scan = ( let text = `×${totalCount}`; if (totalSelfTime > 0) text += ` (${totalSelfTime.toFixed(2)}ms)`; status.textContent = `${text} · react-scan`; - flushOutlines(ctx); + flushOutlines(canvasInstance.ctx); } }; diff --git a/src/overlay.ts b/src/overlay.ts index e7496379..db8e08e3 100644 --- a/src/overlay.ts +++ b/src/overlay.ts @@ -13,7 +13,7 @@ import type { ChangedProp, OutlinePaintTask, } from './types'; -import { onIdle, fastSerialize } from './utils'; +import { debounce, onIdle, fastSerialize } from './utils'; import { getCurrentOptions } from './auto'; import { MONO_FONT, PURPLE_RGB } from './constants'; @@ -382,23 +382,47 @@ export const createFullscreenCanvas = () => { let resizeScheduled = false; const resize = () => { + activeOutlines = []; + pendingOutlines = []; + const dpi = window.devicePixelRatio; - canvas.width = dpi * window.innerWidth; - canvas.height = dpi * window.innerHeight; + const visualViewport = window.visualViewport; + + // Use visualViewport for mobile and fallback to regular viewport for desktop + const width = visualViewport?.width ?? window.innerWidth; + const height = visualViewport?.height ?? window.innerHeight; + + canvas.width = dpi * width; + canvas.height = dpi * height; + + ctx?.setTransform(1, 0, 0, 1, 0, 0); ctx?.scale(dpi, dpi); + + // Only apply offset on mobile devices using visualViewport + if (visualViewport && /Mobi|Android/i.test(navigator.userAgent)) { + ctx?.translate(-visualViewport.offsetLeft, -visualViewport.offsetTop); + } + resizeScheduled = false; }; resize(); - window.addEventListener('resize', () => { + const events = ['resize', 'scroll', 'touchend', 'orientationchange']; + + const handleViewportChange = debounce(() => { if (!resizeScheduled) { resizeScheduled = true; requestAnimationFrame(() => { resize(); }); } - }); + }, 100); + + for (let i = 0, len = events.length; i < len; i++) { + const event = events[i]; + window.addEventListener(event, handleViewportChange, { passive: true }); + } onIdle(() => { const prevCanvas = document.getElementById('react-scan-canvas'); @@ -408,7 +432,14 @@ export const createFullscreenCanvas = () => { document.documentElement.appendChild(canvas); }); - return ctx; + const cleanup = () => { + for (let i = 0, len = events.length; i < len; i++) { + const event = events[i]; + window.removeEventListener(event, handleViewportChange); + } + }; + + return { ctx, cleanup }; }; export const createStatus = () => { diff --git a/src/utils.ts b/src/utils.ts index 2f37cb28..d1d17494 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,3 +80,15 @@ export const isProd = () => { export const NO_OP = () => { /**/ }; + +export const debounce = any>( + fn: T, + delay: number +): ((...args: Parameters) => void) => { + let timeoutId: ReturnType; + + return (...args: Parameters) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +};