diff --git a/lib/atom/pane-item.js b/lib/atom/pane-item.js
index f564d021d0..3021453145 100644
--- a/lib/atom/pane-item.js
+++ b/lib/atom/pane-item.js
@@ -39,7 +39,7 @@ export default class PaneItem extends React.Component {
constructor(props) {
super(props);
- autobind(this, 'opener');
+ autobind(this, 'opener', 'notifyItemAdded');
const uriPattern = new URIPattern(this.props.uriPattern);
const currentlyOpen = this.props.workspace.getPaneItems()
@@ -56,7 +56,9 @@ export default class PaneItem extends React.Component {
return arr;
}, []);
+ this.pendingNotification = new Map();
this.subs = new CompositeDisposable();
+
this.state = {uriPattern, currentlyOpen};
}
@@ -80,9 +82,13 @@ export default class PaneItem extends React.Component {
if (this.props.className) {
openItem.addClassName(this.props.className);
}
+ openItem.didAddItem();
}
- this.subs.add(this.props.workspace.addOpener(this.opener));
+ this.subs.add(
+ this.props.workspace.onDidAddPaneItem(this.notifyItemAdded),
+ this.props.workspace.addOpener(this.opener),
+ );
}
render() {
@@ -99,6 +105,14 @@ export default class PaneItem extends React.Component {
this.subs.dispose();
}
+ notifyItemAdded({item}) {
+ const openItem = this.pendingNotification.get(item);
+ if (openItem !== undefined) {
+ openItem.didAddItem();
+ this.pendingNotification.delete(item);
+ }
+ }
+
opener(uri) {
const m = this.state.uriPattern.matches(uri);
if (!m.ok()) {
@@ -118,6 +132,7 @@ export default class PaneItem extends React.Component {
copy: () => this.copyOpenItem(openItem),
});
this.registerCloseListener(paneItem, openItem);
+ this.pendingNotification.set(paneItem, openItem);
resolve(paneItem);
});
});
@@ -149,6 +164,7 @@ export default class PaneItem extends React.Component {
if (item === paneItem) {
sub.dispose();
this.subs.remove(sub);
+ this.pendingNotification.delete(item);
this.setState(prevState => ({
currentlyOpen: prevState.currentlyOpen.filter(each => each !== openItem),
}));
@@ -159,6 +175,37 @@ export default class PaneItem extends React.Component {
}
}
+const ItemAddedContext = React.createContext(Promise.resolve());
+
+export class ItemAdded extends React.Component {
+ static propTypes = {
+ children: PropTypes.func.isRequired,
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {attached: false};
+ }
+
+ render() {
+ return (
+
+
+ {addedPromise => {
+ addedPromise.then(() => {
+ if (!this.state.attached) {
+ this.setState({attached: true});
+ }
+ });
+ }}
+
+ {this.state.attached ? this.props.children() : null}
+
+ );
+ }
+}
+
/**
* A subtree rendered through a portal onto a detached DOM node for use as the root as a PaneItem.
*/
@@ -174,6 +221,9 @@ class OpenItem {
this.domNode.onfocus = this.onFocus.bind(this);
this.stubItem = stub;
this.match = match;
+ this.itemAddedPromise = new Promise(resolve => {
+ this.resolveItemAdded = resolve;
+ });
this.itemHolder = new RefHolder();
}
@@ -217,13 +267,19 @@ class OpenItem {
return this.itemHolder.map(item => item.focus && item.focus());
}
+ didAddItem() {
+ this.resolveItemAdded();
+ }
+
renderPortal(renderProp) {
return ReactDOM.createPortal(
- renderProp({
- itemHolder: this.itemHolder,
- params: this.match.getParams(),
- uri: this.match.getURI(),
- }),
+
+ {renderProp({
+ itemHolder: this.itemHolder,
+ params: this.match.getParams(),
+ uri: this.match.getURI(),
+ })}
+ ,
this.domNode,
);
}
diff --git a/lib/controllers/root-controller.js b/lib/controllers/root-controller.js
index 3144cbb2fe..0e23ab6b23 100644
--- a/lib/controllers/root-controller.js
+++ b/lib/controllers/root-controller.js
@@ -18,6 +18,7 @@ import FilePatchController from './file-patch-controller';
import IssueishDetailItem from '../items/issueish-detail-item';
import GitTabItem from '../items/git-tab-item';
import GitHubTabItem from '../items/github-tab-item';
+import LogItem from '../items/log-item';
import StatusBarTileController from './status-bar-tile-controller';
import RepositoryConflictController from './repository-conflict-controller';
import GitCacheView from '../views/git-cache-view';
@@ -64,7 +65,8 @@ export default class RootController extends React.Component {
autobind(
this,
'installReactDevTools', 'getRepositoryForWorkdir', 'clearGithubToken', 'initializeRepo', 'showOpenIssueishDialog',
- 'showWaterfallDiagnostics', 'showCacheDiagnostics', 'acceptClone', 'cancelClone', 'acceptInit', 'cancelInit',
+ 'showLogItem', 'showWaterfallDiagnostics', 'showCacheDiagnostics',
+ 'acceptClone', 'cancelClone', 'acceptInit', 'cancelInit',
'acceptOpenIssueish', 'cancelOpenIssueish', 'surfaceFromFileAtPath', 'destroyFilePatchPaneItems',
'destroyEmptyFilePatchPaneItems', 'openCloneDialog', 'quietlySelectItem', 'viewUnstagedChangesForCurrentFile',
'viewStagedChangesForCurrentFile', 'openFiles', 'getUnsavedFiles', 'ensureNoUnsavedFiles',
@@ -126,6 +128,7 @@ export default class RootController extends React.Component {
+
)}
+
+ {({itemHolder}) => }
+
{({itemHolder}) => }
@@ -423,6 +429,10 @@ export default class RootController extends React.Component {
this.setState({openIssueishDialogActive: true});
}
+ showLogItem() {
+ return this.props.workspace.open(LogItem.buildURI());
+ }
+
showWaterfallDiagnostics() {
this.props.workspace.open(GitTimingsView.buildURI());
}
diff --git a/lib/items/log-item.js b/lib/items/log-item.js
new file mode 100644
index 0000000000..8cbd64fdd1
--- /dev/null
+++ b/lib/items/log-item.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import LogView from '../views/log-view';
+
+export default class LogItem extends React.Component {
+ static uriPattern = 'atom-github://log'
+
+ static buildURI() {
+ return this.uriPattern;
+ }
+
+ render() {
+ return ;
+ }
+
+ getTitle() {
+ return 'Git Log';
+ }
+
+ getIconName() {
+ return 'git-commit';
+ }
+
+ getURI() {
+ return this.constructor.uriPattern;
+ }
+}
diff --git a/lib/views/log-view.js b/lib/views/log-view.js
new file mode 100644
index 0000000000..d0f146a7b8
--- /dev/null
+++ b/lib/views/log-view.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import * as Three from 'three';
+
+import ThreeScene from './three-scene';
+import {autobind} from '../helpers';
+
+export default class LogView extends React.Component {
+ constructor(props) {
+ super(props);
+ autobind(this, 'setUp', 'animate');
+
+ this.cube = null;
+ this.wireframe = null;
+ }
+
+ render() {
+ return ;
+ }
+
+ setUp({scene, camera}) {
+ const geometry = new Three.BoxGeometry(1, 1, 1);
+ const faceMaterial = new Three.MeshBasicMaterial({color: 0x339999});
+ this.cube = new Three.Mesh(geometry, faceMaterial);
+
+ const lineMaterial = new Three.MeshBasicMaterial({color: 0xffffff, wireframe: true});
+ this.wireframe = new Three.Mesh(geometry, lineMaterial);
+
+ scene.add(this.cube);
+ scene.add(this.wireframe);
+
+ camera.position.z = 5;
+ }
+
+ animate({scene}) {
+ this.cube.rotation.x += 0.01;
+ this.cube.rotation.y += 0.01;
+
+ this.wireframe.rotation.x += 0.01;
+ this.wireframe.rotation.y += 0.01;
+ }
+}
diff --git a/lib/views/three-scene.js b/lib/views/three-scene.js
new file mode 100644
index 0000000000..4865424e9f
--- /dev/null
+++ b/lib/views/three-scene.js
@@ -0,0 +1,100 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import * as Three from 'three';
+import {CompositeDisposable, Disposable} from 'event-kit';
+
+import RefHolder from '../models/ref-holder';
+import {ItemAdded} from '../atom/pane-item';
+import {autobind} from '../helpers';
+
+export default class ThreeScene extends React.Component {
+ static propTypes = {
+ fieldOfView: PropTypes.number,
+ nearClippingPlane: PropTypes.number,
+ farClippingPlane: PropTypes.number,
+
+ setUp: PropTypes.func,
+ animate: PropTypes.func,
+ }
+
+ static defaultProps = {
+ fieldOfView: 75,
+ nearClippingPlane: 0.1,
+ farClippingPlane: 1000,
+
+ setUp: () => {},
+ animate: () => {},
+ }
+
+ constructor(props) {
+ super(props);
+ autobind(this, 'setUpScene', 'animateScene');
+
+ this.subs = new CompositeDisposable();
+
+ this.refRoot = new RefHolder();
+
+ this.scene = null;
+ this.camera = null;
+ this.renderer = null;
+ this.active = false;
+
+ this.subs.add(
+ this.refRoot.observe(this.setUpScene),
+ new Disposable(() => { this.active = false; }),
+ );
+ }
+
+ render() {
+ return (
+
+ {() => }
+
+ );
+ }
+
+ componentWillUnmount() {
+ this.subs.dispose();
+ }
+
+ setUpScene(rootElement) {
+ if (!rootElement) {
+ this.active = false;
+ return;
+ }
+ this.active = true;
+
+ const {width, height} = rootElement.getBoundingClientRect();
+
+ this.scene = new Three.Scene();
+
+ this.camera = new Three.PerspectiveCamera(
+ this.props.fieldOfView,
+ width / height,
+ this.props.nearClippingPlane,
+ this.props.farClippingPlane,
+ );
+
+ this.renderer = new Three.WebGLRenderer({antialias: true});
+ this.renderer.setSize(width, height);
+ rootElement.appendChild(this.renderer.domElement);
+
+ this.props.setUp({scene: this.scene, camera: this.camera});
+
+ this.animateScene();
+ }
+
+ animateScene() {
+ this.refRoot.map(rootElement => {
+ if (!this.active) {
+ return null;
+ }
+
+ requestAnimationFrame(this.animateScene);
+ this.props.animate({scene: this.scene, camera: this.camera});
+ this.renderer.render(this.scene, this.camera);
+
+ return null;
+ });
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 0f5e626432..589aa682a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7168,6 +7168,11 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
+ "three": {
+ "version": "0.96.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.96.0.tgz",
+ "integrity": "sha512-tS+A5kelQgBblElc/E1G5zR3m6wNjbqmrf6OAjijuNJM7yoYQjOktPoa+Lglx73OTiTOJ3+Ff+pgWdOFt7cOhQ=="
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
diff --git a/package.json b/package.json
index a4966cfd98..fcc0019797 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"react-select": "1.2.1",
"relay-runtime": "1.6.0",
"temp": "0.8.3",
+ "three": "0.96.0",
"tinycolor2": "1.4.1",
"tree-kill": "1.2.0",
"what-the-diff": "0.4.0",
diff --git a/styles/three-scene.less b/styles/three-scene.less
new file mode 100644
index 0000000000..ee2f29305b
--- /dev/null
+++ b/styles/three-scene.less
@@ -0,0 +1,4 @@
+.github-ThreeScene {
+ width: 100%;
+ height: 100%;
+}