Browse Source

Add PluginContext.mount/unmount

dsehnal 2 years ago
parent
commit
bbc43d5113
3 changed files with 58 additions and 8 deletions
  1. 2 0
      CHANGELOG.md
  2. 6 8
      src/mol-plugin-ui/viewport/canvas.tsx
  3. 50 0
      src/mol-plugin/context.ts

+ 2 - 0
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` 

+ 6 - 8
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>;
     }

+ 50 - 0
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;
     }