diff --git a/src/components/controls/controls.css b/src/components/controls/controls.css
new file mode 100644
index 000000000..7e60ae334
--- /dev/null
+++ b/src/components/controls/controls.css
@@ -0,0 +1,3 @@
+.controls-container {
+ display: flex;
+}
diff --git a/src/components/controls/controls.jsx b/src/components/controls/controls.jsx
new file mode 100644
index 000000000..8fec705b0
--- /dev/null
+++ b/src/components/controls/controls.jsx
@@ -0,0 +1,53 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import GreenFlag from '../green-flag/green-flag.jsx';
+import StopAll from '../stop-all/stop-all.jsx';
+import TurboMode from '../turbo-mode/turbo-mode.jsx';
+
+import styles from './controls.css';
+
+const Controls = function (props) {
+ const {
+ active,
+ className,
+ onGreenFlagClick,
+ onStopAllClick,
+ turbo,
+ ...componentProps
+ } = props;
+ return (
+
+
+
+ {turbo ? (
+
+ ) : null}
+
+ );
+};
+
+Controls.propTypes = {
+ active: PropTypes.bool,
+ className: PropTypes.string,
+ onGreenFlagClick: PropTypes.func.isRequired,
+ onStopAllClick: PropTypes.func.isRequired,
+ turbo: PropTypes.bool
+};
+
+Controls.defaultProps = {
+ active: false,
+ turbo: false
+};
+
+export default Controls;
diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css
index 3fda3a207..2c1452838 100644
--- a/src/components/green-flag/green-flag.css
+++ b/src/components/green-flag/green-flag.css
@@ -1,13 +1,21 @@
.green-flag {
- width: 1.1rem;
- height: 1.1rem;
- opacity: 0.5;
+ box-sizing: content-box;
+ width: 1.25rem;
+ height: 1.25rem;
+ padding: 0.375rem;
+ border-radius: 0.25rem;
user-select: none;
cursor: pointer;
- transition: opacity 0.2s ease-out; /* @todo: standardize with var */
+ transition: 0.2s ease-out;
}
-.green-flag.is-active,
.green-flag:hover {
- opacity: 1;
+ /* Scale flag image by 1.2, but keep background static */
+ width: 1.5rem;
+ height: 1.5rem;
+ padding: 0.25rem;
+}
+
+.green-flag.is-active {
+ background-color: rgba(0, 0, 0, 0.1);
}
diff --git a/src/components/green-flag/icon-green-flag.svg b/src/components/green-flag/icon-green-flag.svg
deleted file mode 100644
index 4bd2528f9..000000000
--- a/src/components/green-flag/icon-green-flag.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index 6a063a166..c8a9285a2 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -101,10 +101,6 @@
position: relative;
}
-.green-flag {
- margin: 0.25rem 0.6rem;
-}
-
.stage-and-target-wrapper {
/*
Makes rows for children:
@@ -130,7 +126,7 @@
display: flex;
flex-grow: 1;
flex-basis: 0;
-
+
padding-top: $space;
padding-left: $space;
padding-right: $space;
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 8c8c9f96d..1eb0e6505 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -8,11 +8,10 @@ import VM from 'scratch-vm';
import Blocks from '../../containers/blocks.jsx';
import CostumeTab from '../../containers/costume-tab.jsx';
-import GreenFlag from '../../containers/green-flag.jsx';
+import Controls from '../../containers/controls.jsx';
import TargetPane from '../../containers/target-pane.jsx';
import SoundTab from '../../containers/sound-tab.jsx';
import Stage from '../../containers/stage.jsx';
-import StopAll from '../../containers/stop-all.jsx';
import Box from '../box/box.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx';
@@ -90,11 +89,7 @@ const GUIComponent = props => {
-
-
+
{isFullSize => (
diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css
index 95a633771..99ebee892 100644
--- a/src/components/stop-all/stop-all.css
+++ b/src/components/stop-all/stop-all.css
@@ -1,13 +1,22 @@
.stop-all {
- width: 1.1rem;
- height: 1.1rem;
- opacity: 0.5;
+ box-sizing: content-box;
+ width: 1.25rem;
+ height: 1.25rem;
+ padding: 0.375rem;
+ border-radius: 0.25rem;
user-select: none;
cursor: pointer;
- transition: opacity 0.2s ease-out; /* @todo: standardize with var */
+ transition: 0.2s ease-out;
+}
+
+.stop-all {
+ opacity: 0.5;
}
-.stop-all.is-active,
.stop-all:hover {
+ transform: scale(1.2);
+}
+
+.stop-all.is-active {
opacity: 1;
}
diff --git a/src/components/turbo-mode/icon--turbo.svg b/src/components/turbo-mode/icon--turbo.svg
new file mode 100644
index 000000000..64fd1665f
--- /dev/null
+++ b/src/components/turbo-mode/icon--turbo.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/src/components/turbo-mode/turbo-mode.css b/src/components/turbo-mode/turbo-mode.css
new file mode 100644
index 000000000..d12699cee
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.css
@@ -0,0 +1,20 @@
+@import "../../css/colors.css";
+
+.turbo-container {
+ display: flex;
+ align-items: center;
+ padding: 0.25rem;
+ user-select: none;
+}
+
+.turbo-icon {
+ margin: 0.25rem;
+}
+
+.turbo-label {
+ font-size: 0.625rem;
+ font-weight: bold;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: $control-primary;
+ white-space: nowrap;
+}
diff --git a/src/components/turbo-mode/turbo-mode.jsx b/src/components/turbo-mode/turbo-mode.jsx
new file mode 100644
index 000000000..724af24d1
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import turboIcon from './icon--turbo.svg';
+
+import styles from './turbo-mode.css';
+
+const TurboMode = () => (
+
+

+
+
+
+
+);
+
+export default TurboMode;
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
new file mode 100644
index 000000000..18dd44e5e
--- /dev/null
+++ b/src/containers/controls.jsx
@@ -0,0 +1,70 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import VM from 'scratch-vm';
+
+import ControlsComponent from '../components/controls/controls.jsx';
+
+class Controls extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleGreenFlagClick',
+ 'handleStopAllClick',
+ 'onProjectRunStart',
+ 'onProjectRunStop'
+ ]);
+ this.state = {
+ projectRunning: false,
+ turbo: false
+ };
+ }
+ componentDidMount () {
+ this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
+ this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
+ }
+ componentWillUnmount () {
+ this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
+ this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
+ }
+ onProjectRunStart () {
+ this.setState({projectRunning: true});
+ }
+ onProjectRunStop () {
+ this.setState({projectRunning: false});
+ }
+ handleGreenFlagClick (e) {
+ e.preventDefault();
+ if (e.shiftKey) {
+ this.setState({turbo: !this.state.turbo});
+ this.props.vm.setTurboMode(!this.state.turbo);
+ } else {
+ this.props.vm.greenFlag();
+ }
+ }
+ handleStopAllClick (e) {
+ e.preventDefault();
+ this.props.vm.stopAll();
+ }
+ render () {
+ const {
+ vm, // eslint-disable-line no-unused-vars
+ ...props
+ } = this.props;
+ return (
+
+ );
+ }
+}
+
+Controls.propTypes = {
+ vm: PropTypes.instanceOf(VM)
+};
+
+export default Controls;
diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx
index b1f6acabf..a725b7e1b 100644
--- a/src/containers/green-flag.jsx
+++ b/src/containers/green-flag.jsx
@@ -11,24 +11,18 @@ class GreenFlag extends React.Component {
super(props);
bindAll(this, [
'handleClick',
- 'handleKeyDown',
- 'handleKeyUp',
'onProjectRunStart',
'onProjectRunStop'
]);
- this.state = {projectRunning: false, shiftKeyDown: false};
+ this.state = {projectRunning: false};
}
componentDidMount () {
this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- document.addEventListener('keydown', this.handleKeyDown);
- document.addEventListener('keyup', this.handleKeyUp);
}
componentWillUnmount () {
this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- document.removeEventListener('keydown', this.handleKeyDown);
- document.removeEventListener('keyup', this.handleKeyUp);
}
onProjectRunStart () {
this.setState({projectRunning: true});
@@ -36,15 +30,9 @@ class GreenFlag extends React.Component {
onProjectRunStop () {
this.setState({projectRunning: false});
}
- handleKeyDown (e) {
- this.setState({shiftKeyDown: e.shiftKey});
- }
- handleKeyUp (e) {
- this.setState({shiftKeyDown: e.shiftKey});
- }
handleClick (e) {
e.preventDefault();
- if (this.state.shiftKeyDown) {
+ if (e.shiftKey) {
this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode);
} else {
this.props.vm.greenFlag();
diff --git a/src/css/colors.css b/src/css/colors.css
index ea3f5936f..d524795cd 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -14,4 +14,6 @@ $red-tertiary: #E64D00;
$sound-primary: #CF63CF;
$sound-tertiary: #A63FA6;
+$control-primary: #FFAB19;
+
$form-border: #E9EEF2;
diff --git a/src/examples/blocks-only.css b/src/examples/blocks-only.css
index 971361388..0f3ac07f8 100644
--- a/src/examples/blocks-only.css
+++ b/src/examples/blocks-only.css
@@ -1,11 +1,4 @@
-.green-flag {
- position: absolute;
- z-index: 2;
- top: 10px;
- right: 50px;
-}
-
-.stop-all {
+.controls {
position: absolute;
z-index: 2;
top: 10px;
diff --git a/src/examples/blocks-only.jsx b/src/examples/blocks-only.jsx
index c97444d9e..8d5159e82 100644
--- a/src/examples/blocks-only.jsx
+++ b/src/examples/blocks-only.jsx
@@ -3,8 +3,7 @@ import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import AppStateHOC from '../lib/app-state-hoc.jsx';
-import GreenFlag from '../containers/green-flag.jsx';
-import StopAll from '../containers/stop-all.jsx';
+import Controls from '../containers/controls.jsx';
import Blocks from '../containers/blocks.jsx';
import GUI from '../containers/gui.jsx';
import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
@@ -14,8 +13,7 @@ import styles from './blocks-only.css';
const mapStateToProps = state => ({vm: state.vm});
const VMBlocks = connect(mapStateToProps)(Blocks);
-const VMGreenFlag = connect(mapStateToProps)(GreenFlag);
-const VMStopAll = connect(mapStateToProps)(StopAll);
+const VMControls = connect(mapStateToProps)(Controls);
const BlocksOnly = props => (
@@ -25,8 +23,7 @@ const BlocksOnly = props => (
media: `static/blocks-media/`
}}
/>
-
-
+
);
diff --git a/src/examples/player.jsx b/src/examples/player.jsx
index 56c7a7360..ab50527cd 100644
--- a/src/examples/player.jsx
+++ b/src/examples/player.jsx
@@ -3,8 +3,7 @@ import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import AppStateHOC from '../lib/app-state-hoc.jsx';
-import GreenFlag from '../containers/green-flag.jsx';
-import StopAll from '../containers/stop-all.jsx';
+import Controls from '../containers/controls.jsx';
import Stage from '../containers/stage.jsx';
import Box from '../components/box/box.jsx';
import GUI from '../containers/gui.jsx';
@@ -15,8 +14,7 @@ import './player.css';
const mapStateToProps = state => ({vm: state.vm});
const VMStage = connect(mapStateToProps)(Stage);
-const VMGreenFlag = connect(mapStateToProps)(GreenFlag);
-const VMStopAll = connect(mapStateToProps)(StopAll);
+const VMControls = connect(mapStateToProps)(Controls);
class Player extends React.Component {
constructor (props) {
@@ -55,17 +53,12 @@ class Player extends React.Component {
width={width}
>
-
-
{
+ const defaultProps = () => ({
+ active: false,
+ greenFlagTitle: 'Go',
+ onGreenFlagClick: jest.fn(),
+ onStopAllClick: jest.fn(),
+ stopAllTitle: 'Stop',
+ turbo: false
+ });
+
+ test('shows turbo mode when in turbo mode', () => {
+ const component = shallowWithIntl(
+
+ );
+ expect(component.find(TurboMode).exists()).toEqual(false);
+ component.setProps({turbo: true});
+ expect(component.find(TurboMode).exists()).toEqual(true);
+ });
+
+ test('triggers the right callbacks when clicked', () => {
+ const props = defaultProps();
+ const component = shallowWithIntl(
+
+ );
+ component.find('[title="Go"]').simulate('click');
+ expect(props.onGreenFlagClick).toHaveBeenCalled();
+
+ component.find('[title="Stop"]').simulate('click');
+ expect(props.onStopAllClick).toHaveBeenCalled();
+ });
+});