diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa078502e6490fff494ff16b38c915018f2883b..d7211759e33229d017b9ab1850d51dc66a08ec7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- Add `PluginContext.mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI + ## [v3.22.0] - 2022-10-17 - Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality` diff --git a/src/mol-plugin-ui/viewport/canvas.tsx b/src/mol-plugin-ui/viewport/canvas.tsx index ab49d381a4e449ff238eea734d1b0f5def960e86..c8b29ebf5b6b476e8094655bbf5f62855571961e 100644 --- a/src/mol-plugin-ui/viewport/canvas.tsx +++ b/src/mol-plugin-ui/viewport/canvas.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -19,13 +19,14 @@ export interface ViewportCanvasParams { parentClassName?: string, parentStyle?: React.CSSProperties, + // NOTE: hostClassName/hostStyle no longer in use + // TODO: remove in 4.0 hostClassName?: string, hostStyle?: React.CSSProperties, } export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, ViewportCanvasState> { private container = React.createRef<HTMLDivElement>(); - private canvas = React.createRef<HTMLCanvasElement>(); state: ViewportCanvasState = { noWebGl: false, @@ -37,7 +38,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View }; componentDidMount() { - if (!this.canvas.current || !this.container.current || !this.plugin.initViewer(this.canvas.current!, this.container.current!)) { + if (!this.container.current || !this.plugin.mount(this.container.current!)) { this.setState({ noWebGl: true }); return; } @@ -47,7 +48,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View componentWillUnmount() { super.componentWillUnmount(); - // TODO viewer cleanup + this.plugin.unmount(); } renderMissing() { @@ -70,10 +71,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View const Logo = this.props.logo; - return <div className={this.props.parentClassName || 'msp-viewport'} style={this.props.parentStyle}> - <div className={this.props.hostClassName || 'msp-viewport-host3d'} style={this.props.hostStyle} ref={this.container}> - <canvas ref={this.canvas} /> - </div> + return <div className={this.props.parentClassName || 'msp-viewport'} style={this.props.parentStyle} ref={this.container}> {(this.state.showLogo && Logo) && <Logo />} </div>; } diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 118112f7211121472a5a7ee5487dccea8d5f24fb..16cb5b519d18bc8b69926f686b433b8304cef16e 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -73,6 +73,7 @@ export class PluginContext { protected subs: Subscription[] = []; private disposed = false; + private canvasContainer: HTMLDivElement | undefined = void 0; private ev = RxEventHelper.create(); readonly config = new PluginConfigManager(this.spec.config); // needed to init state @@ -186,6 +187,52 @@ export class PluginContext { */ readonly customState: unknown = Object.create(null); + mount(target: HTMLElement, canvas3dContext?: Canvas3DContext) { + if (this.disposed) throw new Error('Cannot mount a disposed context'); + + if (!this.canvasContainer) { + const container = document.createElement('div'); + Object.assign(container.style, { + position: 'absolute', + left: 0, + top: 0, + right: 0, + bottom: 0, + '-webkit-user-select': 'none', + 'user-select': 'none', + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)', + '-webkit-touch-callout': 'none', + 'touch-action': 'manipulation', + }); + let canvas = canvas3dContext?.canvas; + if (!canvas) { + canvas = document.createElement('canvas'); + Object.assign(canvas.style, { + 'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)', + 'background-size': '60px 60px', + 'background-position': '0 0, 30px 30px' + }); + container.appendChild(canvas); + } + if (!this.initViewer(canvas, container, canvas3dContext)) { + return false; + } + this.canvasContainer = container; + } + + if (this.canvasContainer.parentElement !== target) { + this.canvasContainer.parentElement?.removeChild(this.canvasContainer); + } + + target.appendChild(this.canvasContainer); + this.handleResize(); + return true; + } + + unmount() { + this.canvasContainer?.parentElement?.removeChild(this.canvasContainer); + } + initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement, canvas3dContext?: Canvas3DContext) { try { this.layout.setRoot(container); @@ -306,6 +353,9 @@ export class PluginContext { objectForEach(this.managers, m => (m as any)?.dispose?.()); objectForEach(this.managers.structure, m => (m as any)?.dispose?.()); + this.unmount(); + this.canvasContainer = undefined; + this.disposed = true; }