Skip to main content

GUI Component

The GUI component is the heart of MistWarp's interface, orchestrating all major UI elements and managing the overall application layout. This component serves as the main container that brings together the blocks editor, stage, sprite management, and various modals.

Component Overview

Located at src/components/gui/gui.jsx, the GUI component is a complex React component that:

  • Manages the overall application layout
  • Coordinates between different editing modes (blocks, costumes, sounds)
  • Handles full-screen and embedded modes
  • Manages modal visibility and state
  • Integrates with the theme system

File Structure

src/components/gui/
├── gui.jsx # Main GUI component
├── gui.css # Component styles
├── icon--code.svg # Code tab icon
├── icon--costumes.svg # Costumes tab icon
├── icon--sounds.svg # Sounds tab icon
└── icon--extensions.svg # Extensions button icon

Component Architecture

Main Layout Structure

const GUIComponent = props => {
return (
<MediaQuery minWidth={1024}>
{isDesktop => (
<Box className={styles.pageWrapper}>
{/* Menu Bar */}
<MenuBar {...menuBarProps} />

{/* Main Content Area */}
<Box className={styles.bodyWrapper}>
<Box className={styles.flexWrapper}>

{/* Left Panel - Blocks Editor */}
<Box className={styles.editorWrapper}>
<Tabs selectedIndex={activeTabIndex}>
<TabList className={styles.tabList}>
<Tab className={styles.tab}>Code</Tab>
<Tab className={styles.tab}>Costumes</Tab>
<Tab className={styles.tab}>Sounds</Tab>
</TabList>

<TabPanel className={styles.tabPanel}>
<Blocks vm={vm} />
</TabPanel>
<TabPanel className={styles.tabPanel}>
<CostumeTab vm={vm} />
</TabPanel>
<TabPanel className={styles.tabPanel}>
<SoundTab vm={vm} />
</TabPanel>
</Tabs>
</Box>

{/* Right Panel - Stage and Targets */}
<Box className={styles.stageAndTargetWrapper}>
<StageWrapper vm={vm} />
<TargetPane vm={vm} />
</Box>
</Box>
</Box>

{/* Modals and Overlays */}
<ExtensionLibrary vm={vm} />
<CostumeLibrary vm={vm} />
<SoundLibrary vm={vm} />
<Alerts />
</Box>
)}
</MediaQuery>
);
};

Key Features

Responsive Layout

The GUI adapts to different screen sizes using react-responsive:

<MediaQuery minWidth={1024}>
{isDesktop => (
<Box className={isDesktop ? styles.desktop : styles.mobile}>
{/* Content adapts to screen size */}
</Box>
)}
</MediaQuery>

Tab Management

The component manages three main editing tabs:

const BLOCKS_TAB_INDEX = 0;
const COSTUMES_TAB_INDEX = 1;
const SOUNDS_TAB_INDEX = 2;

const handleActivateTab = (tabIndex) => {
if (tabIndex === COSTUMES_TAB_INDEX) {
onActivateCostumesTab();
} else if (tabIndex === SOUNDS_TAB_INDEX) {
onActivateSoundsTab();
}
onActivateTab(tabIndex);
};

Mode Handling

Different display modes are supported:

// Full screen mode
if (isFullScreen) {
return (
<div className={styles.fullscreenBackground}>
<StageWrapper vm={vm} />
</div>
);
}

// Player-only mode
if (isPlayerOnly) {
return (
<Box className={styles.playerOnly}>
<StageWrapper vm={vm} />
<Controls vm={vm} />
</Box>
);
}

// Embedded mode
if (isEmbedded) {
return (
<Box className={styles.embedded}>
{/* Simplified interface */}
</Box>
);
}

CSS Architecture

Layout System

The GUI uses Flexbox for responsive layout:

.flex-wrapper {
display: flex;
flex-direction: row;
height: 100%;
overflow: hidden;
}

.editor-wrapper {
flex-basis: calc(1024px - 408px - (($space + $stage-standard-border-width) * 2));
flex-grow: 1;
flex-shrink: 0;
position: relative;
display: flex;
flex-direction: column;
}

