diff --git a/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.css b/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.css index 37e6acec36d..ddc23fe8bfb 100644 --- a/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.css +++ b/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.css @@ -1,12 +1,15 @@ @import "../../css/z-index.css"; -/* Position above the Scratch Blocks zoom controls. - bottom = scrollbar(11) + MARGIN_BOTTOM(12) + HEIGHT(124) + gap(8) - 1 = 154px - right = MARGIN_SIDE(12) + scrollbar(11) - 1 = 22px */ +/* Position is set at runtime by `blocks-screenshot-button.jsx` so the camera + button stays anchored to the actual scratch-blocks v2 zoom controls + regardless of scrollbar width or window resize. The CSS values below are + only used until the first measurement settles in (typically the next + animation frame), so they are sized to match the v2 zoom-controls layout + (MARGIN_VERTICAL=20, MARGIN_HORIZONTAL=20, total height 124, gap 8). */ .screenshotButton { position: absolute; - bottom: 154px; - right: 22px; + bottom: 152px; + right: 20px; width: 36px; height: 36px; padding: 0; diff --git a/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.jsx b/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.jsx index f6d031e1895..e945686d9e8 100644 --- a/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.jsx +++ b/packages/scratch-gui/src/components/blocks-screenshot-button/blocks-screenshot-button.jsx @@ -1,25 +1,100 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useLayoutEffect, useRef } from 'react'; import cameraIcon from './icon--camera.svg'; import styles from './blocks-screenshot-button.css'; -const BlocksScreenshotButton = ({onClick}) => ( - -); +/** Visual gap between the camera button and the topmost zoom button. */ +const ZOOM_GAP_PX = 8; + +/** + * Camera button that exports the workspace blocks as a PNG image. Positioned + * directly above the scratch-blocks zoom controls. The position is measured + * at runtime (rather than fixed via CSS) so it survives: + * - scrollbar width differences across platforms (macOS overlay vs Windows + * classic scrollbars), + * - mobile and window-resize layout changes that move the zoom controls, + * - scratch-blocks v2's reflow that re-runs the zoom controls auto-layout. + * @param {object} props - Component props. + * @param {Function} props.onClick - Click handler for the export action. + * @returns {React.ReactElement} The screenshot button element. + */ +const BlocksScreenshotButton = ({ onClick }) => { + const buttonRef = useRef(null); + + useLayoutEffect(() => { + const button = buttonRef.current; + if (!button) return; + + let raf = 0; + const updatePosition = () => { + const wrapper = button.offsetParent; + if (!wrapper) return; + // ScratchZoomControls (scratch-blocks v2) renders a `` with + // class `blocklyZoom blocklyZoomIn` that ends up at the top of + // the zoom column when the workspace is in the bottom corner. + const zoomIn = wrapper.querySelector('.blocklyZoomIn'); + if (!zoomIn) return; + const zoomRect = zoomIn.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); + const right = Math.max(0, wrapperRect.right - zoomRect.right); + const bottom = Math.max( + 0, + wrapperRect.bottom - (zoomRect.top - ZOOM_GAP_PX), + ); + button.style.right = `${right}px`; + button.style.bottom = `${bottom}px`; + }; + + const schedule = () => { + cancelAnimationFrame(raf); + raf = requestAnimationFrame(updatePosition); + }; + + // Initial measurement; retry a few frames in case the zoom controls + // haven't been mounted yet at this point in the workspace lifecycle. + let retries = 10; + const tryUpdate = () => { + updatePosition(); + const wrapper = button.offsetParent; + const zoomIn = wrapper && wrapper.querySelector('.blocklyZoomIn'); + if (!zoomIn && retries > 0) { + retries -= 1; + requestAnimationFrame(tryUpdate); + } + }; + tryUpdate(); + + const wrapper = button.offsetParent; + const observer = new ResizeObserver(schedule); + if (wrapper) observer.observe(wrapper); + window.addEventListener('resize', schedule); + + return () => { + cancelAnimationFrame(raf); + observer.disconnect(); + window.removeEventListener('resize', schedule); + }; + }, []); + + return ( + + ); +}; BlocksScreenshotButton.propTypes = { - onClick: PropTypes.func.isRequired + onClick: PropTypes.func.isRequired, }; export default BlocksScreenshotButton; diff --git a/packages/scratch-gui/src/containers/ruby-tab/ruby-tab.css b/packages/scratch-gui/src/containers/ruby-tab/ruby-tab.css index 1723c604dc2..b78cbc6ad50 100644 --- a/packages/scratch-gui/src/containers/ruby-tab/ruby-tab.css +++ b/packages/scratch-gui/src/containers/ruby-tab/ruby-tab.css @@ -2,39 +2,15 @@ @import "../../css/units.css"; @import "../../css/z-index.css"; -.downloadButton { - z-index: $z-index-add-button; - width: 2.75rem; - height: 2.75rem; - border: none; - border-radius: 100%; - background-color: $looks-secondary; - box-shadow: 0 0 0 4px $looks-transparent; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - padding: 0; -} - -.downloadIcon { - width: 28px; - height: 28px; -} - -.downloadWrapper { - position: absolute; - bottom: 22px; - right: 70px; /* 22px (zoom margin) + 36px (zoom width) + 12px (gap) */ - display: flex; - flex-direction: column; - align-items: center; -} - .zoomControlsWrapper { position: absolute; - bottom: 22px; - right: 22px; /* Match Blockly zoom controls position */ + /* Match the on-screen position of the Code tab's scratch-blocks v2 zoom + controls. ScratchZoomControls uses MARGIN_VERTICAL = MARGIN_HORIZONTAL = 20 + inside the workspace's view metrics, which exclude the scratch-blocks + scrollbar (11px on desktop). Add that scrollbar offset here so the icons + sit at the same screen X/Y when switching tabs. */ + bottom: 31px; + right: 31px; display: flex; flex-direction: column; align-items: center;