Source: ui/scrollable-panel.js

import { VRSPACEUI } from './vrspace-ui.js';
import { TextWriter } from './text-writer.js';
import { TextArea } from './text-area.js';

/**
 * A 3D panel displayed in the world, with arbitrary number of holographic buttons.
 * Each button must have text and image. Text is displayed when pointer is over the button,
 * and it can be either 3D (framerate impact) or texture (readability impact). This is controlled by
 * text3d property, false by default.
 */
export class ScrollablePanel {
  constructor(scene, name) {
    this.scene = scene;
    this.uiRoot = new BABYLON.TransformNode(name);
    this.text3d = false;

    this.uiRoot.position = new BABYLON.Vector3(0, 3, 0);
    this.uiRoot.rotation = new BABYLON.Vector3(0, 0, 0);
    //this.guiManager = new BABYLON.GUI.GUI3DManager(this.scene); // causes transparency issues
    this.guiManager = VRSPACEUI.guiManager;
    this.panel = new BABYLON.GUI.CylinderPanel();
    this.panel.blocklayout = true; // optimization, requires updateLayout() call
    this.panel.margin = 0.05;
    this.panel.columns = 6;
    this.guiManager.addControl(this.panel);
    this.panel.linkToTransformNode(this.uiRoot);

    this.buttonPrev = new BABYLON.GUI.HolographicButton("prev");
    this.buttonPrev.imageUrl = VRSPACEUI.contentBase + "/content/icons/upload.png";
    this.guiManager.addControl(this.buttonPrev);
    this.buttonPrev.linkToTransformNode(this.uiRoot);
    this.buttonPrev.position = new BABYLON.Vector3(-4, 0, 4);
    this.buttonPrev.mesh.rotation = new BABYLON.Vector3(0, 0, Math.PI / 2);
    this.buttonPrev.tooltipText = "Previous";
    this.buttonPrev.isVisible = false;

    this.buttonNext = new BABYLON.GUI.HolographicButton("next");
    this.buttonNext.imageUrl = VRSPACEUI.contentBase + "/content/icons/upload.png";
    this.guiManager.addControl(this.buttonNext);
    this.buttonNext.linkToTransformNode(this.uiRoot);
    this.buttonNext.position = new BABYLON.Vector3(4, 0, 4);
    this.buttonNext.mesh.rotation = new BABYLON.Vector3(0, 0, -Math.PI / 2);
    this.buttonNext.tooltipText = "Next";
    this.buttonNext.isVisible = false;

    // same material used for all buttons in this UI:
    this.buttonNext.backMaterial.alpha = .5;
  }

  /**
   * Relocate panel to given distance from the camera, by default 6
   */
  relocatePanel(distanceFromCamera = 6) {
    var forwardDirection = VRSPACEUI.hud.camera.getForwardRay(distanceFromCamera).direction;
    this.uiRoot.position = VRSPACEUI.hud.camera.position.add(forwardDirection);
    this.uiRoot.rotation = new BABYLON.Vector3(VRSPACEUI.hud.camera.rotation.x, VRSPACEUI.hud.camera.rotation.y, VRSPACEUI.hud.camera.rotation.z);
  }

  /**
   * Call this before consecutive addButton calls.
   * @param hasPrevious true if previous button is to be rendered
   * @param hasNext true if next button is to be rendered
   * @param onPrevious callback to be executed when previous button is activated
   * @param onNext callback to be executed when next button is activated
   */
  beginUpdate(hasPrevious, hasNext, onPrevious, onNext) {
    // workaround for panel buttons all messed up
    this.previousCoord = { pos: this.uiRoot.position, rot: this.uiRoot.rotation };
    this.uiRoot.position = new BABYLON.Vector3(0, 2, 0);
    this.uiRoot.rotation = new BABYLON.Vector3(0, 0, 0);
    this.panel.linkToTransformNode();

    this.panel.children.forEach((button) => { button.dispose() });

    this.buttonPrev.isVisible = hasPrevious;
    this.buttonPrev.onPointerDownObservable.clear();
    this.buttonPrev.onPointerDownObservable.add(onPrevious);

    this.buttonNext.isVisible = hasNext;
    this.buttonNext.onPointerDownObservable.clear();
    this.buttonNext.onPointerDownObservable.add(onNext);

  }

  /**
   * Call this after all buttons are added. Optionally relocates the panel.
   */
  endUpdate(relocate) {
    this.panel.linkToTransformNode(this.uiRoot);
    this.panel.updateLayout();
    if (relocate) {
      this.relocatePanel();
    } else {
      this.uiRoot.position = this.previousCoord.pos;
      this.uiRoot.rotation = this.previousCoord.rot;
    }
  }

  /**
   * Create and add a holographic button to the panel.
   * @param text to be rendered on pointer over, String or array of String
   * @param image url to display over the button
   * @param callback function called on pointer down, takes the button as the argument
   */
  addButton(text, image, callback) {
    if (typeof (text) === "string") {
      text = [text];
    }

    var button = new BABYLON.GUI.HolographicButton(text[0]);
    this.panel.addControl(button);

    button.imageUrl = image;

    button.plateMaterial.disableLighting = true;

    button.content.scaleX = 2;
    button.content.scaleY = 2;

    button.onPointerEnterObservable.add(() => {
      this.buttonTextWrite(button.node, text);
    });
    button.onPointerOutObservable.add(() => {
      this.buttonTextClear(button.node);
    });
    button.onPointerDownObservable.add(() => callback(button));

  }
  /**
   * Internally called to show tooltip text on pointer enter
   */
  buttonTextWrite(node, lines) {
    if (this.text3d) {
      if ( ! this.writer ) {
        this.writer = new TextWriter(this.scene);
      }
      this.writer.writeArray(node, lines);
    } else {
      if (!this.textArea) {
        this.textArea = new TextArea(this.scene);
        this.textArea.addHandles = false;
        this.textArea.size = 1;
        this.textArea.addBackground = false;
        this.textArea.height = 128;
        this.textArea.width = 256;
        this.textArea.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
        this.textArea.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
        this.textArea.position = new BABYLON.Vector3(0, 1, -.2);
        this.textArea.show();
      }
      this.textArea.group.parent = node;
      this.textArea.writeArray(lines);
    }
  }
  /**
   * Internally called to remove tooltip on pointer exit
   */
  buttonTextClear(node) {
    if (this.text3d) {
      this.writer.clear(node);
    } else if (this.textArea) {
      this.textArea.dispose();
      this.textArea = null;
    }
  }

  /**
   * Clean up
   */
  dispose() {
    if ( this.textArea ) {
      this.textArea.dispose();
    }
    if ( this.writer ) {
      // currently we can't dispose of writer
      //this.writer.dispose();
    }
    this.buttonPrev.dispose();
    this.buttonNext.dispose();
    this.panel.dispose();
  }
}