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 @@ + + + + turbo-bolt-icon + Created with Sketch. + + + + + + + \ 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(); + }); +});