.stage-and-target-wrapper {
display: flex;
flex-direction: column;
flex-basis: 0;
padding-left: $space;
padding-right: $space;
}

Theme Integration

CSS variables enable dynamic theming:

.page-wrapper {
height: 100%;
background-color: var(--ui-primary);
color: var(--text-primary);
}

.body-wrapper {
height: calc(100% - $menu-bar-height);
background-color: var(--ui-primary);
}

.tab {
background-color: var(--ui-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}

.tab.is-selected {
background-color: var(--ui-white);
color: var(--text-primary);
}

Responsive Breakpoints

/* Desktop (1024px+) */
@media (min-width: 1024px) {
.editor-wrapper {
min-width: 480px;
}
}

/* Tablet (768px - 1023px) */
@media (max-width: 1023px) {
.flex-wrapper {
flex-direction: column;
}
}

/* Mobile (< 768px) */
@media (max-width: 767px) {
.stage-and-target-wrapper {
order: -1;
}
}

Props Interface

Required Props

interface GUIProps {
vm: VM; // Scratch VM instance
activeTabIndex: number; // Current active tab (0-2)
onActivateTab: (index: number) => void;
}

Optional Props

interface OptionalGUIProps {
// Display modes
isFullScreen?: boolean;
isPlayerOnly?: boolean;
isEmbedded?: boolean;

// Project state
loading?: boolean;
projectId?: string;

// Modal visibility
extensionLibraryVisible?: boolean;
costumeLibraryVisible?: boolean;
soundLibraryVisible?: boolean;

// Event handlers
onRequestCloseExtensionLibrary?: () => void;
onRequestCloseCostumeLibrary?: () => void;
onRequestCloseSoundLibrary?: () => void;

// Customization
className?: string;
style?: React.CSSProperties;
}

State Management Integration

Redux Connection

The GUI component connects to multiple Redux state slices:

const mapStateToProps = state => ({
// Tab management
activeTabIndex: state.scratchGui.editorTab.activeTabIndex,

// Display modes
isFullScreen: state.scratchGui.mode.isFullScreen,
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
isEmbedded: state.scratchGui.mode.isEmbedded,

// Project state
loading: getIsLoading(state.scratchGui.projectState.loadingState),
projectId: state.scratchGui.projectState.projectId,

// Modal visibility
extensionLibraryVisible: state.scratchGui.modals.extensionLibrary,
costumeLibraryVisible: state.scratchGui.modals.costumeLibrary,
soundLibraryVisible: state.scratchGui.modals.soundLibrary,

// Theme
theme: state.scratchGui.theme.theme,

// MistWarp specific
customStageSize: state.scratchGui.customStageSize
});

Action Dispatchers

const mapDispatchToProps = dispatch => ({
onActivateTab: tabIndex => dispatch(activateTab(tabIndex)),
onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)),
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),

onRequestCloseExtensionLibrary: () => dispatch(closeExtensionLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
onRequestCloseSoundLibrary: () => dispatch(closeSoundLibrary())
});

Event Handling

Tab Switching

const handleActivateTab = useCallback((tabIndex) => {
// Special handling for certain tabs
if (tabIndex === COSTUMES_TAB_INDEX) {
onActivateCostumesTab();
} else if (tabIndex === SOUNDS_TAB_INDEX) {
onActivateSoundsTab();
}

onActivateTab(tabIndex);

// Analytics tracking
if (window.gtag) {
window.gtag('event', 'tab_switch', {
tab_name: ['blocks', 'costumes', 'sounds'][tabIndex]
});
}
}, [onActivateTab, onActivateCostumesTab, onActivateSoundsTab]);

Keyboard Shortcuts

useEffect(() => {
const handleKeyDown = (e) => {
// Tab switching shortcuts
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case '1':
e.preventDefault();
handleActivateTab(BLOCKS_TAB_INDEX);
break;
case '2':
e.preventDefault();
handleActivateTab(COSTUMES_TAB_INDEX);
break;
case '3':
e.preventDefault();
handleActivateTab(SOUNDS_TAB_INDEX);
break;
}
}
};

