Skip to content
Snippets Groups Projects
Commit 77d2f23e authored by David Sehnal's avatar David Sehnal
Browse files

wip: layout state

parent bbaef7df
No related branches found
No related tags found
No related merge requests found
......@@ -9,6 +9,7 @@ import { PluginCommand } from './command/base';
import { Transform, State } from 'mol-state';
import { StateAction } from 'mol-state/action';
import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
import { PluginLayoutStateProps } from './layout';
export * from './command/base';
......@@ -38,6 +39,9 @@ export const PluginCommands = {
OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }),
}
},
Layout: {
Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true })
},
Camera: {
Reset: PluginCommand<{}>({ isImmediate: true }),
SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }),
......
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { PluginContext } from './context';
import { shallowMergeArray } from 'mol-util/object';
export class PluginComponent<State> {
private _state: BehaviorSubject<State>;
private _updated = new Subject();
updateState(...states: Partial<State>[]) {
const latest = this.latestState;
const s = shallowMergeArray(latest, states);
if (s !== latest) {
this._state.next(s);
}
}
get states() {
return <Observable<State>>this._state;
}
get latestState() {
return this._state.value;
}
get updated() {
return <Observable<{}>>this._updated;
}
triggerUpdate() {
this._updated.next({});
}
constructor(public context: PluginContext, initialState: State) {
this._state = new BehaviorSubject<State>(initialState);
}
}
\ No newline at end of file
......@@ -27,6 +27,7 @@ import { ajaxGet } from 'mol-util/data-source';
import { CustomPropertyRegistry } from './util/custom-prop-registry';
import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
import { PluginLayout } from './layout';
export class PluginContext {
private disposed = false;
......@@ -70,6 +71,7 @@ export class PluginContext {
};
readonly canvas3d: Canvas3D;
readonly layout: PluginLayout = new PluginLayout(this);
readonly lociLabels: LociLabelManager;
......@@ -87,6 +89,8 @@ export class PluginContext {
initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
try {
this.layout.setRoot(container);
if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout);
(this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
this.canvas3d.animate();
......
......@@ -4,4 +4,175 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
// TODO
\ No newline at end of file
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { PluginComponent } from './component';
import { PluginContext } from './context';
import { PluginCommands } from './command';
export const PluginLayoutStateParams = {
isExpanded: PD.Boolean(false),
showControls: PD.Boolean(true)
}
export type PluginLayoutStateProps = PD.Values<typeof PluginLayoutStateParams>
interface RootState {
top: string | null,
bottom: string | null,
left: string | null,
right: string | null,
width: string | null,
height: string | null,
maxWidth: string | null,
maxHeight: string | null,
margin: string | null,
marginLeft: string | null,
marginRight: string | null,
marginTop: string | null,
marginBottom: string | null,
scrollTop: number,
scrollLeft: number,
position: string | null,
overflow: string | null,
viewports: HTMLElement[],
zindex: string | null
}
export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
private updateProps(state: Partial<PluginLayoutStateProps>) {
let prevExpanded = !!this.latestState.isExpanded;
this.updateState(state);
if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
this.triggerUpdate();
}
private root: HTMLElement;
private rootState: RootState | undefined = void 0;
private expandedViewport: HTMLMetaElement;
setRoot(root: HTMLElement) {
this.root = root;
if (this.latestState.isExpanded) this.handleExpand();
}
private getScrollElement() {
if ((document as any).scrollingElement) return (document as any).scrollingElement;
if (document.documentElement) return document.documentElement;
return document.body;
}
private handleExpand() {
try {
let body = document.getElementsByTagName('body')[0];
let head = document.getElementsByTagName('head')[0];
if (!body || !head) return;
if (this.latestState.isExpanded) {
let children = head.children;
let hasExp = false;
let viewports: HTMLElement[] = [];
for (let i = 0; i < children.length; i++) {
if (children[i] === this.expandedViewport) {
hasExp = true;
} else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
viewports.push(children[i] as any);
}
}
for (let v of viewports) {
head.removeChild(v);
}
if (!hasExp) head.appendChild(this.expandedViewport);
let s = body.style;
let doc = this.getScrollElement();
let scrollLeft = doc.scrollLeft;
let scrollTop = doc.scrollTop;
this.rootState = {
top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex,
width: s.width, height: s.height,
maxWidth: s.maxWidth, maxHeight: s.maxHeight,
margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
};
s.overflow = 'hidden';
s.position = 'fixed';
s.top = '0';
s.bottom = '0';
s.right = '0';
s.left = '0';
s.width = '100%';
s.height = '100%';
s.maxWidth = '100%';
s.maxHeight = '100%';
s.margin = '0';
s.marginLeft = '0';
s.marginRight = '0';
s.marginTop = '0';
s.marginBottom = '0';
this.root.style.zIndex = '2147483647';
} else {
let children = head.children;
for (let i = 0; i < children.length; i++) {
if (children[i] === this.expandedViewport) {
head.removeChild(this.expandedViewport);
break;
}
}
if (this.rootState) {
let s = body.style, t = this.rootState;
for (let v of t.viewports) {
head.appendChild(v);
}
s.top = t.top;
s.bottom = t.bottom;
s.left = t.left;
s.right = t.right;
s.width = t.width;
s.height = t.height;
s.maxWidth = t.maxWidth;
s.maxHeight = t.maxHeight;
s.margin = t.margin;
s.marginLeft = t.marginLeft;
s.marginRight = t.marginRight;
s.marginTop = t.marginTop;
s.marginBottom = t.marginBottom;
s.position = t.position;
s.overflow = t.overflow;
let doc = this.getScrollElement();
doc.scrollTop = t.scrollTop;
doc.scrollLeft = t.scrollLeft;
this.rootState = void 0;
this.root.style.zIndex = t.zindex;
}
}
} catch (e) {
this.context.log.error('Layout change error, you might have to reload the page.');
console.log('Layout change error, you might have to reload the page.', e);
}
}
constructor(context: PluginContext) {
super(context, PD.getDefaultValues(PluginLayoutStateParams));
PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
// <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
this.expandedViewport = document.createElement('meta') as any;
this.expandedViewport.name = 'viewport';
this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
}
}
\ No newline at end of file
......@@ -218,7 +218,7 @@
//border-left-style: solid;
//border-left-color: color-increase-contrast($default-background, 10%);
margin-bottom: 1px;
margin-bottom: 0px;
padding-top: 1px;
}
......
......@@ -7,12 +7,14 @@
import { StateAction } from 'mol-state/action';
import { Transformer } from 'mol-state';
import { StateTransformParameters } from './ui/state/common';
import { PluginLayoutStateProps } from './layout';
export { PluginSpec }
interface PluginSpec {
actions: PluginSpec.Action[],
behaviors: PluginSpec.Behavior[]
behaviors: PluginSpec.Behavior[],
initialLayout?: PluginLayoutStateProps
}
namespace PluginSpec {
......
......@@ -4,6 +4,29 @@
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react';
export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
state = { isExpanded: !!this.props.initialExpanded }
toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
render() {
return <div className='msp-control-group-wrapper'>
<div className='msp-control-group-header'>
<button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
<span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
{this.props.header}
</button>
</div>
{this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
{this.props.children}
</div>
}
</div>
}
}
// export const ToggleButton = (props: {
// onChange: (v: boolean) => void,
// value: boolean,
......
......@@ -13,6 +13,8 @@ import { PluginCommands } from 'mol-plugin/command';
import { ParamDefinition as PD } from 'mol-util/param-definition';
import { ParameterControls } from './controls/parameters';
import { Canvas3DParams } from 'mol-canvas3d/canvas3d';
import { PluginLayoutStateParams } from 'mol-plugin/layout';
import { ControlGroup } from './controls/common';
interface ViewportState {
noWebGl: boolean
......@@ -20,8 +22,7 @@ interface ViewportState {
export class ViewportControls extends PluginComponent {
state = {
isSettingsExpanded: false,
settings: PD.getDefaultValues(Canvas3DParams)
isSettingsExpanded: false
}
resetCamera = () => {
......@@ -33,21 +34,21 @@ export class ViewportControls extends PluginComponent {
e.currentTarget.blur();
}
// hideSettings = () => {
// this.setState({ isSettingsExpanded: false });
// }
setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { [p.name]: p.value } });
}
componentDidMount() {
if (this.plugin.canvas3d) {
this.setState({ settings: this.plugin.canvas3d.props });
setLayout = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
}
componentDidMount() {
this.subscribe(this.plugin.events.canvad3d.settingsUpdated, e => {
this.setState({ settings: this.plugin.canvas3d.props });
this.forceUpdate();
});
this.subscribe(this.plugin.layout.updated, () => {
this.forceUpdate();
});
}
......@@ -60,7 +61,12 @@ export class ViewportControls extends PluginComponent {
</div>
{this.state.isSettingsExpanded &&
<div className='msp-viewport-controls-scene-options'>
<ParameterControls params={Canvas3DParams} values={this.state.settings} onChange={this.setSettings} />
<ControlGroup header='Layout' initialExpanded={true}>
<ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} />
</ControlGroup>
<ControlGroup header='Viewport' initialExpanded={true}>
<ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
</ControlGroup>
</div>}
</div>
}
......
......@@ -41,6 +41,10 @@ export function shallowEqual<T>(a: T, b: T) {
}
export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T {
return shallowMergeArray(source, rest);
}
export function shallowMergeArray<T>(source: T, rest: (Partial<T> | undefined)[]): T {
// Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
let ret: any = source;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment