diff --git a/src/renderer/src/utils/image.ts b/src/renderer/src/utils/image.ts index e98b48918d..53669bd29f 100644 --- a/src/renderer/src/utils/image.ts +++ b/src/renderer/src/utils/image.ts @@ -72,8 +72,26 @@ export const captureScrollable = async (elRef: React.RefObject> - const modifiedElements: Array<{ node: HTMLElement; style: SavedStyle }> = [] + type SavedStyle = Partial< + Pick + > + const savedStyles = new Map() + const modifiedNodes: HTMLElement[] = [] + const save = (node: HTMLElement, fields: Array) => { + let saved = savedStyles.get(node) + if (!saved) { + saved = {} + savedStyles.set(node, saved) + modifiedNodes.push(node) + } + for (const f of fields) { + if (saved[f] === undefined) { + // Record inline style only once + // @ts-expect-error index signature + saved[f] = node.style[f] + } + } + } // Hide scrollbars during capture el.classList.add('hide-scrollbar') @@ -89,34 +107,39 @@ export const captureScrollable = async (elRef: React.RefObject('*')) for (const node of all) { const cs = getComputedStyle(node) - const save = (fields: Array) => { - const saved: SavedStyle = {} - for (const f of fields) saved[f] = (node.style as any)[f] - modifiedElements.push({ node, style: saved }) - } + if (cs.display === 'none') continue - // Expand vertical scroll regions - if (cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'hidden') { - save(['overflow', 'overflowY', 'height', 'maxHeight']) + // Expand vertical scroll regions only if content is clipped + const needsV = + (cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'hidden') && + node.scrollHeight > node.clientHeight + if (needsV) { + save(node, ['overflow', 'overflowY', 'height', 'maxHeight']) node.style.overflowY = 'visible' node.style.overflow = 'visible' node.style.maxHeight = 'none' node.style.height = 'auto' } - // Expand horizontal scroll regions - if (cs.overflowX === 'auto' || cs.overflowX === 'scroll' || cs.overflowX === 'hidden') { - save(['overflow', 'overflowX', 'width']) + // Expand horizontal scroll regions only if content is clipped + const needsH = + (cs.overflowX === 'auto' || cs.overflowX === 'scroll' || cs.overflowX === 'hidden') && + node.scrollWidth > node.clientWidth + if (needsH) { + save(node, ['overflow', 'overflowX', 'minWidth']) node.style.overflowX = 'visible' node.style.overflow = 'visible' - // Ensure full width for horizontally-scrolling containers + // Ensure full width for horizontally-scrolling containers using minWidth to avoid layout breakage const sw = Math.max(node.scrollWidth, node.clientWidth) - if (sw > 0) node.style.width = `${sw}px` + if (sw > 0) node.style.minWidth = `${sw}px` } // Special case: message content containers in horizontal layout - if (node.classList.contains('message-content-container')) { - save(['maxHeight', 'overflow', 'overflowY', 'height']) + if ( + node.classList.contains('message-content-container') && + node.scrollHeight > node.clientHeight + ) { + save(node, ['maxHeight', 'overflow', 'overflowY', 'height']) node.style.maxHeight = 'none' node.style.overflow = 'visible' node.style.overflowY = 'visible' @@ -127,7 +150,10 @@ export const captureScrollable = async (elRef: React.RefObject requestAnimationFrame(() => r(null))) + + // calculate the size of the element after expansion const totalWidth = el.scrollWidth const totalHeight = el.scrollHeight @@ -141,13 +167,17 @@ export const captureScrollable = async (elRef: React.RefObject= 0; i--) { + const node = modifiedNodes[i] + const style = savedStyles.get(node) + if (!style) continue if (style.overflow !== undefined) node.style.overflow = style.overflow if (style.overflowX !== undefined) node.style.overflowX = style.overflowX if (style.overflowY !== undefined) node.style.overflowY = style.overflowY if (style.height !== undefined) node.style.height = style.height if (style.maxHeight !== undefined) node.style.maxHeight = style.maxHeight if (style.width !== undefined) node.style.width = style.width + if (style.minWidth !== undefined) node.style.minWidth = style.minWidth } // restore the original scroll position @@ -158,19 +188,26 @@ export const captureScrollable = async (elRef: React.RefObject((resolve, reject) => { htmlToImage .toCanvas(el, { backgroundColor: getComputedStyle(el).getPropertyValue('--color-background'), cacheBust: true, - pixelRatio: window.devicePixelRatio, + pixelRatio: maxAllowedPR, skipAutoScale: true, - width: el.scrollWidth, - height: el.scrollHeight, + width: totalWidth, + height: totalHeight, style: { backgroundColor: getComputedStyle(el).backgroundColor, - color: getComputedStyle(el).color + color: getComputedStyle(el).color, + width: `${totalWidth}px`, + height: `${totalHeight}px`, + overflow: 'visible', + display: 'block' } }) .then((canvas) => resolve(canvas)) @@ -184,13 +221,17 @@ export const captureScrollable = async (elRef: React.RefObject= 0; i--) { + const node = modifiedNodes[i] + const style = savedStyles.get(node) + if (!style) continue if (style.overflow !== undefined) node.style.overflow = style.overflow if (style.overflowX !== undefined) node.style.overflowX = style.overflowX if (style.overflowY !== undefined) node.style.overflowY = style.overflowY if (style.height !== undefined) node.style.height = style.height if (style.maxHeight !== undefined) node.style.maxHeight = style.maxHeight if (style.width !== undefined) node.style.width = style.width + if (style.minWidth !== undefined) node.style.minWidth = style.minWidth } const imageData = canvas