document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleActivateTab]);

Performance Optimizations

Memoization

const GUI = React.memo(({ vm, activeTabIndex, ...props }) => {
// Memoize expensive calculations
const stageSize = useMemo(() =>
resolveStageSize(props.stageSizeMode, props.isFullScreen),
[props.stageSizeMode, props.isFullScreen]
);

// Memoize event handlers
const handleActivateTab = useCallback((tabIndex) => {
props.onActivateTab(tabIndex);
}, [props.onActivateTab]);

return (
// Component JSX
);
});

Lazy Loading

// Lazy load heavy components
const CostumeTab = React.lazy(() => import('../../containers/costume-tab.jsx'));
const SoundTab = React.lazy(() => import('../../containers/sound-tab.jsx'));

const GUI = () => (
<Suspense fallback={<Loader />}>
<TabPanel>
<CostumeTab vm={vm} />
</TabPanel>
<TabPanel>
<SoundTab vm={vm} />
</TabPanel>
</Suspense>
);

Addon Integration Points

Shared Spaces

The GUI provides several "shared spaces" where addons can inject content:

// Available shared spaces
const SHARED_SPACES = {
stageHeader: '.stage-header',
editorTabs: '.tab-list',
menuBar: '.menu-bar',
fullscreenButton: '.fullscreen-button'
};

// Addon usage example
addon.tab.appendToSharedSpace({
space: 'stageHeader',
element: myButton,
order: 1
});

Component Wrapping

Addons can wrap GUI components:

// HOC for wrapping GUI
const withAddonEnhancements = (WrappedComponent) => {
return (props) => {
// Addon modifications
const enhancedProps = {
...props,
additionalFeatures: true
};

return <WrappedComponent {...enhancedProps} />;
};
};

Testing

Unit Tests

describe('GUI Component', () => {
let mockVM;

beforeEach(() => {
mockVM = {
on: jest.fn(),
off: jest.fn(),
start: jest.fn(),
greenFlag: jest.fn()
};
});

it('renders without crashing', () => {
render(
<GUI
vm={mockVM}
activeTabIndex={0}
onActivateTab={jest.fn()}
/>
);
});

it('switches tabs correctly', () => {
const onActivateTab = jest.fn();
const { getByText } = render(
<GUI
vm={mockVM}
activeTabIndex={0}
onActivateTab={onActivateTab}
/>
);

fireEvent.click(getByText('Costumes'));
expect(onActivateTab).toHaveBeenCalledWith(1);
});
});

Integration Tests

describe('GUI Integration', () => {
it('coordinates with VM correctly', async () => {
const { container } = render(
<Provider store={store}>
<GUI vm={mockVM} />
</Provider>
);

// Simulate VM events
act(() => {
mockVM.emit('PROJECT_LOADED');
});

await waitFor(() => {
expect(container.querySelector('.blocks-wrapper')).toBeInTheDocument();
});
});
});

Common Customizations

Custom Tab

// Adding a custom tab
const CustomGUI = (props) => (
<GUI {...props}>
<TabList className={styles.tabList}>
<Tab>Code</Tab>
<Tab>Costumes</Tab>
<Tab>Sounds</Tab>
<Tab>Extensions</Tab> {/* Custom tab */}
</TabList>

<TabPanel><Blocks vm={props.vm} /></TabPanel>
<TabPanel><CostumeTab vm={props.vm} /></TabPanel>
<TabPanel><SoundTab vm={props.vm} /></TabPanel>
<TabPanel><ExtensionTab vm={props.vm} /></TabPanel>
</GUI>
);

Layout Modifications

/* Custom layout for wider screens */
@media (min-width: 1440px) {
.editor-wrapper {
flex-basis: 60%;
}

.stage-and-target-wrapper {
flex-basis: 40%;
}
}

The GUI component is the cornerstone of MistWarp's interface, providing a flexible and extensible foundation for the entire application. Its modular design and integration points make it easy to customize and extend while maintaining performance and usability.


For more details on specific child components, see their individual documentation pages.