diff --git a/src/commons/sideContent/SideContent.tsx b/src/commons/sideContent/SideContent.tsx index 75cffad579..201f1711d5 100644 --- a/src/commons/sideContent/SideContent.tsx +++ b/src/commons/sideContent/SideContent.tsx @@ -12,7 +12,6 @@ import type { export type SideContentProps = { renderActiveTabPanelOnly?: boolean; - editorWidth?: string; tabs?: { beforeDynamicTabs: SideContentTab[]; afterDynamicTabs: SideContentTab[]; @@ -27,7 +26,6 @@ const renderTab = ( tab: SideContentTab, shouldAlert: boolean, workspaceLocation?: SideContentLocation, - editorWidth?: string, sideContentHeight?: number ) => { const iconSize = 20; @@ -56,7 +54,6 @@ const renderTab = ( props: { ...tab.body.props, workspaceLocation, - editorWidth, sideContentHeight } } @@ -66,7 +63,7 @@ const renderTab = ( return ; }; -const SideContent = ({ renderActiveTabPanelOnly, editorWidth, ...props }: SideContentProps) => ( +const SideContent = ({ renderActiveTabPanelOnly, ...props }: SideContentProps) => ( {({ tabs: allTabs, alerts: tabAlerts, changeTabsCallback, selectedTab, height }) => (
@@ -80,13 +77,7 @@ const SideContent = ({ renderActiveTabPanelOnly, editorWidth, ...props }: SideCo > {allTabs.map(tab => { const tabId = getTabId(tab); - return renderTab( - tab, - tabAlerts.includes(tabId), - props.workspaceLocation, - editorWidth, - height - ); + return renderTab(tab, tabAlerts.includes(tabId), props.workspaceLocation, height); })}
diff --git a/src/commons/sideContent/content/SideContentCseMachine.tsx b/src/commons/sideContent/content/SideContentCseMachine.tsx index 71aa0c31ca..9109bb2e16 100644 --- a/src/commons/sideContent/content/SideContentCseMachine.tsx +++ b/src/commons/sideContent/content/SideContentCseMachine.tsx @@ -14,7 +14,7 @@ import { bindActionCreators } from '@reduxjs/toolkit'; import classNames from 'classnames'; import { Chapter } from 'js-slang/dist/types'; import { debounce } from 'lodash'; -import React from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import HotKeys from 'src/commons/hotkeys/HotKeys'; import { Output } from 'src/commons/repl/Repl'; @@ -32,20 +32,7 @@ import { beginAlertSideContent } from '../SideContentActions'; import { getLocation } from '../SideContentHelper'; import { NonStoryWorkspaceLocation, SideContentTab, SideContentType } from '../SideContentTypes'; -type State = { - visualization: React.ReactNode; - value: number; - height: number; - width: number; - lastStep: boolean; - stepLimitExceeded: boolean; - chapter: Chapter; -}; - -type CseMachineProps = OwnProps & StateProps & DispatchProps; - type StateProps = { - editorWidth?: string; sideContentHeight?: number; stepsTotal: number; currentStep: number; @@ -70,470 +57,389 @@ type DispatchProps = { handleAlertSideContent: () => void; }; -class SideContentCseMachineBase extends React.Component { - constructor(props: CseMachineProps) { - super(props); - this.state = { - visualization: null, - value: -1, - width: this.calculateWidth(props.editorWidth), - height: this.calculateHeight(props.sideContentHeight), - lastStep: false, - stepLimitExceeded: false, - chapter: props.chapter - }; - if (this.isJava()) { - JavaCseMachine.init( - visualization => this.setState({ visualization }), - (segments: [number, number][]) => { - props.setEditorHighlightedLines(0, segments); +const calculateWidth = (editorWidth?: string) => { + const horizontalPadding = 50; + const maxWidth = 5000; // limit for visible diagram width for huge screens + const width = window.innerWidth - horizontalPadding; + return Math.min(width, maxWidth); +}; + +const calculateHeight = (sideContentHeight?: number) => { + const verticalPadding = 150; + const maxHeight = 5000; // limit for visible diagram height for huge screens + let height; + if (window.innerWidth < Constants.mobileBreakpoint) { + // mobile mode + height = window.innerHeight - verticalPadding; + } else if (sideContentHeight === undefined) { + height = window.innerHeight - verticalPadding; + } else { + height = sideContentHeight - verticalPadding; + } + return Math.min(height, maxHeight); +}; + +type Props = OwnProps & StateProps & DispatchProps; + +const SideContentCseMachineBase: React.FC = ({ + // DispatchProps + handleStepUpdate, + handleEditorEval, + setEditorHighlightedLines, + handleAlertSideContent, + ...props +}) => { + const [visualization, setVisualization] = useState(null); + const [value, setValue] = useState(-1); + const [width, setWidth] = useState(calculateWidth()); + const [height, setHeight] = useState(calculateHeight(props.sideContentHeight)); + const [stepLimitExceeded, setStepLimitExceeded] = useState(false); + + const isJava = useCallback(() => props.chapter === Chapter.FULL_JAVA, [props.chapter]); + + const handleResize = useMemo( + () => + debounce(() => { + const newWidth = calculateWidth(); + const newHeight = calculateHeight(props.sideContentHeight); + if (newWidth !== width || newHeight !== height) { + setWidth(newWidth); + setHeight(newHeight); + CseMachine.updateDimensions(newWidth, newHeight); } - ); + }, 300), + [props.sideContentHeight, width, height] + ); + + useEffect(() => { + handleResize(); + window.addEventListener('resize', handleResize); + CseMachine.redraw(); + + return () => { + handleResize.cancel(); + window.removeEventListener('resize', handleResize); + }; + }, [handleResize]); + + useEffect(() => { + if (isJava()) { + JavaCseMachine.init(setVisualization, (segments: [number, number][]) => { + setEditorHighlightedLines(0, segments); + }); } else { CseMachine.init( visualization => { - this.setState({ visualization }, () => CseAnimation.playAnimation()); - if (visualization) this.props.handleAlertSideContent(); + setVisualization(visualization); + CseAnimation.playAnimation(); + if (visualization) handleAlertSideContent(); }, - this.state.width, - this.state.height, + width, + height, (segments: [number, number][]) => { - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - // This comment is copied over from workspace saga - props.setEditorHighlightedLines(0, segments); + setEditorHighlightedLines(0, segments); }, - // We shouldn't be able to move slider to a step number beyond the step limit - isControlEmpty => { - this.setState({ stepLimitExceeded: false }); - } + () => setStepLimitExceeded(false) ); } - } + }, [isJava, handleAlertSideContent, width, height, setEditorHighlightedLines]); - private isJava(): boolean { - return this.props.chapter === Chapter.FULL_JAVA; - } + const sliderRelease = useCallback( + (newValue: number) => { + handleEditorEval(); + }, + [handleEditorEval] + ); - private calculateWidth(editorWidth?: string) { - const horizontalPadding = 50; - const maxWidth = 5000; // limit for visible diagram width for huge screens - let width; - if (editorWidth === undefined) { - width = window.innerWidth - horizontalPadding; - } else { - width = Math.min( - maxWidth, - (window.innerWidth * (100 - parseFloat(editorWidth))) / 100 - horizontalPadding - ); - } - return Math.min(width, maxWidth); - } + const sliderShift = useCallback( + (newValue: number) => { + handleStepUpdate(newValue); + setValue(newValue); + }, + [handleStepUpdate] + ); - private calculateHeight(sideContentHeight?: number) { - const verticalPadding = 150; - const maxHeight = 5000; // limit for visible diagram height for huge screens - let height; - if (window.innerWidth < Constants.mobileBreakpoint) { - // mobile mode - height = window.innerHeight - verticalPadding; - } else if (sideContentHeight === undefined) { - height = window.innerHeight - verticalPadding; - } else { - height = sideContentHeight - verticalPadding; + const stepPrevious = useCallback(() => { + if (value !== 0) { + sliderShift(value - 1); + sliderRelease(value - 1); } - return Math.min(height, maxHeight); - } + }, [value, sliderShift, sliderRelease]); - handleResize = debounce(() => { - const newWidth = this.calculateWidth(this.props.editorWidth); - const newHeight = this.calculateHeight(this.props.sideContentHeight); - if (newWidth !== this.state.width || newHeight !== this.state.height) { - this.setState({ - height: newHeight, - width: newWidth - }); - CseMachine.updateDimensions(newWidth, newHeight); + const stepNext = useCallback(() => { + const lastStepValue = props.stepsTotal; + if (value !== lastStepValue) { + sliderShift(value + 1); + sliderRelease(value + 1); + CseAnimation.enableAnimations(); } - }, 300); - - componentDidMount() { - this.handleResize(); - window.addEventListener('resize', this.handleResize); - CseMachine.redraw(); - } + }, [value, props.stepsTotal, sliderShift, sliderRelease]); - componentWillUnmount() { - this.handleResize.cancel(); - window.removeEventListener('resize', this.handleResize); - } + const stepFirst = useCallback(() => { + // Move to the first step + sliderShift(0); + sliderRelease(0); + }, [sliderShift, sliderRelease]); + + const stepLast = useCallback( + (lastStepValue: number) => { + // Move to the last step + sliderShift(lastStepValue); + sliderRelease(lastStepValue); + }, + [sliderShift, sliderRelease] + ); - componentDidUpdate(prevProps: { - editorWidth?: string; - sideContentHeight?: number; - stepsTotal: number; - needCseUpdate: boolean; - }) { - if ( - prevProps.sideContentHeight !== this.props.sideContentHeight || - prevProps.editorWidth !== this.props.editorWidth - ) { - this.handleResize(); + const stepNextBreakpoint = useCallback(() => { + for (const step of props.breakpointSteps) { + if (step > value) { + sliderShift(step); + sliderRelease(step); + return; + } + } + sliderShift(props.stepsTotal); + sliderRelease(props.stepsTotal); + }, [props.breakpointSteps, props.stepsTotal, value, sliderShift, sliderRelease]); + + const stepPrevBreakpoint = useCallback(() => { + for (let i = props.breakpointSteps.length - 1; i >= 0; i--) { + const step = props.breakpointSteps[i]; + if (step < value) { + sliderShift(step); + sliderRelease(step); + return; + } } - if (prevProps.needCseUpdate && !this.props.needCseUpdate) { - this.stepFirst(); - if (this.isJava()) { + sliderShift(0); + sliderRelease(0); + }, [props.breakpointSteps, value, sliderShift, sliderRelease]); + + const stepNextChangepoint = useCallback(() => { + for (const step of props.changepointSteps) { + if (step > value) { + sliderShift(step); + sliderRelease(step); + return; + } + } + sliderShift(props.stepsTotal); + sliderRelease(props.stepsTotal); + }, [props.changepointSteps, props.stepsTotal, value, sliderShift, sliderRelease]); + + const stepPrevChangepoint = useCallback(() => { + for (let i = props.changepointSteps.length - 1; i >= 0; i--) { + const step = props.changepointSteps[i]; + if (step < value) { + sliderShift(step); + sliderRelease(step); + return; + } + } + sliderShift(0); + sliderRelease(0); + }, [props.changepointSteps, value, sliderShift, sliderRelease]); + + useEffect(() => { + if (props.needCseUpdate) { + stepFirst(); + if (isJava()) { JavaCseMachine.clearCse(); } else { CseMachine.clearCse(); } } - } + }, [props.needCseUpdate, isJava, stepFirst]); - public render() { - const hotkeyBindings: HotkeyItem[] = this.state.visualization - ? [ - ['a', this.stepFirst], - ['f', this.stepNext], - ['b', this.stepPrevious], - ['e', this.stepLast(this.props.stepsTotal)] - ] - : [ - ['a', () => {}], - ['f', () => {}], - ['b', () => {}], - ['e', () => {}] - ]; - - return ( - -
- -
- {!this.isJava() && ( - - - { - if (this.state.visualization) { - CseMachine.toggleControlStash(); - CseMachine.redraw(); - } - }} - icon="layers" - disabled={!this.state.visualization} - > - - - - - { - if (this.state.visualization) { - CseMachine.toggleStackTruncated(); - CseMachine.redraw(); - } - }} - icon="minimize" - disabled={!this.state.visualization} - > - - - - - )} + const zoomStage = useCallback( + (isZoomIn: boolean, multiplier: number) => { + if (isJava()) { + JavaCseMachine.zoomStage(isZoomIn, multiplier); + } else { + Layout.zoomStage(isZoomIn, multiplier); + } + }, + [isJava] + ); + + const hotkeyBindings: HotkeyItem[] = useMemo( + () => + visualization + ? [ + ['a', stepFirst], + ['f', stepNext], + ['b', stepPrevious], + ['e', () => stepLast(props.stepsTotal)] + ] + : [ + ['a', () => {}], + ['f', () => {}], + ['b', () => {}], + ['e', () => {}] + ], + [visualization, stepFirst, stepNext, stepPrevious, props.stepsTotal, stepLast] + ); + + return ( + +
+ +
+ {!isJava() && ( -
-
{' '} - {this.state.visualization && - this.props.machineOutput.length && - this.props.machineOutput[0].type === 'errors' ? ( - this.props.machineOutput.map((slice, index) => ( - - )) - ) : ( -
- )} - {this.state.visualization ? ( - this.state.stepLimitExceeded ? ( -
- Maximum number of steps exceeded. - - Please increase the step limit if you would like to see futher evaluation. -
- ) : ( - this.state.visualization - ) - ) : ( + + + + + + + )} +
+
+ {visualization && props.machineOutput.length && props.machineOutput[0].type === 'errors' ? ( + props.machineOutput.map((slice, index) => ( + + )) + ) : ( +
+ )} + {visualization ? ( + stepLimitExceeded ? (
- {this.isJava() ? ( - - The CSEC machine generates control, stash, environment and class model diagrams - adapted from the notation introduced in{' '} - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter - 3, Section 2 - - - {'. '} - You have chosen the sublanguage{' '} - - Java CSEC - - - ) : ( - - The CSE machine generates control, stash and environment model diagrams following a - notation introduced in{' '} - - - Structure and Interpretation of Computer Programs, JavaScript Edition, Chapter - 3, Section 2 - - - - )} - . -
-
On this tab, the REPL will be hidden from view, so do check that your code has no - errors before running the stepper. You may use this tool by running your program and - then dragging the slider above to see the state of the control, stash and environment at - different stages in the evaluation of your program. Clicking on the fast-forward button - (double chevron) will take you to the next breakpoint in your program -
-
+ Maximum number of steps exceeded. - Some useful keyboard shortcuts: -
-
- a: Move to the first step -
- e: Move to the last step -
- f: Move to the next step -
- b: Move to the previous step -
-
- Note that these shortcuts are only active when the browser focus is on this tab. + Please increase the step limit if you would like to see futher evaluation.
- )} - -