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
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/scratch-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"get-user-media-promise": "1.1.4",
"graphql": "^14.7.0",
"highlight.js": "^10.7.3",
"html-to-image": "^1.11.13",
"immutable": "3.8.3",
"intl": "1.2.5",
"js-base64": "2.6.4",
Expand All @@ -176,7 +177,7 @@
"react-intl": "6.8.9",
"react-modal": "3.16.3",
"react-popover": "0.5.10",
"react-redux": "8.1.3",
"react-redux": "^8.0.0",
"react-responsive": "9.0.2",
"react-style-proptype": "3.2.2",
"react-tabs": "5.2.0",
Expand Down
26 changes: 26 additions & 0 deletions packages/scratch-gui/src/containers/ruby-tab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import AutoCorrectModal from '../components/auto-correct-modal/auto-correct-moda
import RubyScriptPreview from '../components/ruby-script-preview/ruby-script-preview.jsx';
import {generatePreviewCode} from '../lib/ruby-script-preview';
import {autoCorrect, defaultSettings as defaultAutoCorrectSettings} from '../lib/auto-correct';
import {downloadRubyAsImage} from '../lib/ruby-screenshot';
import cameraIcon from '../components/blocks-screenshot-button/icon--camera.svg';
import styles from './ruby-tab/ruby-tab.css';
import {loadMonacoLocale} from '../lib/monaco-i18n-helper';
import {getPrism, loadPrism} from '../lib/prism-parser';
Expand Down Expand Up @@ -422,6 +424,14 @@ const RubyTab = props => {
onFontSizeChange(DEFAULT_FONT_SIZE);
}, [onFontSizeChange]);

const handleScreenshot = useCallback(() => {
if (!editorRef.current) return;
const target = vm.editingTarget;
const spriteName = target ? target.sprite.name : 'sprite';
const title = props.projectTitle || 'project';
downloadRubyAsImage(editorRef.current, title, spriteName);
}, [vm, props.projectTitle]);

const handleSelectTarget = useCallback(targetId => {
const target = vm.runtime.getTargetById(targetId);
if (target) vm.setEditingTarget(target.id);
Expand Down Expand Up @@ -967,6 +977,20 @@ const RubyTab = props => {
<div className={styles.zoomControlsWrapper}>
<button
className={styles.zoomButton}
data-testid="ruby-screenshot"
title="Rubyコードを画像として保存"
onClick={handleScreenshot}
>
<img
alt="Rubyコードを画像として保存"
className={styles.zoomIcon}
draggable={false}
src={cameraIcon}
/>
</button>
<button
className={styles.zoomButton}
data-testid="ruby-zoom-in"
onClick={handleZoomIn}
>
<img
Expand All @@ -976,6 +1000,7 @@ const RubyTab = props => {
</button>
<button
className={styles.zoomButton}
data-testid="ruby-zoom-out"
onClick={handleZoomOut}
>
<img
Expand All @@ -985,6 +1010,7 @@ const RubyTab = props => {
</button>
<button
className={styles.zoomButton}
data-testid="ruby-zoom-reset"
onClick={handleZoomReset}
>
<img
Expand Down
88 changes: 88 additions & 0 deletions packages/scratch-gui/src/lib/ruby-screenshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// === Smalruby: This file is Smalruby-specific (Ruby tab screenshot export) ===

import {toBlob} from 'html-to-image';
import downloadBlob from './download-blob';

/**
* Builds the export filename for Ruby tab screenshots.
* @param {string} projectTitle - Project name
* @param {string} spriteName - Sprite / stage name
* @returns {string} PNG filename
*/
const buildFilename = function (projectTitle, spriteName) {
return `${projectTitle}_${spriteName}_ruby.png`;
};

/**
* Captures the Monaco editor content as a PNG image and triggers a download.
* Temporarily expands the editor to its full content height so that all lines
* (including those scrolled out of view) are rendered in the DOM before capture.
* ViewZones (e.g. furigana annotations) are captured naturally as DOM elements.
* @param {object} editor - Monaco editor instance
* @param {string} projectTitle - Project name (used in filename)
* @param {string} spriteName - Sprite / stage name (used in filename)
* @returns {Promise<void>}
*/
const downloadRubyAsImage = async function (editor, projectTitle, spriteName) {
if (!editor) return;

const editorDomNode = editor.getDomNode();
if (!editorDomNode) return;

const model = editor.getModel();
if (!model || model.getLineCount() === 0) return;

// Save original state
const scrollTop = editor.getScrollTop();
const scrollLeft = editor.getScrollLeft();
const containerEl = editorDomNode.parentElement;
const originalHeight = containerEl.style.height;
const originalOverflow = containerEl.style.overflow;

try {
// Disable scrollBeyondLastLine to get tight content height
editor.updateOptions({scrollBeyondLastLine: false});
editor.layout();

// Expand editor to full content height so all lines are in the DOM
const contentHeight = editor.getContentHeight();
containerEl.style.height = `${contentHeight}px`;
containerEl.style.overflow = 'hidden';

// Force Monaco to re-layout at the new size
editor.layout();

// Wait for Monaco to render all lines
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});

// Capture the editor DOM as a PNG blob.
// skipFonts avoids SecurityError when html-to-image tries to read
// cssRules from cross-origin stylesheets (Monaco's CDN CSS).
const blob = await toBlob(editorDomNode, {
backgroundColor: '#ffffff',
pixelRatio: 2,
skipFonts: true
});

if (blob) {
downloadBlob(buildFilename(projectTitle, spriteName), blob);
}
} finally {
// Restore original state
editor.updateOptions({scrollBeyondLastLine: true});
containerEl.style.height = originalHeight;
containerEl.style.overflow = originalOverflow;
editor.layout();
editor.setScrollTop(scrollTop);
editor.setScrollLeft(scrollLeft);
}
};

export {
buildFilename,
downloadRubyAsImage
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ const getFuriganaInfo = async () => {
* Click the zoom-in button in the Ruby tab.
*/
const clickZoomIn = async () => {
await clickXpath('//div[contains(@class, "ruby-tab_zoomControlsWrapper")]//button[1]');
await clickXpath('//button[@data-testid="ruby-zoom-in"]');
};

/**
* Click the zoom-reset button in the Ruby tab.
*/
const clickZoomReset = async () => {
await clickXpath('//div[contains(@class, "ruby-tab_zoomControlsWrapper")]//button[3]');
await clickXpath('//button[@data-testid="ruby-zoom-reset"]');
};

describe('Ruby tab furigana zoom follow', () => {
Expand Down
Loading
Loading