<NME> shader_controls.ts
<BEF> /**
 * @license
 * Copyright 2019 Google Inc.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import debounce from 'lodash/debounce';
import {DisplayContext} from 'neuroglancer/display_context';
import {UserLayer, UserLayerConstructor} from 'neuroglancer/layer';
import {registerLayerTool, ToolActivation} from 'neuroglancer/ui/tool';
import {RefCounted} from 'neuroglancer/util/disposable';
import {RefCounted} from 'neuroglancer/util/disposable';
import {removeChildren} from 'neuroglancer/util/dom';
import {WatchableVisibilityPriority} from 'neuroglancer/visibility_priority/frontend';
import {ShaderControlState} from 'neuroglancer/webgl/shader_ui_controls';
import {ColorWidget} from 'neuroglancer/widget/color';
import {RangeWidget} from 'neuroglancer/widget/range';
import {Tab} from 'neuroglancer/widget/tab_view';

export interface ShaderControlsOptions {
  visibility?: WatchableVisibilityPriority;
}

}

function getShaderLayerControlFactory<LayerType extends UserLayer>(
    layerShaderControls: LayerShaderControls, controlId: string): LayerControlFactory<LayerType>|
    undefined {
  const {shaderControlState} = layerShaderControls;
  const controlState = shaderControlState.state.get(controlId);
  if (controlState === undefined) return undefined;
  const {control} = controlState;
  switch (control.type) {
    case 'slider':
      return rangeLayerControl(() => ({
                                 value: controlState.trackable,
                                 options: {min: control.min, max: control.max, step: control.step},
                               }));
    case 'color':
      return colorLayerControl(() => controlState.trackable);
    case 'checkbox':
      return checkboxLayerControl(() => controlState.trackable);
      removeChildren(element);
    }
    const controlDisposer = this.controlDisposer = new RefCounted();
    for (const [name, controlState] of this.state.state) {
      const {control} = controlState;
      const label = document.createElement('label');
      label.textContent = name;
      element.appendChild(label);
      switch (control.type) {
        case 'slider': {
          const widget = controlDisposer.registerDisposer(new RangeWidget(
            legendShaderOptions: layerShaderControls.legendShaderOptions,
          }));
    }
    case 'propertyInvlerp': {
      let histogramIndex = 0;
      for (const [otherName, {control: {type: otherType}}] of shaderControlState.state) {
        if (otherName === controlId) break;
          element.appendChild(widget.element);
          break;
        }
      }
    }
  }
    const {element} = this;
    element.style.display = 'contents';
    const {controls} = state;
    this.registerDisposer(
        controls.changed.add(this.registerCancellable(debounce(() => this.updateControls(), 0))));
    this.updateControls();
  }

  updateControls() {
    const {element} = this;
    if (this.controlDisposer !== undefined) {
      this.controlDisposer.dispose();
      removeChildren(element);
    }
    const controlDisposer = this.controlDisposer = new RefCounted();
    const layerShaderControlsGetter = () => ({
      shaderControlState: this.state,
      legendShaderOptions: this.options.legendShaderOptions,
    });
    for (const name of this.state.state.keys()) {
      element.appendChild(addLayerControlToOptionsTab(
          controlDisposer, this.layer, this.visibility,
          getShaderLayerControlDefinition(layerShaderControlsGetter, this.toolId, name)));
    }
  }

  disposed() {
    this.controlDisposer?.dispose();
    super.disposed();
  }
}

interface LayerShaderControls {
  shaderControlState: ShaderControlState;
  legendShaderOptions?: LegendShaderOptions;
}

export const SHADER_CONTROL_TOOL_ID = 'shaderControl';
const CONTROL_JSON_KEY = 'control';

function shaderControlToolJson(control: string, toolId: string) {
  return {type: toolId, [CONTROL_JSON_KEY]: control};
}

class ShaderControlTool extends LayerControlTool {
  constructor(
      layer: UserLayer, private layerShaderControls: LayerShaderControls, toolId: string,
      private control: string) {
    super(layer, getShaderLayerControlDefinition(() => layerShaderControls, toolId, control));
    this.registerDisposer(layerShaderControls.shaderControlState.controls.changed.add(
        this.registerCancellable(debounce(() => {
          if (layerShaderControls.shaderControlState.state.get(control) === undefined) {
            this.unbind();
          }
        }))));
  }
  activate(activation: ToolActivation<this>) {
    const {shaderControlState} = this.layerShaderControls;
    const controlState = shaderControlState.state.get(this.control);
    if (controlState === undefined) return;
    super.activate(activation);
  }
}

export function registerLayerShaderControlsTool<LayerType extends UserLayer>(
    layerType: UserLayerConstructor&AnyConstructor<LayerType>,
    getter: (layer: LayerType) => LayerShaderControls, toolId: string = SHADER_CONTROL_TOOL_ID) {
  registerLayerTool(layerType, toolId, (layer, options) => {
    const control = verifyObjectProperty(options, CONTROL_JSON_KEY, verifyString);
    return new ShaderControlTool(layer, getter(layer), toolId, control);
  });
}

<MSG> feat(image_user_layer): add support for invlerp shader UI control

<DFF> @@ -22,12 +22,22 @@ import {TrackableRGB} from 'neuroglancer/util/color';
 import {RefCounted} from 'neuroglancer/util/disposable';
 import {removeChildren} from 'neuroglancer/util/dom';
 import {WatchableVisibilityPriority} from 'neuroglancer/visibility_priority/frontend';
+import {ParameterizedEmitterDependentShaderOptions, ParameterizedShaderGetterResult} from 'neuroglancer/webgl/dynamic_shader';
 import {ShaderControlState} from 'neuroglancer/webgl/shader_ui_controls';
 import {ColorWidget} from 'neuroglancer/widget/color';
+import {InvlerpWidget} from 'neuroglancer/widget/invlerp';
 import {RangeWidget} from 'neuroglancer/widget/range';
 import {Tab} from 'neuroglancer/widget/tab_view';
+import { Position } from '../navigation_state';
+import { PositionWidget } from './position_widget';
+import { arraysEqual } from '../util/array';
+
+export interface LegendShaderOptions extends ParameterizedEmitterDependentShaderOptions {
+  initializeShader: (shaderResult: ParameterizedShaderGetterResult) => void;
+}
 
 export interface ShaderControlsOptions {
+  legendShaderOptions?: LegendShaderOptions;
   visibility?: WatchableVisibilityPriority;
 }
 
@@ -53,11 +63,14 @@ export class ShaderControls extends Tab {
       removeChildren(element);
     }
     const controlDisposer = this.controlDisposer = new RefCounted();
+    let histogramIndex = 0;
     for (const [name, controlState] of this.state.state) {
       const {control} = controlState;
+      const labelDiv = document.createElement('div');
       const label = document.createElement('label');
       label.textContent = name;
-      element.appendChild(label);
+      labelDiv.appendChild(label);
+      element.appendChild(labelDiv);
       switch (control.type) {
         case 'slider': {
           const widget = controlDisposer.registerDisposer(new RangeWidget(
@@ -71,6 +84,42 @@ export class ShaderControls extends Tab {
           element.appendChild(widget.element);
           break;
         }
+        case 'invlerp': {
+          const {channelCoordinateSpaceCombiner} = this.state;
+          if (channelCoordinateSpaceCombiner !== undefined &&
+              control.default.channel.length !== 0) {
+            const position = controlDisposer.registerDisposer(
+                new Position(channelCoordinateSpaceCombiner.combined));
+            const positionWidget = controlDisposer.registerDisposer(
+              new PositionWidget(position, channelCoordinateSpaceCombiner, {copyButton: false}));
+            const {trackable} = controlState;
+            controlDisposer.registerDisposer(position.changed.add(() => {
+              const value = position.value;
+              const newChannel = Array.from(value, x => Math.floor(x));
+              const oldParams = trackable.value;
+              if (!arraysEqual(oldParams.channel, newChannel)) {
+                trackable.value = {...trackable.value, channel: newChannel};
+              }
+            }));
+            const updatePosition = () => {
+              const value = position.value;
+              const params = trackable.value;
+              if (!arraysEqual(value, params.channel)) {
+                value.set(params.channel);
+                position.changed.dispatch();
+              }
+            };
+            updatePosition();
+            controlDisposer.registerDisposer(trackable.changed.add(updatePosition));
+            labelDiv.appendChild(positionWidget.element);
+          }
+          const widget = controlDisposer.registerDisposer(new InvlerpWidget(
+              this.visibility, this.display, control, controlState.trackable,
+              this.state.histogramSpecifications, histogramIndex, this.options));
+          element.appendChild(widget.element);
+          ++histogramIndex;
+          break;
+        }
       }
     }
   }

feat(image_user_layer): add support for invlerp shader UI control