diff --git a/src/renderer/src/utils/image.ts b/src/renderer/src/utils/image.ts index 6f8d8c3d18..b7a0602953 100644 --- a/src/renderer/src/utils/image.ts +++ b/src/renderer/src/utils/image.ts @@ -71,6 +71,29 @@ export const captureScrollable = async (elRef: React.RefObject + > + 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) { + saved[f] = node.style[f] + } + } + } + // Hide scrollbars during capture el.classList.add('hide-scrollbar') @@ -80,7 +103,55 @@ export const captureScrollable = async (elRef: React.RefObject { + const all = Array.from(el.querySelectorAll('*')) + for (const node of all) { + const cs = getComputedStyle(node) + if (cs.display === 'none') continue + + // 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 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 using minWidth to avoid layout breakage + const sw = Math.max(node.scrollWidth, node.clientWidth) + if (sw > 0) node.style.minWidth = `${sw}px` + } + + // Special case: message content containers in horizontal layout + 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' + node.style.height = 'auto' + } + } + } + + expandNestedScrollables() + + // Wait one frame for layout to settle after style changes + await new Promise((r) => requestAnimationFrame(() => r(null))) + + // calculate the size of the element after expansion const totalWidth = el.scrollWidth const totalHeight = el.scrollHeight @@ -93,6 +164,20 @@ 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 setTimeout(() => { el.scrollTop = originalScrollTop @@ -101,19 +186,29 @@ 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, - canvasWidth: el.scrollWidth, - canvasHeight: 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)) @@ -126,6 +221,20 @@ 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 // Restore original scroll position