From 77d2f23ede9bf5112cd4e40323cfda11857dab57 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Mon, 11 Feb 2019 13:35:48 +0100 Subject: [PATCH] wip: layout state --- src/mol-plugin/command.ts | 4 + src/mol-plugin/component.ts | 42 +++++ src/mol-plugin/context.ts | 4 + src/mol-plugin/layout.ts | 173 +++++++++++++++++- .../skin/base/components/controls.scss | 2 +- src/mol-plugin/spec.ts | 4 +- src/mol-plugin/ui/controls/common.tsx | 25 ++- src/mol-plugin/ui/viewport.tsx | 30 +-- src/mol-util/object.ts | 4 + 9 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 src/mol-plugin/component.ts diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 6e8cfff33..d17b98dae 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -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 }), diff --git a/src/mol-plugin/component.ts b/src/mol-plugin/component.ts new file mode 100644 index 000000000..56577179e --- /dev/null +++ b/src/mol-plugin/component.ts @@ -0,0 +1,42 @@ +/** + * 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 diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 5f616a0fb..0e524b8b3 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -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(); diff --git a/src/mol-plugin/layout.ts b/src/mol-plugin/layout.ts index 7c7fe8338..66e3f7d7d 100644 --- a/src/mol-plugin/layout.ts +++ b/src/mol-plugin/layout.ts @@ -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 diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index 31474759c..3605b3cd9 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/src/mol-plugin/skin/base/components/controls.scss @@ -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; } diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts index 40413fd87..86b955008 100644 --- a/src/mol-plugin/spec.ts +++ b/src/mol-plugin/spec.ts @@ -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 { diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 07e2c3813..5c3308253 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -4,12 +4,35 @@ * @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, // label: string, // title?: string -// }) => <div className='lm-control-row lm-toggle-button' title={props.title}> +// }) => <div className='lm-control-row lm-toggle-button' title={props.title}> // <span>{props.label}</span> // <div> // <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}> diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index 6ccfd15a8..85eacd40a 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -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> } diff --git a/src/mol-util/object.ts b/src/mol-util/object.ts index 8521c9675..a88d70325 100644 --- a/src/mol-util/object.ts +++ b/src/mol-util/object.ts @@ -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; -- GitLab