import "./GUIController.css";
import React from "react";
import Viewport from "./Viewport";
import BrushActions from "./BrushActions";
import BrushTypes from "./BrushTypes";
import File from "./File";
import Render from "./Render";
import Examples from "./Examples";
import GitHubLink from "./GitHubLink";
import ColorPalette from "./ColorPalette";
import {
Modal,
Button,
Grid,
Segment,
Menu,
Accordion,
Header,
Icon,
} from "semantic-ui-react";
/**
* Handles switching between both desktop and mobile versions of the
* UI. Whenever one of its chidlren updates, it will pass that data
* up to its parent component.
* @extends React.Component
*/
class GUIController extends React.Component {
constructor(props) {
super(props);
this.state = {
isMobile: false,
mobile: {
isModalOpen: false,
modalContentType: "",
},
desktop: {
brushSettings: {
activeAccordionIndices: [0, 1],
},
colorPalette: {
activeAccordionIndices: [0],
},
},
};
}
/**
* Handler for screen resize events that updates whether or not the application
* should currently be using the mobile or desktop GUI.
*/
updateMobileState = () => {
// If width below 768, use mobile GUI
const isMobile = window.innerWidth < 768;
this.setState({ isMobile });
};
/**
* Toggles the accordion at the given index for the given state with a
* activeAccordionIndices property.
* @param {number} index - Index of accordion to toggle
* @param {string} componentName - The state.desktop property with a activeAccordionIndices property
*/
handleAccordionIndicesChange = (index, componentName) => {
// Make a copy of the desktop state object
const desktop = { ...this.state.desktop };
// Get the active indices for that component
const { activeAccordionIndices } = desktop[componentName];
// Get the position of the accordion index
const pos = activeAccordionIndices.indexOf(index);
// Position was found, remove the index for the user is closing the accordion
if (pos !== -1) {
activeAccordionIndices.splice(pos, 1);
}
// Position not found, add the index
else {
activeAccordionIndices.push(index);
}
// Update the state of the active indices for the given component
this.setState({ desktop });
};
componentDidMount() {
// Perform initial check for mobile device
this.updateMobileState();
window.addEventListener("resize", this.updateMobileState);
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateMobileState);
}
/**
* Creates the JSX for the desktop version of the viewport.
* @returns {JSX}
*/
createDesktopViewport() {
return <Viewport callbacks={this.props.callbacks.viewport} />;
}
/**
* Creates the JSX for the desktop version of the brush.
* @returns {JSX}
*/
createDesktopBrush = () => {
const { brushSettings } = this.state.desktop;
return (
<Segment.Group>
<Segment inverted>
<Header as="h4" inverted>
<Icon name="paint brush" />
<Header.Content>
Brush Settings
<Header.Subheader>Add, remove, or paint voxels</Header.Subheader>
</Header.Content>
</Header>
<Accordion inverted fluid exclusive={false}>
<Accordion.Title
active={brushSettings.activeAccordionIndices.includes(0)}
content="Brush Actions"
index={0}
onClick={(e, titleProps) => {
this.handleAccordionIndicesChange(
titleProps.index,
"brushSettings"
);
}}
/>
<Accordion.Content
active={brushSettings.activeAccordionIndices.includes(0)}
>
<Menu inverted vertical fluid>
<BrushActions
callbacks={this.props.callbacks.brush.brushActions}
/>
</Menu>
</Accordion.Content>
</Accordion>
<Accordion inverted fluid exclusive={false}>
<Accordion.Title
active={brushSettings.activeAccordionIndices.includes(1)}
content="Brush Types"
index={1}
onClick={(e, titleProps) => {
this.handleAccordionIndicesChange(
titleProps.index,
"brushSettings"
);
}}
/>
<Accordion.Content
active={brushSettings.activeAccordionIndices.includes(1)}
>
<Menu inverted vertical fluid>
<BrushTypes callbacks={this.props.callbacks.brush.brushTypes} />
</Menu>
</Accordion.Content>
</Accordion>
</Segment>
</Segment.Group>
);
};
/**
* Creates the JSX for the desktop version of the color palette.
* @returns {JSX}
*/
createDesktopColorPalette = () => {
const { colorPalette } = this.state.desktop;
return (
<Segment.Group>
<Segment inverted>
<Header as="h4" inverted>
<Icon name="tint" />
<Header.Content>
Color Palette
<Header.Subheader>Select a color to paint with</Header.Subheader>
</Header.Content>
</Header>
<Accordion inverted fluid exclusive={false}>
<Accordion.Title
active={colorPalette.activeAccordionIndices.includes(0)}
content="Color Selection"
index={0}
onClick={(e, titleProps) => {
this.handleAccordionIndicesChange(
titleProps.index,
"colorPalette"
);
}}
/>
<Accordion.Content
active={colorPalette.activeAccordionIndices.includes(0)}
>
<ColorPalette callbacks={this.props.callbacks.colorPalette} />
</Accordion.Content>
</Accordion>
</Segment>
</Segment.Group>
);
};
/**
* Create the desktop version of the UI.
* @returns {JSX}
*/
createDesktopGUI() {
return (
<Grid padded style={{ height: "100vh" }}>
<Grid.Row style={{ paddingTop: "0", paddingBottom: "0" }}>
<Grid.Column>
<Menu inverted>
<File callbacks={this.props.callbacks.file} />
{/*<Edit />*/}
<Render callbacks={this.props.callbacks.render} />
<Examples callbacks={this.props.callbacks.examples} />
<GitHubLink />
</Menu>
</Grid.Column>
</Grid.Row>
<Grid.Row
style={{ height: "90%", paddingTop: "0", paddingBottom: "0" }}
>
<Grid.Column
width={3}
style={{
height: "100%",
overflowY: "auto",
}}
>
{this.createDesktopBrush()}
{this.createDesktopColorPalette()}
</Grid.Column>
<Grid.Column width={13} style={{ height: "100%" }}>
{this.createDesktopViewport()}
</Grid.Column>
{/* Right Panel. Empty for now.*/}
{/* <Grid.Column width={2} style={{ height: "100%" }}></Grid.Column> */}
</Grid.Row>
</Grid>
);
}
/**
* Creates JSX for modals on mobile devices.
* @returns {JSX}
*/
createMobileModal() {
return (
<Modal
className="mobileModal"
open={this.state.mobile.isModalOpen}
onClose={() =>
this.setState((prevState) => ({
mobile: { ...prevState.mobile, isModalOpen: false },
}))
}
onOpen={() =>
this.setState((prevState) => ({
mobile: { ...prevState.mobile, isModalOpen: true },
}))
}
>
{/* Populate the modal with relevant content */}
{this.createMobileModalContent()}
<Modal.Actions>
<Button
onClick={() =>
this.setState((prevState) => ({
mobile: { ...prevState.mobile, isModalOpen: false },
}))
}
primary
>
Close
</Button>
</Modal.Actions>
</Modal>
);
}
/**
* Populates the mobile modal with content relevant to what the user selected.
* For example, opening the ColorPalette will fill the modal with ColorPalette
* related JSX.
* @returns {JSX}
*/
createMobileModalContent() {
const { modalContentType } = this.state.mobile;
// Header and description of our modal
let header, description;
// Generate JSX based on the current modal type
switch (this.state.mobile.modalContentType) {
case "ColorPalette":
header = (
<Header as="h4">
<Icon name="tint" />
<Header.Content>
Color Palette
<Header.Subheader>Select a color to paint with</Header.Subheader>
</Header.Content>
</Header>
);
description = (
<ColorPalette callbacks={this.props.callbacks.colorPalette} />
);
break;
case "BrushSettings":
header = (
<Header as="h4">
<Icon name="paint brush" />
<Header.Content>
Brush Settings
<Header.Subheader>Add, remove, or paint voxels</Header.Subheader>
</Header.Content>
</Header>
);
description = (
<Menu inverted vertical fluid>
<Menu.Item header>Brush Actions</Menu.Item>
<BrushActions callbacks={this.props.callbacks.brush.brushActions} />
<Menu.Item header>Brush Types</Menu.Item>
<BrushTypes callbacks={this.props.callbacks.brush.brushTypes} />
</Menu>
);
break;
default:
header = "Empty Modal";
description = `Nothing in here. Thr current modal type is '${modalContentType}'`;
}
// Return JSX for the modal contents based on our header and description
return (
<React.Fragment>
<Modal.Header>{header}</Modal.Header>
<Modal.Content scrolling>
<Modal.Description>{description}</Modal.Description>
</Modal.Content>
</React.Fragment>
);
}
/**
* Create the mobile version of the UI.
* @returns {JSX}
*/
createMobileGUI() {
return (
<React.Fragment>
{this.createMobileModal()}
<div style={{ height: window.innerHeight }}>
{/* Create top menu */}
<Menu fixed="top" inverted>
<File callbacks={this.props.callbacks.file} />
{/*<Edit />*/}
<Render callbacks={this.props.callbacks.render} />
<Examples callbacks={this.props.callbacks.examples} />
<GitHubLink />
</Menu>
<Viewport callbacks={this.props.callbacks.viewport} />
{/* Create bottom menu */}
<Menu fixed="bottom" inverted style={{ overflowX: "auto" }}>
<Menu.Item
as="a"
onClick={() =>
this.setState({
mobile: {
isModalOpen: true,
modalContentType: "BrushSettings",
},
})
}
>
Brush Settings
</Menu.Item>
<Menu.Item
as="a"
onClick={() =>
this.setState({
mobile: {
isModalOpen: true,
modalContentType: "ColorPalette",
},
})
}
>
Color Palette
</Menu.Item>
</Menu>
</div>
</React.Fragment>
);
}
render() {
return this.state.isMobile
? this.createMobileGUI()
: this.createDesktopGUI();
}
}
export default GUIController;