<NME> annotations.ts
<BEF> /**
 * @license
 * Copyright 2018 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.
 */

/**
 * @file User interface for display and editing annotations.
 */

import './annotations.css';

import {Annotation, AnnotationReference, AnnotationSource, annotationToJson, AnnotationType, annotationTypeHandlers, AxisAlignedBoundingBox, Ellipsoid, Line} from 'neuroglancer/annotation';
import {AnnotationDisplayState, AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state';
import {MultiscaleAnnotationSource} from 'neuroglancer/annotation/frontend_source';
import {AnnotationLayer, PerspectiveViewAnnotationLayer, SliceViewAnnotationLayer} from 'neuroglancer/annotation/renderlayer';
import {CoordinateSpace} from 'neuroglancer/coordinate_transform';
import {MouseSelectionState, UserLayer} from 'neuroglancer/layer';
import {LoadedDataSubsource} from 'neuroglancer/layer_data_source';
import {ChunkTransformParameters, getChunkPositionFromCombinedGlobalLocalPositions} from 'neuroglancer/render_coordinate_transform';
import {RenderScaleHistogram, trackableRenderScaleTarget} from 'neuroglancer/render_scale_statistics';
import {RenderLayerRole} from 'neuroglancer/renderlayer';
import {bindSegmentListWidth, registerCallbackWhenSegmentationDisplayStateChanged, SegmentationDisplayState, SegmentWidgetFactory} from 'neuroglancer/segmentation_display_state/frontend';
import {ElementVisibilityFromTrackableBoolean} from 'neuroglancer/trackable_boolean';
import {AggregateWatchableValue, makeCachedLazyDerivedWatchableValue, registerNested, WatchableValueInterface} from 'neuroglancer/trackable_value';
import {getDefaultAnnotationListBindings} from 'neuroglancer/ui/default_input_event_bindings';
import {getDefaultSelectBindings} from 'neuroglancer/ui/default_input_event_bindings';
import {registerTool, Tool} from 'neuroglancer/ui/tool';
import {animationFrameDebounce} from 'neuroglancer/util/animation_frame_debounce';
import {arraysEqual, gatherUpdate} from 'neuroglancer/util/array';
import {setClipboard} from 'neuroglancer/util/clipboard';
import {serializeColor, unpackRGB, unpackRGBA, useWhiteBackground} from 'neuroglancer/util/color';
import {Borrowed, disposableOnce, Owned, RefCounted} from 'neuroglancer/util/disposable';
import {removeChildren, removeFromParent, updateChildren} from 'neuroglancer/util/dom';
import {ValueOrError} from 'neuroglancer/util/error';
import {vec3} from 'neuroglancer/util/geom';
import {verifyInt, verifyObject, verifyObjectProperty, verifyOptionalObjectProperty, verifyString} from 'neuroglancer/util/json';
import * as matrix from 'neuroglancer/util/matrix';
import * as matrix from 'neuroglancer/util/matrix';
import {MouseEventBinder} from 'neuroglancer/util/mouse_bindings';
import {formatScaleWithUnitAsString} from 'neuroglancer/util/si_units';
import {NullarySignal} from 'neuroglancer/util/signal';
import {formatIntegerBounds, formatIntegerPoint} from 'neuroglancer/util/spatial_units';
import {Uint64} from 'neuroglancer/util/uint64';
import * as vector from 'neuroglancer/util/vector';
import {makeCopyButton} from 'neuroglancer/widget/copy_button';
import {makeDeleteButton} from 'neuroglancer/widget/delete_button';
import {DependentViewContext, DependentViewWidget} from 'neuroglancer/widget/dependent_view_widget';
import {makeIcon} from 'neuroglancer/widget/icon';
import {makeMoveToButton} from 'neuroglancer/widget/move_to_button';
import {makeIcon} from 'neuroglancer/widget/icon';
import {makeMoveToButton} from 'neuroglancer/widget/move_to_button';
import {Tab} from 'neuroglancer/widget/tab_view';

interface AnnotationIdAndPart {
  id: string, sourceIndex: number;
  states: Borrowed<AnnotationLayerState>[] = [];
  relationships: string[] = [];
  private loadingCount = 0;

  get value() {
    return this.states;
  }

  get isLoading() {
    return this.loadingCount !== 0;
  }

  markLoading() {
    this.loadingCount++;
    return () => {
      if (--this.loadingCount === 0) {
        this.isLoadingChanged.dispatch();
      }
    };
  }

  private sort() {
    this.states.sort((a, b) => {
      let d = a.sourceIndex - b.sourceIndex;
      if (d !== 0) return d;
      return a.subsourceIndex - b.subsourceIndex;
    });
  }

  private updateRelationships() {
    const newRelationships = new Set<string>();
    for (const state of this.states) {
      for (const relationship of state.source.relationships) {
        newRelationships.add(relationship);
      }
    }
    this.relationships = Array.from(newRelationships);
  }

  add(state: Borrowed<AnnotationLayerState>) {
    this.states.push(state);
    this.sort();
    this.updateRelationships();
    this.changed.dispatch();
    return () => {
      const index = this.states.indexOf(state);
      this.states.splice(index, 1);
      this.updateRelationships();
      this.changed.dispatch();
    };
  }
}

function getCenterPosition(center: Float32Array, annotation: Annotation) {
  switch (annotation.type) {
    case AnnotationType.AXIS_ALIGNED_BOUNDING_BOX:
    case AnnotationType.LINE:
      vector.add(center, annotation.pointA, annotation.pointB);
      vector.scale(center, center, 0.5);
      break;
    case AnnotationType.POINT:
      center.set(annotation.point);
      break;
    case AnnotationType.ELLIPSOID:
      center.set(annotation.center);
      break;
  }
}


function setLayerPosition(
    layer: UserLayer, chunkTransform: ValueOrError<ChunkTransformParameters>,
    layerPosition: Float32Array) {
  if (chunkTransform.error !== undefined) return;
  layer.setLayerPosition(chunkTransform.modelTransform, layerPosition);
}


function visitTransformedAnnotationGeometry(
    annotation: Annotation, chunkTransform: ChunkTransformParameters,
    callback: (layerPosition: Float32Array, isVector: boolean) => void) {
  const {layerRank} = chunkTransform;
  const paddedChunkPosition = new Float32Array(layerRank);
  annotationTypeHandlers[annotation.type].visitGeometry(annotation, (chunkPosition, isVector) => {
    // Rank of "chunk" coordinate space may be less than rank of layer space if the annotations are
    // embedded in a higher-dimensional space.  The extra embedding dimensions always are last and
    // have a coordinate of 0.
    paddedChunkPosition.set(chunkPosition);
    const layerPosition = new Float32Array(layerRank);
    (isVector ? matrix.transformVector : matrix.transformPoint)(
        layerPosition, chunkTransform.chunkToLayerTransform, layerRank + 1, paddedChunkPosition,
        layerRank);
    callback(layerPosition, isVector);
  });
}

interface AnnotationLayerViewAttachedState {
  refCounted: RefCounted;
  annotations: Annotation[];
  idToIndex: Map<AnnotationId, number>;
  listOffset: number;
}

export class AnnotationLayerView extends Tab {
  private previousSelectedState:
      {annotationId: string, annotationLayerState: AnnotationLayerState, pin: boolean}|undefined =
          undefined;
  private previousHoverId: string|undefined = undefined;
  private previousHoverAnnotationLayerState: AnnotationLayerState|undefined = undefined;

  private virtualListSource: VirtualListSource = {
    length: 0,
    render: (index: number) => this.render(index),
    changed: new Signal<(splices: ArraySpliceOp[]) => void>(),
  };
  private virtualList = new VirtualList({source: this.virtualListSource});
  private listElements: {state: AnnotationLayerState, annotation: Annotation}[] = [];
  private updated = false;
  private mutableControls = document.createElement('div');
  private headerRow = document.createElement('div');

  get annotationStates() {
    return this.layer.annotationStates;
  }

  private attachedAnnotationStates =
      new Map<AnnotationLayerState, AnnotationLayerViewAttachedState>();

  private updateAttachedAnnotationLayerStates() {
    const states = this.annotationStates.states;
    const {attachedAnnotationStates} = this;
    const newAttachedAnnotationStates =
        new Map<AnnotationLayerState, AnnotationLayerViewAttachedState>();
    for (const [state, info] of attachedAnnotationStates) {
      if (!states.includes(state)) {
        attachedAnnotationStates.delete(state);
        info.refCounted.dispose();
      }
    }
    for (const state of states) {
      const info = attachedAnnotationStates.get(state);
      if (info !== undefined) {
        newAttachedAnnotationStates.set(state, info);
        continue;
      }
      const source = state.source;
      const refCounted = new RefCounted();
      if (source instanceof AnnotationSource) {
        refCounted.registerDisposer(
            source.childAdded.add((annotation) => this.addAnnotationElement(annotation, state)));
        refCounted.registerDisposer(source.childUpdated.add(
            (annotation) => this.updateAnnotationElement(annotation, state)));
        refCounted.registerDisposer(source.childDeleted.add(
            (annotationId) => this.deleteAnnotationElement(annotationId, state)));
      }
      refCounted.registerDisposer(state.transform.changed.add(this.forceUpdateView));
      newAttachedAnnotationStates.set(
          state, {refCounted, annotations: [], idToIndex: new Map(), listOffset: 0});
    }
    this.attachedAnnotationStates = newAttachedAnnotationStates;
    attachedAnnotationStates.clear();
    this.updateCoordinateSpace();
    this.forceUpdateView();
  }

  private forceUpdateView = () => {
    this.updated = false;
    this.updateView();
  };

  private globalDimensionIndices: number[] = [];
  private localDimensionIndices: number[] = [];
  private curCoordinateSpaceGeneration = -1;
  private prevCoordinateSpaceGeneration = -1;
  private columnWidths: number[] = [];
  private gridTemplate: string = '';

  private updateCoordinateSpace() {
    const localCoordinateSpace = this.layer.localCoordinateSpace.value;
    const globalCoordinateSpace = this.layer.manager.root.coordinateSpace.value;
    const globalDimensionIndices: number[] = [];
    const localDimensionIndices: number[] = [];
    for (let globalDim = 0, globalRank = globalCoordinateSpace.rank; globalDim < globalRank;
         ++globalDim) {
      if (this.annotationStates.states.some(state => {
            const transform = state.transform.value;
            if (transform.error !== undefined) return false;
            return transform.globalToRenderLayerDimensions[globalDim] !== -1;
          })) {
        globalDimensionIndices.push(globalDim);
      }
    }
    for (let localDim = 0, localRank = localCoordinateSpace.rank; localDim < localRank;
         ++localDim) {
      if (this.annotationStates.states.some(state => {
            const transform = state.transform.value;
            if (transform.error !== undefined) return false;
            return transform.localToRenderLayerDimensions[localDim] !== -1;
          })) {
        localDimensionIndices.push(localDim);
      }
    }
    if (!arraysEqual(globalDimensionIndices, this.globalDimensionIndices) ||
        !arraysEqual(localDimensionIndices, this.localDimensionIndices)) {
      this.localDimensionIndices = localDimensionIndices;
      this.globalDimensionIndices = globalDimensionIndices;
      ++this.curCoordinateSpaceGeneration;
    }
  }

  constructor(
      public layer: Borrowed<UserLayerWithAnnotations>,
      public displayState: AnnotationDisplayState) {
    super();
    this.element.classList.add('neuroglancer-annotation-layer-view');
    this.registerDisposer(this.visibility.changed.add(() => this.updateView()));
    this.registerDisposer(
        this.annotationStates.changed.add(() => this.updateAttachedAnnotationLayerStates()));
    this.headerRow.classList.add('neuroglancer-annotation-list-header');

    const toolbox = document.createElement('div');
    toolbox.className = 'neuroglancer-annotation-toolbox';

    layer.initializeAnnotationLayerViewTab(this);
    const colorPicker = this.registerDisposer(new ColorWidget(this.displayState.color));
    colorPicker.element.title = 'Change annotation display color';
    this.registerDisposer(new ElementVisibilityFromTrackableBoolean(
        makeCachedLazyDerivedWatchableValue(
            shader => shader.match(/\bdefaultColor\b/) !== null,
            displayState.shaderControls.processedFragmentMain),
        colorPicker.element));
    toolbox.appendChild(colorPicker.element);
    const {mutableControls} = this;
    const pointButton = makeIcon({
      text: annotationTypeHandlers[AnnotationType.POINT].icon,
      title: 'Annotate point',
      onClick: () => {
        this.layer.tool.value = new PlacePointTool(this.layer, {});
      },
    });
    mutableControls.appendChild(pointButton);

    const boundingBoxButton = makeIcon({
      text: annotationTypeHandlers[AnnotationType.AXIS_ALIGNED_BOUNDING_BOX].icon,
      title: 'Annotate bounding box',
      onClick: () => {
        this.layer.tool.value = new PlaceBoundingBoxTool(this.layer, {});
      },
    });
    mutableControls.appendChild(boundingBoxButton);

    const lineButton = makeIcon({
      text: annotationTypeHandlers[AnnotationType.LINE].icon,
      title: 'Annotate line',
      onClick: () => {
        this.layer.tool.value = new PlaceLineTool(this.layer, {});
      },
    });
    mutableControls.appendChild(lineButton);

    const ellipsoidButton = makeIcon({
      text: annotationTypeHandlers[AnnotationType.ELLIPSOID].icon,
      title: 'Annotate ellipsoid',
      onClick: () => {
        this.layer.tool.value = new PlaceEllipsoidTool(this.layer, {});
      },
    });
    mutableControls.appendChild(ellipsoidButton);
    toolbox.appendChild(mutableControls);
    this.element.appendChild(toolbox);

    this.element.appendChild(this.headerRow);
    const {virtualList} = this;
    virtualList.element.classList.add('neuroglancer-annotation-list');
    this.element.appendChild(virtualList.element);
    this.virtualList.element.addEventListener('mouseleave', () => {
      this.displayState.hoverState.value = undefined;
    });

    const bindings = getDefaultAnnotationListBindings();
    this.registerDisposer(new MouseEventBinder(this.virtualList.element, bindings));
    this.virtualList.element.title = bindings.describe();
    this.registerDisposer(this.displayState.hoverState.changed.add(() => this.updateHoverView()));
    this.registerDisposer(
        this.selectedAnnotationState.changed.add(() => this.updateSelectionView()));
    this.registerDisposer(this.layer.localCoordinateSpace.changed.add(() => {
      this.updateCoordinateSpace();
      this.updateView();
    }));
    this.registerDisposer(this.layer.manager.root.coordinateSpace.changed.add(() => {
      this.updateCoordinateSpace();
      this.updateView();
    }));
    this.updateCoordinateSpace();
    this.updateAttachedAnnotationLayerStates();
    this.updateSelectionView();
  }

  private getRenderedAnnotationListElement(
      state: AnnotationLayerState, id: AnnotationId, scrollIntoView: boolean = false): HTMLElement
      |undefined {
    const attached = this.attachedAnnotationStates.get(state);
    if (attached == undefined) return undefined;
    const index = attached.idToIndex.get(id);
    if (index === undefined) return undefined;
    const listIndex = attached.listOffset + index;
    if (scrollIntoView) {
      this.virtualList.scrollItemIntoView(index)
    }
    return this.virtualList.getItemElement(listIndex);
  }

  private clearSelectionClass() {
    const {previousSelectedState: state} = this;
    if (state === undefined) return;
    this.previousSelectedState = undefined;
    const element =
        this.getRenderedAnnotationListElement(state.annotationLayerState, state.annotationId);

interface AnnotationLayerViewAttachedState {
  refCounted: RefCounted;
  listElements: Map<string, HTMLElement>;
  sublistContainer: HTMLElement;
}

export class AnnotationLayerView extends Tab {
  private previousSelectedId: string|undefined = undefined;
  private previousSelectedAnnotationLayerState: AnnotationLayerState|undefined = undefined;
  private previousHoverId: string|undefined = undefined;
  private previousHoverAnnotationLayerState: AnnotationLayerState|undefined = undefined;

  private listContainer = document.createElement('div');
  private updated = false;
  private mutableControls = document.createElement('div');
  private headerRow = document.createElement('div');
    const annotationLayerState = this.annotationStates.states.find(
        x => x.sourceIndex === layerSelectionState.annotationSourceIndex &&
            (layerSelectionState.annotationSubsource === undefined ||
             x.subsourceId === layerSelectionState.annotationSubsource));
    if (annotationLayerState === undefined) return undefined;
    return {annotationId, annotationLayerState, pin};
  }, this.layer.manager.root.selectionState, this.layer.manager.root.selectionState.pin);

  private updateSelectionView() {
    const selectionState = this.selectedAnnotationState.value;
    const {previousSelectedState} = this;
    if (previousSelectedState === selectionState ||
        (previousSelectedState !== undefined && selectionState !== undefined &&
    for (const [state, info] of attachedAnnotationStates) {
      if (!states.includes(state)) {
        attachedAnnotationStates.delete(state);
        info.listElements.clear();
        info.refCounted.dispose();
      }
    }
    this.previousSelectedState = selectionState;
    if (selectionState === undefined) return;
    const element = this.getRenderedAnnotationListElement(
        selectionState.annotationLayerState, selectionState.annotationId,
        /*scrollIntoView=*/selectionState.pin);
    if (element !== undefined) {
      element.classList.add('neuroglancer-annotation-selected');
    }
  }

  private updateHoverView() {
    const selectedValue = this.displayState.hoverState.value;
    let newHoverId: string|undefined;
    let newAnnotationLayerState: AnnotationLayerState|undefined;
            (annotationId) => this.deleteAnnotationElement(annotationId, state)));
      }
      refCounted.registerDisposer(state.transform.changed.add(this.forceUpdateView));
      const sublistContainer = document.createElement('div');
      sublistContainer.classList.add('neuroglancer-annotation-sublist');
      newAttachedAnnotationStates.set(
          state, {refCounted, listElements: new Map(), sublistContainer});
    }
    this.attachedAnnotationStates = newAttachedAnnotationStates;
    attachedAnnotationStates.clear();
    }
    this.clearHoverClass();
    this.previousHoverId = newHoverId;
    this.previousHoverAnnotationLayerState = newAnnotationLayerState;
    if (newHoverId === undefined) return;
    const element = this.getRenderedAnnotationListElement(newAnnotationLayerState!, newHoverId);
    if (element === undefined) return;
    element.classList.add('neuroglancer-annotation-hover');
  }

  private localDimensionIndices: number[] = [];
  private curCoordinateSpaceGeneration = -1;
  private prevCoordinateSpaceGeneration = -1;

  private updateCoordinateSpace() {
    const localCoordinateSpace = this.layer.localCoordinateSpace.value;
    const {columnWidths} = this;
    if (columnWidths[column] > width) {
      // False if `columnWidths[column] === undefined`.
      return;
    }
    columnWidths[column] = width;
    this.element.style.setProperty(`--neuroglancer-column-${column}-width`, `${width}ch`);
  }

  private updateView() {
    if (!this.visible) {
      return;
    }
    if (this.curCoordinateSpaceGeneration !== this.prevCoordinateSpaceGeneration) {
      this.updated = false;
      const {columnWidths} = this;
      columnWidths.length = 0;
      const {headerRow} = this;
      const symbolPlaceholder = document.createElement('div');
      symbolPlaceholder.style.gridColumn = `symbol`;

      const deletePlaceholder = document.createElement('div');
      deletePlaceholder.style.gridColumn = `delete`;

      removeChildren(headerRow);
      headerRow.appendChild(symbolPlaceholder);
      let i = 0;
      let gridTemplate = '[symbol] 2ch';
      const addDimension = (coordinateSpace: CoordinateSpace, dimIndex: number) => {
        const dimWidget = document.createElement('div');
        dimWidget.classList.add('neuroglancer-annotations-view-dimension');
        const name = document.createElement('span');
        name.classList.add('neuroglancer-annotations-view-dimension-name');
      public state: Owned<SelectedAnnotationState>, public displayState: AnnotationDisplayState) {
    super();
    this.element.classList.add('neuroglancer-annotation-layer-view');
    this.listContainer.classList.add('neuroglancer-annotation-list');
    this.registerDisposer(state);
    this.registerDisposer(this.visibility.changed.add(() => this.updateView()));
    this.registerDisposer(
        dimWidget.appendChild(scale);
        dimWidget.style.gridColumn = `dim ${i + 1}`;
        this.setColumnWidth(i, scale.textContent.length + name.textContent.length + 3);
        gridTemplate += ` [dim] var(--neuroglancer-column-${i}-width)`;
        ++i;
        headerRow.appendChild(dimWidget);
      };
      const globalCoordinateSpace = this.layer.manager.root.coordinateSpace.value;
      for (const globalDim of this.globalDimensionIndices) {
        addDimension(globalCoordinateSpace, globalDim);
      }
      const localCoordinateSpace = this.layer.localCoordinateSpace.value;
      for (const localDim of this.localDimensionIndices) {
        addDimension(localCoordinateSpace, localDim);
      }
      headerRow.appendChild(deletePlaceholder);
      gridTemplate += ` [delete] 2ch`;
      this.gridTemplate = gridTemplate;
      headerRow.style.gridTemplateColumns = gridTemplate;
      this.prevCoordinateSpaceGeneration = this.curCoordinateSpaceGeneration;
    }
    if (this.updated) {
      return;
    }

    let isMutable = false;
    const {listElements} = this;
    listElements.length = 0;
    for (const [state, info] of this.attachedAnnotationStates) {
      if (!state.source.readonly) isMutable = true;
      if (state.chunkTransform.value.error !== undefined) continue;
      const {source} = state;
      const annotations = Array.from(source);
      info.annotations = annotations;
      const {idToIndex} = info;
      idToIndex.clear();
      for (let i = 0, length = annotations.length; i < length; ++i) {
        idToIndex.set(annotations[i].id, i);
      }
      for (const annotation of annotations) {
        listElements.push({state, annotation});
      }
    }
    const oldLength = this.virtualListSource.length;
    this.updateListLength();
    this.virtualListSource.changed!.dispatch(
        [{retainCount: 0, deleteCount: oldLength, insertCount: listElements.length}]);
    this.mutableControls.style.display = isMutable ? 'contents' : 'none';
    this.resetOnUpdate();
  }

    toolbox.appendChild(mutableControls);
    this.element.appendChild(toolbox);

    this.element.appendChild(this.listContainer);
    this.listContainer.addEventListener('mouseleave', () => {
      this.displayState.hoverState.value = undefined;
    });

    this.registerDisposer(new MouseEventBinder(this.listContainer, getDefaultSelectBindings()));
    this.registerDisposer(this.displayState.hoverState.changed.add(() => this.updateHoverView()));
    this.registerDisposer(this.state.changed.add(() => this.updateSelectionView()));
    this.registerDisposer(this.layer.localCoordinateSpace.changed.add(() => {
      this.updateCoordinateSpace();
      this.updateView();
    if (info !== undefined) {
      const index = info.annotations.length;
      info.annotations.push(annotation);
      info.idToIndex.set(annotation.id, index);
    }));
    this.updateCoordinateSpace();
    this.updateAttachedAnnotationLayerStates();
  }

  private clearSelectionClass() {
    const {previousSelectedAnnotationLayerState, previousSelectedId} = this;
    if (previousSelectedAnnotationLayerState !== undefined) {
      this.previousSelectedAnnotationLayerState = undefined;
      this.previousSelectedId = undefined;
      const attached = this.attachedAnnotationStates.get(previousSelectedAnnotationLayerState);
      if (attached === undefined) return;
      const element = attached.listElements.get(previousSelectedId!);
      if (element !== undefined) {
        element.classList.remove('neuroglancer-annotation-selected');
      }
    }
  }


  private deleteAnnotationElement(annotationId: string, state: AnnotationLayerState) {
    if (previousHoverAnnotationLayerState !== undefined) {
      this.previousHoverAnnotationLayerState = undefined;
      this.previousHoverId = undefined;
      const attached = this.attachedAnnotationStates.get(previousHoverAnnotationLayerState);
      if (attached === undefined) return;
      const element = attached.listElements.get(previousHoverId!);
      if (element !== undefined) {
        element.classList.remove('neuroglancer-annotation-hover');
      }
    }
  }

  private updateSelectionView() {
    const selectedValue = this.state.value;
    let newSelectedId: string|undefined;
    let newSelectedAnnotationLayerState: AnnotationLayerState|undefined;
    if (selectedValue !== undefined) {
      newSelectedId = selectedValue.id;
      newSelectedAnnotationLayerState = this.state.selectedAnnotationLayer;
    }
    const {previousSelectedId, previousSelectedAnnotationLayerState} = this;
    if (newSelectedId === previousSelectedId &&
        previousSelectedAnnotationLayerState === newSelectedAnnotationLayerState) {
      return;
    }
    this.clearSelectionClass();
    this.previousSelectedId = newSelectedId;
    this.previousSelectedAnnotationLayerState = newSelectedAnnotationLayerState;
    if (newSelectedId === undefined) return;
    const attached = this.attachedAnnotationStates.get(newSelectedAnnotationLayerState!);
    if (attached === undefined) return;
    const element = attached.listElements.get(newSelectedId);
    if (element === undefined) return;
    element.classList.add('neuroglancer-annotation-selected');
    element.scrollIntoView();
  }

  private updateHoverView() {
    let deleteButton: HTMLElement|undefined;

    const maybeAddDeleteButton = () => {
      if (state.source.readonly) return;
      if (deleteButton !== undefined) return;
      deleteButton = makeDeleteButton({
        title: 'Delete annotation',
        onClick: event => {
          event.stopPropagation();
          event.preventDefault();
          const ref = state.source.getReference(annotation.id);
          try {
            state.source.delete(ref);
    this.previousHoverId = newHoverId;
    this.previousHoverAnnotationLayerState = newAnnotationLayerState;
    if (newHoverId === undefined) return;
    const attached = this.attachedAnnotationStates.get(newAnnotationLayerState!);
    if (attached === undefined) return;
    const element = attached.listElements.get(newHoverId);
    if (element === undefined) return;
    element.classList.add('neuroglancer-annotation-hover');
  }

  private updateView() {
    if (!this.visible) {
      return;
    }
    if (this.curCoordinateSpaceGeneration !== this.prevCoordinateSpaceGeneration) {
      this.updated = false;
      const {headerRow} = this;
      const symbolPlaceholder = document.createElement('div');
      symbolPlaceholder.style.gridColumn = `symbol`;
      addDims(
          this.localDimensionIndices, chunkTransform.modelTransform.localToRenderLayerDimensions);
      maybeAddDeleteButton();
    });
      removeChildren(headerRow);
      headerRow.appendChild(symbolPlaceholder);
      let i = 0;
      const addDimension = (coordinateSpace: CoordinateSpace, dimIndex: number) => {
        const dimWidget = document.createElement('div');
        dimWidget.classList.add('neuroglancer-annotations-view-dimension');
    icon.style.gridRow = `span ${numRows}`;
    if (deleteButton !== undefined) {
      deleteButton.style.gridRow = `span ${numRows}`;
    }
    element.addEventListener('mouseenter', () => {
      this.displayState.hoverState.value = {
        id: annotation.id,
        dimWidget.appendChild(name);
        dimWidget.appendChild(scale);
        dimWidget.style.gridColumn = `dim ${i + 1}`;
        ++i;
        headerRow.appendChild(dimWidget);
      };
    });

    element.addEventListener('action:pin-annotation', event => {
      event.stopPropagation();
      this.layer.selectAnnotation(state, annotation.id, true);
    });
        addDimension(localCoordinateSpace, localDim);
      }
      headerRow.appendChild(deletePlaceholder);
      this.listContainer.style.gridTemplateColumns =
          `[symbol] min-content repeat(${i}, [dim] min-content) [delete] min-content`;
      this.prevCoordinateSpaceGeneration = this.curCoordinateSpaceGeneration;
    }
    if (this.updated) {
          layerPosition, chunkTransform.chunkToLayerTransform, layerRank + 1, chunkPosition,
    }

    let isMutable = false;
    const self = this;
    function* sublistContainers() {
      yield self.headerRow;
      for (const [state, {sublistContainer, listElements}] of self.attachedAnnotationStates) {
        if (!state.source.readonly) isMutable = true;
        removeChildren(sublistContainer);
        listElements.clear();
        if (state.chunkTransform.value.error !== undefined) continue;
        for (const annotation of state.source) {
          sublistContainer.appendChild(self.makeAnnotationListElement(annotation, state));
        }
        yield sublistContainer;
      }
    }
    updateChildren(this.listContainer, sublistContainers());
    this.mutableControls.style.display = isMutable ? 'contents' : 'none';
    this.resetOnUpdate();
  }

  private addAnnotationElement(annotation: Annotation, state: AnnotationLayerState) {
    if (!this.visible) {
      this.updated = false;

abstract class PlaceAnnotationTool extends LegacyTool {
  layer: UserLayerWithAnnotations;
  constructor(layer: UserLayerWithAnnotations, options: any) {
    super(layer);
    }
    const info = this.attachedAnnotationStates.get(state);
    if (info !== undefined) {
      info.sublistContainer.appendChild(this.makeAnnotationListElement(annotation, state));
    }
    this.resetOnUpdate();
  }
const ANNOTATE_ELLIPSOID_TOOL_ID = 'annotateSphere';

export class PlacePointTool extends PlaceAnnotationTool {
  trigger(mouseState: MouseSelectionState) {
    const {annotationLayer} = this;
    if (annotationLayer === undefined) {
      // Not yet ready.
      return;
    }
    }
    const info = this.attachedAnnotationStates.get(state);
    if (info !== undefined) {
      const {listElements} = info;
      const element = listElements.get(annotation.id);
      if (element !== undefined) {
        const newElement = this.makeAnnotationListElement(annotation, state);
        info.sublistContainer.replaceChild(newElement, element);
        listElements.set(annotation.id, newElement);
      }
    }
    this.resetOnUpdate();
      reference.dispose();
    }
  }

  get description() {
    return `annotate point`;
  }

      this.updateView();
      return;
    }
    const attached = this.attachedAnnotationStates.get(state);
    if (attached !== undefined) {
      let element = attached.listElements.get(annotationId);
      if (element !== undefined) {
        removeFromParent(element);
        attached.listElements.delete(annotationId);
      }
    }
    this.resetOnUpdate();
      undefined;

  abstract getInitialAnnotation(
      mouseState: MouseSelectionState, annotationLayer: AnnotationLayerState): Annotation;
  abstract getUpdatedAnnotation(
      oldAnnotation: Annotation, mouseState: MouseSelectionState,
      annotationLayer: AnnotationLayerState): Annotation;

  trigger(mouseState: MouseSelectionState) {
    const {annotationLayer} = this;
    if (annotationLayer === undefined) {
    const chunkTransform = state.chunkTransform.value as ChunkTransformParameters;
    const element = document.createElement('div');
    element.classList.add('neuroglancer-annotation-list-entry');
    element.title = 'Click to select, right click to recenter view.';

    const icon = document.createElement('div');
    icon.className = 'neuroglancer-annotation-icon';
    icon.textContent = annotationTypeHandlers[annotation.type].icon;
    icon.classList.add('neuroglancer-annotation-list-entry-highlight');
    element.appendChild(icon);

    let deleteButton: HTMLElement|undefined;
        state.annotationLayer.source.update(reference, newAnnotation);
        this.layer.selectAnnotation(annotationLayer, reference.id, true);
      };

      if (this.inProgressAnnotation === undefined) {
        const reference = annotationLayer.source.add(
            this.getInitialAnnotation(mouseState, annotationLayer), /*commit=*/false);
        this.layer.selectAnnotation(annotationLayer, reference.id, true);
        const mouseDisposer = mouseState.changed.add(updatePointB);
        const disposer = () => {
          mouseDisposer();
          reference.dispose();
        };
        this.inProgressAnnotation = {
          annotationLayer,
          reference,
          disposer,
        };
      } else {
        updatePointB();
        this.inProgressAnnotation.annotationLayer.source.commit(
            this.inProgressAnnotation.reference);
        this.inProgressAnnotation.disposer();
        this.inProgressAnnotation = undefined;
      }
    }
  }

  disposed() {
    this.deactivate();
    super.disposed();
              if (layerDim !== -1) {
                const coord = Math.floor(layerPosition[layerDim]);
                const coordElement = document.createElement('div');
                coordElement.textContent = coord.toString();
                coordElement.classList.add('neuroglancer-annotation-coordinate');
                coordElement.classList.add('neuroglancer-annotation-list-entry-highlight');
                coordElement.style.gridColumn = `dim ${i + 1}`;
                position.appendChild(coordElement);
              }
              ++i;

abstract class PlaceTwoCornerAnnotationTool extends TwoStepAnnotationTool {
  annotationType: AnnotationType.LINE|AnnotationType.AXIS_ALIGNED_BOUNDING_BOX;

  getInitialAnnotation(mouseState: MouseSelectionState, annotationLayer: AnnotationLayerState):
      Annotation {
    const point = getMousePositionInAnnotationCoordinates(mouseState, annotationLayer);
    return <AxisAlignedBoundingBox|Line>{
      id: '',
      ++numRows;
      const description = document.createElement('div');
      description.classList.add('neuroglancer-annotation-description');
      description.classList.add('neuroglancer-annotation-list-entry-highlight');
      description.textContent = annotation.description;
      element.appendChild(description);
    }
  }
    if (deleteButton !== undefined) {
      deleteButton.style.gridRow = `span ${numRows}`;
    }


    const info = this.attachedAnnotationStates.get(state)!;
    info.listElements.set(annotation.id, element);
    element.addEventListener('mouseenter', () => {
      this.displayState.hoverState.value = {
        id: annotation.id,
    return {...oldAnnotation, pointB: point};
  }
}

export class PlaceBoundingBoxTool extends PlaceTwoCornerAnnotationTool {
  get description() {
    return `annotate bounding box`;
  }

  getUpdatedAnnotation(
      oldAnnotation: AxisAlignedBoundingBox, mouseState: MouseSelectionState,
      annotationLayer: AnnotationLayerState) {
    const result = super.getUpdatedAnnotation(oldAnnotation, mouseState, annotationLayer) as
        AxisAlignedBoundingBox;
    const {pointA, pointB} = result;
    const rank = pointA.length;
    for (let i = 0; i < rank; ++i) {
      if (pointA[i] === pointB[i]) {
        pointB[i] += 1;
        setLayerPosition(this.layer, chunkTransform, layerPosition);
      }
    });

    return element;
  }
}
export class PlaceLineTool extends PlaceTwoCornerAnnotationTool {
  get description() {
    return `annotate line`;
  }

  private initialRelationships: Uint64[][]|undefined;

  getInitialAnnotation(mouseState: MouseSelectionState, annotationLayer: AnnotationLayerState):
      Annotation {
    const result = super.getInitialAnnotation(mouseState, annotationLayer);
    this.initialRelationships = result.relatedSegments =
        getSelectedAssociatedSegments(annotationLayer);
    return result;
  }

  getUpdatedAnnotation(
      oldAnnotation: Line|AxisAlignedBoundingBox, mouseState: MouseSelectionState,
      annotationLayer: AnnotationLayerState) {
    const result = super.getUpdatedAnnotation(oldAnnotation, mouseState, annotationLayer);
    const initialRelationships = this.initialRelationships;
    const newRelationships = getSelectedAssociatedSegments(annotationLayer);
    if (initialRelationships === undefined) {
      result.relatedSegments = newRelationships;
    } else {
      result.relatedSegments = Array.from(newRelationships, (newSegments, i) => {
        const initialSegments = initialRelationships[i];
        newSegments =
            newSegments.filter(x => initialSegments.findIndex(y => Uint64.equal(x, y)) === -1);
        return [...initialSegments, ...newSegments];
      });
    }
    return result;
  }

  toJSON() {
    return ANNOTATE_LINE_TOOL_ID;
  }
}
PlaceLineTool.prototype.annotationType = AnnotationType.LINE;

class PlaceEllipsoidTool extends TwoStepAnnotationTool {
  getInitialAnnotation(mouseState: MouseSelectionState, annotationLayer: AnnotationLayerState):
      Annotation {
    const point = getMousePositionInAnnotationCoordinates(mouseState, annotationLayer);

    return <Ellipsoid>{
      type: AnnotationType.ELLIPSOID,
      id: '',
      description: '',
      segments: getSelectedAssociatedSegments(annotationLayer),
      center: point,
      radii: vec3.fromValues(0, 0, 0),
      properties: annotationLayer.source.properties.map(x => x.default),
    };
  }

  getUpdatedAnnotation(
      oldAnnotation: Ellipsoid, mouseState: MouseSelectionState,
      annotationLayer: AnnotationLayerState) {
    const radii = getMousePositionInAnnotationCoordinates(mouseState, annotationLayer);
    if (radii === undefined) return oldAnnotation;
    const center = oldAnnotation.center;
    const rank = center.length;
    for (let i = 0; i < rank; ++i) {
      radii[i] = Math.abs(center[i] - radii[i]);
    }
    return <Ellipsoid>{
      ...oldAnnotation,
      radii,
    };
  }
  get description() {
    return `annotate ellipsoid`;
  }

  toJSON() {
    return ANNOTATE_ELLIPSOID_TOOL_ID;
  }
}

registerLegacyTool(
    ANNOTATE_POINT_TOOL_ID,
    (layer, options) => new PlacePointTool(<UserLayerWithAnnotations>layer, options));
registerLegacyTool(
    ANNOTATE_BOUNDING_BOX_TOOL_ID,
    (layer, options) => new PlaceBoundingBoxTool(<UserLayerWithAnnotations>layer, options));
registerLegacyTool(
    ANNOTATE_LINE_TOOL_ID,
    (layer, options) => new PlaceLineTool(<UserLayerWithAnnotations>layer, options));
registerLegacyTool(
    ANNOTATE_ELLIPSOID_TOOL_ID,
    (layer, options) => new PlaceEllipsoidTool(<UserLayerWithAnnotations>layer, options));

const newRelatedSegmentKeyMap = EventActionMap.fromObject({
  'enter': {action: 'commit'},
  'escape': {action: 'cancel'},
});

function makeRelatedSegmentList(
    listName: string, segments: Uint64[],
    segmentationDisplayState: WatchableValueInterface<SegmentationDisplayState|null|undefined>,
    mutate?: ((newSegments: Uint64[]) => void)|undefined) {
  return new DependentViewWidget(
      segmentationDisplayState, (segmentationDisplayState, parent, context) => {
        const listElement = document.createElement('div');
        listElement.classList.add('neuroglancer-related-segment-list');
        if (segmentationDisplayState != null) {
          context.registerDisposer(bindSegmentListWidth(segmentationDisplayState, listElement));
        }
        const headerRow = document.createElement('div');
        headerRow.classList.add('neuroglancer-related-segment-list-header');
        const copyButton = makeCopyButton({
          title: `Copy segment IDs`,
          onClick: () => {
            setClipboard(segments.map(x => x.toString()).join(', '));
          },
        });
        headerRow.appendChild(copyButton);
        let headerCheckbox: HTMLInputElement|undefined;
        if (segmentationDisplayState != null) {
          headerCheckbox = document.createElement('input');
          headerCheckbox.type = 'checkbox';
          headerCheckbox.addEventListener('change', () => {
            const {visibleSegments} = segmentationDisplayState.segmentationGroupState.value;
            const add = segments.some(id => !visibleSegments.has(id));
            for (const id of segments) {
              visibleSegments.set(id, add);
            }
          });
          headerRow.appendChild(headerCheckbox);
        }
        if (mutate !== undefined) {
          const deleteButton = makeDeleteButton({
            title: 'Remove all IDs',
            onClick: () => {
              mutate([]);
            },
          });
          headerRow.appendChild(deleteButton);
        }
        const titleElement = document.createElement('span');
        titleElement.classList.add('neuroglancer-related-segment-list-title');
        titleElement.textContent = listName;
        headerRow.appendChild(titleElement);
        if (mutate !== undefined) {
          const addButton = makeAddButton({
            title: 'Add related segment ID',
            onClick: () => {
              const addContext = new RefCounted();
              const addContextDisposer = context.registerDisposer(disposableOnce(addContext));
              const newRow = document.createElement('div');
              newRow.classList.add('neuroglancer-segment-list-entry');
              newRow.classList.add('neuroglancer-segment-list-entry-new');
              const copyButton = makeCopyButton({});
              copyButton.classList.add('neuroglancer-segment-list-entry-copy');
              newRow.appendChild(copyButton);
              if (segmentationDisplayState != null) {
                const checkbox = document.createElement('input');
                checkbox.classList.add('neuroglancer-segment-list-entry-visible-checkbox');
                checkbox.type = 'checkbox';
                newRow.appendChild(checkbox);
              }
              const deleteButton = makeDeleteButton({
                title: 'Cancel adding new segment ID',
                onClick: () => {
                  addContextDisposer();
                },
              });
              deleteButton.classList.add('neuroglancer-segment-list-entry-delete');
              newRow.appendChild(deleteButton);
              const idElement = document.createElement('input');
              idElement.autocomplete = 'off';
              idElement.spellcheck = false;
              idElement.classList.add('neuroglancer-segment-list-entry-id');
              const keyboardEventBinder = addContext.registerDisposer(
                  new KeyboardEventBinder(idElement, newRelatedSegmentKeyMap));
              keyboardEventBinder.allShortcutsAreGlobal = true;
              const validateInput = () => {
                const id = new Uint64();
                if (id.tryParseString(idElement.value)) {
                  idElement.dataset.valid = 'true';
                  return id;
                } else {
                  idElement.dataset.valid = 'false';
                  return undefined;
                }
              };
              validateInput();
              idElement.addEventListener('input', () => {
                validateInput();
              });
              idElement.addEventListener('blur', () => {
                const id = validateInput();
                if (id !== undefined) {
                  mutate([...segments, id]);
                }
                addContextDisposer();
              });
              registerActionListener(idElement, 'cancel', addContextDisposer);
              registerActionListener(idElement, 'commit', () => {
                const id = validateInput();
                if (id !== undefined) {
                  mutate([...segments, id]);
                }
                addContextDisposer();
              });
              newRow.appendChild(idElement);
              listElement.appendChild(newRow);
              idElement.focus();
              addContext.registerDisposer(() => {
                idElement.value = '';
                newRow.remove();
              });
            },
          });
          headerRow.appendChild(addButton);
        }

        listElement.appendChild(headerRow);

        const rows: HTMLElement[] = [];
        const segmentWidgetFactory = SegmentWidgetFactory.make(
            segmentationDisplayState ?? undefined, /*includeMapped=*/ false);
        for (const id of segments) {
          const row = segmentWidgetFactory.get(id);
          rows.push(row);
          if (mutate !== undefined) {
            const deleteButton = makeDeleteButton({
              title: 'Remove ID',
              onClick: event => {
                mutate(segments.filter(x => !Uint64.equal(x, id)));
                event.stopPropagation();
              },
            });
            deleteButton.classList.add('neuroglancer-segment-list-entry-delete');
            row.children[0].appendChild(deleteButton);
          }
          listElement.appendChild(row);
        }
        if (segmentationDisplayState != null) {
          const updateSegments = context.registerCancellable(animationFrameDebounce(() => {
            const {visibleSegments} = segmentationDisplayState.segmentationGroupState.value;
            let numVisible = 0;
            for (const id of segments) {
              if (visibleSegments.has(id)) {
                ++numVisible;
              }
            }
            for (const row of rows) {
              segmentWidgetFactory.update(row);
            }
            headerCheckbox!.checked = numVisible === segments.length && numVisible > 0;
            headerCheckbox!.indeterminate = (numVisible > 0) && (numVisible < segments.length);
          }));
          updateSegments();
          updateSegments.flush();
          registerCallbackWhenSegmentationDisplayStateChanged(
              segmentationDisplayState, context, updateSegments);
          context.registerDisposer(
              segmentationDisplayState.segmentationGroupState.changed.add(updateSegments));
        }
        parent.appendChild(listElement);
      });
}

const ANNOTATION_COLOR_JSON_KEY = 'annotationColor';
export function UserLayerWithAnnotationsMixin<TBase extends {new (...args: any[]): UserLayer}>(
    Base: TBase) {
  abstract class C extends Base implements UserLayerWithAnnotations {
    annotationStates = this.registerDisposer(new MergedAnnotationStates());
    annotationDisplayState = new AnnotationDisplayState();
    annotationCrossSectionRenderScaleHistogram = new RenderScaleHistogram();
    annotationCrossSectionRenderScaleTarget = trackableRenderScaleTarget(8);
    annotationProjectionRenderScaleHistogram = new RenderScaleHistogram();
    annotationProjectionRenderScaleTarget = trackableRenderScaleTarget(8);

    constructor(...args: any[]) {
      super(...args);
      this.annotationDisplayState.color.changed.add(this.specificationChanged.dispatch);
      this.annotationDisplayState.shader.changed.add(this.specificationChanged.dispatch);
      this.annotationDisplayState.shaderControls.changed.add(this.specificationChanged.dispatch);
      this.tabs.add(
          'annotations', {label: 'Annotations', order: 10, getter: () => new AnnotationTab(this)});

      let annotationStateReadyBinding: (() => void)|undefined;

      const updateReadyBinding = () => {
        const isReady = this.isReady;
        if (isReady && annotationStateReadyBinding !== undefined) {
          annotationStateReadyBinding();
          annotationStateReadyBinding = undefined;
        } else if (!isReady && annotationStateReadyBinding === undefined) {
          annotationStateReadyBinding = this.annotationStates.markLoading();
        }
      };
      this.readyStateChanged.add(updateReadyBinding);
      updateReadyBinding();

      const {mouseState} = this.manager.layerSelectedValues;
      this.registerDisposer(mouseState.changed.add(() => {
        if (mouseState.active) {
          const {pickedAnnotationLayer} = mouseState;
          if (pickedAnnotationLayer !== undefined &&
              this.annotationStates.states.includes(pickedAnnotationLayer)) {
            const existingValue = this.annotationDisplayState.hoverState.value;
            if (existingValue === undefined || existingValue.id !== mouseState.pickedAnnotationId!
                || existingValue.partIndex !== mouseState.pickedOffset ||
                existingValue.annotationLayerState !== pickedAnnotationLayer) {
              this.annotationDisplayState.hoverState.value = {
                id: mouseState.pickedAnnotationId!,
                partIndex: mouseState.pickedOffset,
                annotationLayerState: pickedAnnotationLayer,
              };
            }
            return;
          }
        }
        this.annotationDisplayState.hoverState.value = undefined;
      }));
    }

    initializeAnnotationLayerViewTab(tab: AnnotationLayerView) {
      tab;
    }

    restoreState(specification: any) {
      super.restoreState(specification);
      this.annotationDisplayState.color.restoreState(specification[ANNOTATION_COLOR_JSON_KEY]);
    }

    captureSelectionState(state: this['selectionState'], mouseState: MouseSelectionState) {
      super.captureSelectionState(state, mouseState);
      const annotationLayer = mouseState.pickedAnnotationLayer;
      if (annotationLayer === undefined ||
          !this.annotationStates.states.includes(annotationLayer)) {
        return;
      }

      state.annotationId = mouseState.pickedAnnotationId;
      state.annotationType = mouseState.pickedAnnotationType;
      state.annotationBuffer = new Uint8Array(
        mouseState.pickedAnnotationBuffer!, mouseState.pickedAnnotationBufferBaseOffset!);
      state.annotationIndex = mouseState.pickedAnnotationIndex!;
      state.annotationCount = mouseState.pickedAnnotationCount!;
      state.annotationPartIndex = mouseState.pickedOffset;
      state.annotationSourceIndex = annotationLayer.sourceIndex;
      state.annotationSubsource = annotationLayer.subsourceId;
    }

    displayAnnotationState(state: this['selectionState'], parent: HTMLElement, context: RefCounted):
        boolean {
      if (state.annotationId === undefined) return false;
      const annotationLayer = this.annotationStates.states.find(
          x => x.sourceIndex === state.annotationSourceIndex &&
               x.subsubsourceId === state.annotationSubsubsourceId &&
              (state.annotationSubsource === undefined ||
               x.subsourceId === state.annotationSubsource));
      if (annotationLayer === undefined) return false;
      const reference =
          context.registerDisposer(annotationLayer.source.getReference(state.annotationId));
      parent.appendChild(
          context
              .registerDisposer(new DependentViewWidget(
                  context.registerDisposer(
                      new AggregateWatchableValue(() => ({
                                                    annotation: reference,
                                                    chunkTransform: annotationLayer.chunkTransform
                                                  }))),
                  ({annotation, chunkTransform}, parent, context) => {
                  addContextDisposer();
                },
              });
            deleteButton.classList.add('neuroglancer-segment-list-entry-delete');
              newRow.appendChild(deleteButton);
              const idElement = document.createElement('input');
              idElement.autocomplete = 'off';
                        const baseOffset = state.annotationBuffer.byteOffset;
                        const dataView = new DataView(state.annotationBuffer.buffer);
                        const isLittleEndian = Endianness.LITTLE === ENDIANNESS;
                        const {properties} = annotationLayer.source;
                        const annotationPropertySerializer =
                          new AnnotationPropertySerializer(rank, numGeometryBytes, properties);
                        const annotationIndex = state.annotationIndex!;
                        const annotationCount = state.annotationCount!;
                        annotation = handler.deserialize(
                            dataView,
                            baseOffset +
                                annotationPropertySerializer.propertyGroupBytes[0] *
                                    annotationIndex,
                            isLittleEndian, rank, state.annotationId!);
                        annotationPropertySerializer.deserialize(
                            dataView, baseOffset, annotationIndex, annotationCount, isLittleEndian,
                            annotation.properties = new Array(properties.length));
                        if (annotationLayer.source.hasNonSerializedProperties()) {
                          statusText = 'Loading...';
                        }
                      } else {
                        statusText = (annotation === null) ? 'Annotation not found' : 'Loading...';
                      }
                    }
                    if (annotation != null) {
                      const layerRank =
                          chunkTransform.error === undefined ? chunkTransform.layerRank : 0;
                      const positionGrid = document.createElement('div');
                      positionGrid.classList.add(
                          'neuroglancer-selected-annotation-details-position-grid');
                      positionGrid.style.gridTemplateColumns = `[icon] 0fr [copy] 0fr repeat(${
                          layerRank}, [dim] 0fr [coord] 0fr) [move] 0fr [delete] 0fr`;
                      parent.appendChild(positionGrid);

                      const handler = annotationTypeHandlers[annotation.type];
                      const icon = document.createElement('div');
                      icon.className = 'neuroglancer-selected-annotation-details-icon';
                      icon.textContent = handler.icon;
                      positionGrid.appendChild(icon);

                      if (layerRank !== 0) {
                        const {layerDimensionNames} =
                            (chunkTransform as ChunkTransformParameters).modelTransform;
                        for (let i = 0; i < layerRank; ++i) {
                          const dimElement = document.createElement('div');
                          dimElement.classList.add(
                              'neuroglancer-selected-annotation-details-position-dim');
                          dimElement.textContent = layerDimensionNames[i];
                          dimElement.style.gridColumn = `dim ${i + 1}`;
                          positionGrid.appendChild(dimElement);
                        }
                        visitTransformedAnnotationGeometry(
                            annotation, chunkTransform as ChunkTransformParameters,
                            (layerPosition, isVector) => {
                              const copyButton = makeCopyButton({
                                title: 'Copy position',
                                onClick: () => {
                                  setClipboard(layerPosition.map(x => Math.floor(x)).join(', '));
                                },
                              });
                              copyButton.style.gridColumn = 'copy';
                              positionGrid.appendChild(copyButton);
                              for (let layerDim = 0; layerDim < layerRank; ++layerDim) {
                                const coordElement = document.createElement('div');
                                coordElement.classList.add(
                                    'neuroglancer-selected-annotation-details-position-coord');
        }
        if (segmentationDisplayState != null) {
          const updateSegments = context.registerCancellable(animationFrameDebounce(() => {
                const {visibleSegments} = segmentationDisplayState;
                let numVisible = 0;
                for (const id of segments) {
                  if (visibleSegments.has(id)) {
                    ++numVisible;
                  }
                }
                for (const row of rows) {
                  segmentWidgetFactory.update(row);
                }
                headerCheckbox!.checked = numVisible === segments.length && numVisible > 0;
                headerCheckbox!.indeterminate = (numVisible > 0) && (numVisible < segments.length);
          }));
          updateSegments();
          updateSegments.flush();
                      if (!annotationLayer.source.readonly) {
                        const button = makeDeleteButton({
                          title: 'Delete annotation',
                          onClick: () => {
                            annotationLayer.source.delete(reference);
                          }
                        });
                        button.classList.add('neuroglancer-selected-annotation-details-delete');
                        positionGrid.appendChild(button);
                      }

                      const {relationships, properties} = annotationLayer.source;
                      const sourceReadonly = annotationLayer.source.readonly;

                      for (let i = 0, count = properties.length; i < count; ++i) {
                        const property = properties[i];
                        const label = document.createElement('label');
                        label.classList.add('neuroglancer-annotation-property');
                        const idElement = document.createElement('span');
                        idElement.classList.add('neuroglancer-annotation-property-label');
                        idElement.textContent = property.identifier;
                        label.appendChild(idElement);
                        const {description} = property;
                        if (description !== undefined) {
                          label.title = description;
                        }
                        const value = annotation.properties[i];
                        const valueElement = document.createElement('span');
                        valueElement.classList.add('neuroglancer-annotation-property-value');
                        switch (property.type) {
                          case 'rgb': {
                            const colorVec = unpackRGB(value);
                            const hex = serializeColor(colorVec);
                            valueElement.textContent = hex;
                            valueElement.style.backgroundColor = hex;
                            valueElement.style.color =
                                useWhiteBackground(colorVec) ? 'white' : 'black';
                            break;
                          }
                          case 'rgba': {
                            const colorVec = unpackRGB(value);
                            valueElement.textContent = serializeColor(unpackRGBA(value));
                            valueElement.style.backgroundColor = serializeColor(unpackRGB(value));
                            valueElement.style.color =
                                useWhiteBackground(colorVec) ? 'white' : 'black';
                            break;
                          }
                          default:
                            valueElement.textContent = formatNumericProperty(property, value);
                            break;
                        }
                        label.appendChild(valueElement);
                        parent.appendChild(label);
                      }

                      const {relatedSegments} = annotation;
                      for (let i = 0, count = relationships.length; i < count; ++i) {
                        const related = relatedSegments === undefined ? [] : relatedSegments[i];
                        if (related.length === 0 && sourceReadonly) continue;
                        const relationshipIndex = i;
                        const relationship = relationships[i];
                        parent.appendChild(
                            context
                                .registerDisposer(makeRelatedSegmentList(
                                    relationship, related,
                                    annotationLayer.displayState.relationshipStates
                                        .get(relationship)
                                        .segmentationState,
                                    sourceReadonly ?
                                        undefined :
                                        newIds => {
                                          const annotation = reference.value;
                                          if (annotation == null) {
                                            return;
                                          }
                                          let {relatedSegments} = annotation;
                                          if (relatedSegments === undefined) {
                                            relatedSegments =
                                                annotationLayer.source.relationships.map(() => []);
                                          } else {
                                            relatedSegments = relatedSegments.slice();
                                          }
                                          relatedSegments[relationshipIndex] = newIds;
                                          const newAnnotation = {...annotation, relatedSegments};
                                          annotationLayer.source.update(reference, newAnnotation);
                                          annotationLayer.source.commit(reference);
                                        }))
                                .element);
                      }

                      if (!annotationLayer.source.readonly || annotation.description) {
                        if (annotationLayer.source.readonly) {
                          const description = document.createElement('div');
                          description.className = 'neuroglancer-annotation-details-description';
                          description.textContent = annotation.description || '';
                          parent.appendChild(description);
                        } else {
                          const description = document.createElement('textarea');
                          description.value = annotation.description || '';
                          description.rows = 3;
                          description.className = 'neuroglancer-annotation-details-description';
                          description.placeholder = 'Description';
                          description.addEventListener('change', () => {
                            const x = description.value;
                            annotationLayer.source.update(
                                reference, {...annotation!, description: x ? x : undefined});
                            annotationLayer.source.commit(reference);
                          });
                          parent.appendChild(description);
                        }
                      }
                    }
                    if (statusText !== undefined) {
                      const statusMessage = document.createElement('div');
                      statusMessage.classList.add('neuroglancer-selection-annotation-status');
                      statusMessage.textContent = statusText;
                      parent.appendChild(statusMessage);
                    }
                  }))
              .element);
      return true;
    }


    displaySelectionState(
        state: this['selectionState'], parent: HTMLElement,
        context: DependentViewContext): boolean {
      let displayed = this.displayAnnotationState(state, parent, context);
      if (super.displaySelectionState(state, parent, context)) displayed = true;
      return displayed;
    }

    addLocalAnnotations(
        loadedSubsource: LoadedDataSubsource, source: AnnotationSource, role: RenderLayerRole) {
      const {subsourceEntry} = loadedSubsource;
      const state = new AnnotationLayerState({
        localPosition: this.localPosition,
        transform: loadedSubsource.getRenderLayerTransform(),
        source,
        displayState: this.annotationDisplayState,
        dataSource: loadedSubsource.loadedDataSource.layerDataSource,
        subsourceIndex: loadedSubsource.subsourceIndex,
        subsourceId: subsourceEntry.id,
        role,
      });
      this.addAnnotationLayerState(state, loadedSubsource);
    }

    addStaticAnnotations(loadedSubsource: LoadedDataSubsource) {
      const {subsourceEntry} = loadedSubsource;
      const {staticAnnotations} = subsourceEntry.subsource;
      if (staticAnnotations === undefined) return false;
      loadedSubsource.activate(() => {
        this.addLocalAnnotations(
            loadedSubsource, staticAnnotations, RenderLayerRole.DEFAULT_ANNOTATION);
      });
      return true;
    }

    addAnnotationLayerState(state: AnnotationLayerState, loadedSubsource: LoadedDataSubsource) {
      const refCounted = loadedSubsource.activated!;
      refCounted.registerDisposer(this.annotationStates.add(state));
      const annotationLayer = new AnnotationLayer(this.manager.chunkManager, state.addRef());
      if (annotationLayer.source instanceof MultiscaleAnnotationSource) {
        const crossSectionRenderLayer = new SpatiallyIndexedSliceViewAnnotationLayer({
          annotationLayer: annotationLayer.addRef(),
          renderScaleTarget: this.annotationCrossSectionRenderScaleTarget,
          renderScaleHistogram: this.annotationCrossSectionRenderScaleHistogram
        });
        refCounted.registerDisposer(
            loadedSubsource.messages.addChild(crossSectionRenderLayer.messages));

        const projectionRenderLayer = new SpatiallyIndexedPerspectiveViewAnnotationLayer({
          annotationLayer: annotationLayer.addRef(),
          renderScaleTarget: this.annotationProjectionRenderScaleTarget,
          renderScaleHistogram: this.annotationProjectionRenderScaleHistogram
        });
        refCounted.registerDisposer(
            loadedSubsource.messages.addChild(projectionRenderLayer.messages));

        refCounted.registerDisposer(registerNested((context, value) => {
          if (value) {
            context.registerDisposer(this.addRenderLayer(crossSectionRenderLayer.addRef()));
            context.registerDisposer(this.addRenderLayer(projectionRenderLayer.addRef()));
          }
        }, this.annotationDisplayState.displayUnfiltered));
      }
      {
        const renderLayer = new SliceViewAnnotationLayer(
            annotationLayer, this.annotationCrossSectionRenderScaleHistogram);
        refCounted.registerDisposer(this.addRenderLayer(renderLayer));
        refCounted.registerDisposer(loadedSubsource.messages.addChild(renderLayer.messages));
      }
      {
        const renderLayer = new PerspectiveViewAnnotationLayer(
            annotationLayer.addRef(), this.annotationProjectionRenderScaleHistogram);
        refCounted.registerDisposer(this.addRenderLayer(renderLayer));
        refCounted.registerDisposer(loadedSubsource.messages.addChild(renderLayer.messages));
      }
    }

    selectAnnotation(
        annotationLayer: Borrowed<AnnotationLayerState>, id: string, pin: boolean|'toggle') {
      this.manager.root.selectionState.captureSingleLayerState(this, state => {
        state.annotationId = id;
        state.annotationSourceIndex = annotationLayer.sourceIndex;
        state.annotationSubsource = annotationLayer.subsourceId;
        state.annotationSubsubsourceId = annotationLayer.subsubsourceId;
        return true;
      }, pin);
    }

    toJSON() {
      const x = super.toJSON();
      x[ANNOTATION_COLOR_JSON_KEY] = this.annotationDisplayState.color.toJSON();
      return x;
    }
  }
  return C;
}

export type UserLayerWithAnnotations =
    InstanceType<ReturnType<typeof UserLayerWithAnnotationsMixin>>;
                          const hex = serializeColor(colorVec);
                          valueElement.textContent = hex;
                          valueElement.style.backgroundColor = hex;
                          valueElement.style.color = useWhiteBackground(colorVec) ? 'white' : 'black';
                          break;
                        }
                        case 'rgba': {

<MSG> fix(annotations): improve performance of annotation list using virtual_list

This greatly improves the performance of the annotation list when there are a large number of
annotations.

<DFF> @@ -20,7 +20,7 @@
 
 import './annotations.css';
 
-import {Annotation, AnnotationReference, AnnotationSource, annotationToJson, AnnotationType, annotationTypeHandlers, AxisAlignedBoundingBox, Ellipsoid, Line} from 'neuroglancer/annotation';
+import {Annotation, AnnotationId, AnnotationReference, AnnotationSource, annotationToJson, AnnotationType, annotationTypeHandlers, AxisAlignedBoundingBox, Ellipsoid, Line} from 'neuroglancer/annotation';
 import {AnnotationDisplayState, AnnotationLayerState} from 'neuroglancer/annotation/annotation_layer_state';
 import {MultiscaleAnnotationSource} from 'neuroglancer/annotation/frontend_source';
 import {AnnotationLayer, PerspectiveViewAnnotationLayer, SliceViewAnnotationLayer} from 'neuroglancer/annotation/renderlayer';
@@ -37,11 +37,11 @@ import {AggregateWatchableValue, makeCachedLazyDerivedWatchableValue, registerNe
 import {getDefaultSelectBindings} from 'neuroglancer/ui/default_input_event_bindings';
 import {registerTool, Tool} from 'neuroglancer/ui/tool';
 import {animationFrameDebounce} from 'neuroglancer/util/animation_frame_debounce';
-import {arraysEqual, gatherUpdate} from 'neuroglancer/util/array';
+import {arraysEqual, ArraySpliceOp, gatherUpdate} from 'neuroglancer/util/array';
 import {setClipboard} from 'neuroglancer/util/clipboard';
 import {serializeColor, unpackRGB, unpackRGBA, useWhiteBackground} from 'neuroglancer/util/color';
 import {Borrowed, disposableOnce, Owned, RefCounted} from 'neuroglancer/util/disposable';
-import {removeChildren, removeFromParent, updateChildren} from 'neuroglancer/util/dom';
+import {removeChildren} from 'neuroglancer/util/dom';
 import {ValueOrError} from 'neuroglancer/util/error';
 import {vec3} from 'neuroglancer/util/geom';
 import {verifyInt, verifyObject, verifyObjectProperty, verifyOptionalObjectProperty, verifyString} from 'neuroglancer/util/json';
@@ -49,7 +49,7 @@ import {EventActionMap, KeyboardEventBinder, registerActionListener} from 'neuro
 import * as matrix from 'neuroglancer/util/matrix';
 import {MouseEventBinder} from 'neuroglancer/util/mouse_bindings';
 import {formatScaleWithUnitAsString} from 'neuroglancer/util/si_units';
-import {NullarySignal} from 'neuroglancer/util/signal';
+import {NullarySignal, Signal} from 'neuroglancer/util/signal';
 import {formatIntegerBounds, formatIntegerPoint} from 'neuroglancer/util/spatial_units';
 import {Uint64} from 'neuroglancer/util/uint64';
 import * as vector from 'neuroglancer/util/vector';
@@ -61,6 +61,7 @@ import {DependentViewContext, DependentViewWidget} from 'neuroglancer/widget/dep
 import {makeIcon} from 'neuroglancer/widget/icon';
 import {makeMoveToButton} from 'neuroglancer/widget/move_to_button';
 import {Tab} from 'neuroglancer/widget/tab_view';
+import {VirtualList, VirtualListSource} from 'neuroglancer/widget/virtual_list';
 
 interface AnnotationIdAndPart {
   id: string, sourceIndex: number;
@@ -385,17 +386,25 @@ function visitTransformedAnnotationGeometry(
 
 interface AnnotationLayerViewAttachedState {
   refCounted: RefCounted;
-  listElements: Map<string, HTMLElement>;
-  sublistContainer: HTMLElement;
+  annotations: Annotation[];
+  idToIndex: Map<AnnotationId, number>;
+  listOffset: number;
 }
 
 export class AnnotationLayerView extends Tab {
-  private previousSelectedId: string|undefined = undefined;
-  private previousSelectedAnnotationLayerState: AnnotationLayerState|undefined = undefined;
+  private previousSelectedState:
+      {annotationId: string, annotationLayerState: AnnotationLayerState, pin: boolean}|undefined =
+          undefined;
   private previousHoverId: string|undefined = undefined;
   private previousHoverAnnotationLayerState: AnnotationLayerState|undefined = undefined;
 
-  private listContainer = document.createElement('div');
+  private virtualListSource: VirtualListSource = {
+    length: 0,
+    render: (index: number) => this.render(index),
+    changed: new Signal<(splices: ArraySpliceOp[]) => void>(),
+  };
+  private virtualList = new VirtualList({source: this.virtualListSource});
+  private listElements: {state: AnnotationLayerState, annotation: Annotation}[] = [];
   private updated = false;
   private mutableControls = document.createElement('div');
   private headerRow = document.createElement('div');
@@ -415,7 +424,6 @@ export class AnnotationLayerView extends Tab {
     for (const [state, info] of attachedAnnotationStates) {
       if (!states.includes(state)) {
         attachedAnnotationStates.delete(state);
-        info.listElements.clear();
         info.refCounted.dispose();
       }
     }
@@ -436,10 +444,8 @@ export class AnnotationLayerView extends Tab {
             (annotationId) => this.deleteAnnotationElement(annotationId, state)));
       }
       refCounted.registerDisposer(state.transform.changed.add(this.forceUpdateView));
-      const sublistContainer = document.createElement('div');
-      sublistContainer.classList.add('neuroglancer-annotation-sublist');
       newAttachedAnnotationStates.set(
-          state, {refCounted, listElements: new Map(), sublistContainer});
+          state, {refCounted, annotations: [], idToIndex: new Map(), listOffset: 0});
     }
     this.attachedAnnotationStates = newAttachedAnnotationStates;
     attachedAnnotationStates.clear();
@@ -456,6 +462,8 @@ export class AnnotationLayerView extends Tab {
   private localDimensionIndices: number[] = [];
   private curCoordinateSpaceGeneration = -1;
   private prevCoordinateSpaceGeneration = -1;
+  private columnWidths: number[] = [];
+  private gridTemplate: string = '';
 
   private updateCoordinateSpace() {
     const localCoordinateSpace = this.layer.localCoordinateSpace.value;
@@ -495,7 +503,6 @@ export class AnnotationLayerView extends Tab {
       public state: Owned<SelectedAnnotationState>, public displayState: AnnotationDisplayState) {
     super();
     this.element.classList.add('neuroglancer-annotation-layer-view');
-    this.listContainer.classList.add('neuroglancer-annotation-list');
     this.registerDisposer(state);
     this.registerDisposer(this.visibility.changed.add(() => this.updateView()));
     this.registerDisposer(
@@ -553,14 +560,19 @@ export class AnnotationLayerView extends Tab {
     toolbox.appendChild(mutableControls);
     this.element.appendChild(toolbox);
 
-    this.element.appendChild(this.listContainer);
-    this.listContainer.addEventListener('mouseleave', () => {
+    this.element.appendChild(this.headerRow);
+    const {virtualList} = this;
+    virtualList.element.classList.add('neuroglancer-annotation-list');
+    this.element.appendChild(virtualList.element);
+    this.virtualList.element.addEventListener('mouseleave', () => {
       this.displayState.hoverState.value = undefined;
     });
 
-    this.registerDisposer(new MouseEventBinder(this.listContainer, getDefaultSelectBindings()));
+    this.registerDisposer(
+        new MouseEventBinder(this.virtualList.element, getDefaultSelectBindings()));
     this.registerDisposer(this.displayState.hoverState.changed.add(() => this.updateHoverView()));
-    this.registerDisposer(this.state.changed.add(() => this.updateSelectionView()));
+    this.registerDisposer(
+        this.selectedAnnotationState.changed.add(() => this.updateSelectionView()));
     this.registerDisposer(this.layer.localCoordinateSpace.changed.add(() => {
       this.updateCoordinateSpace();
       this.updateView();
@@ -571,19 +583,31 @@ export class AnnotationLayerView extends Tab {
     }));
     this.updateCoordinateSpace();
     this.updateAttachedAnnotationLayerStates();
+    this.updateSelectionView();
+  }
+
+  private getRenderedAnnotationListElement(
+      state: AnnotationLayerState, id: AnnotationId, scrollIntoView: boolean = false): HTMLElement
+      |undefined {
+    const attached = this.attachedAnnotationStates.get(state);
+    if (attached == undefined) return undefined;
+    const index = attached.idToIndex.get(id);
+    if (index === undefined) return undefined;
+    const listIndex = attached.listOffset + index;
+    if (scrollIntoView) {
+      this.virtualList.scrollItemIntoView(index)
+    }
+    return this.virtualList.getItemElement(listIndex);
   }
 
   private clearSelectionClass() {
-    const {previousSelectedAnnotationLayerState, previousSelectedId} = this;
-    if (previousSelectedAnnotationLayerState !== undefined) {
-      this.previousSelectedAnnotationLayerState = undefined;
-      this.previousSelectedId = undefined;
-      const attached = this.attachedAnnotationStates.get(previousSelectedAnnotationLayerState);
-      if (attached === undefined) return;
-      const element = attached.listElements.get(previousSelectedId!);
-      if (element !== undefined) {
-        element.classList.remove('neuroglancer-annotation-selected');
-      }
+    const {previousSelectedState: state} = this;
+    if (state === undefined) return;
+    this.previousSelectedState = undefined;
+    const element =
+        this.getRenderedAnnotationListElement(state.annotationLayerState, state.annotationId);
+    if (element !== undefined) {
+      element.classList.remove('neuroglancer-annotation-selected');
     }
   }
 
@@ -592,38 +616,48 @@ export class AnnotationLayerView extends Tab {
     if (previousHoverAnnotationLayerState !== undefined) {
       this.previousHoverAnnotationLayerState = undefined;
       this.previousHoverId = undefined;
-      const attached = this.attachedAnnotationStates.get(previousHoverAnnotationLayerState);
-      if (attached === undefined) return;
-      const element = attached.listElements.get(previousHoverId!);
+      const element = this.getRenderedAnnotationListElement(
+          previousHoverAnnotationLayerState, previousHoverId!!);
       if (element !== undefined) {
         element.classList.remove('neuroglancer-annotation-hover');
       }
     }
   }
 
+  private selectedAnnotationState = makeCachedLazyDerivedWatchableValue((selectionState, pin) => {
+    if (selectionState === undefined) return undefined;
+    const {layer} = this;
+    const layerSelectionState = selectionState.layers.find(s => s.layer === layer)?.state;
+    if (layerSelectionState === undefined) return undefined;
+    const {annotationId} = layerSelectionState;
+    if (annotationId === undefined) return undefined;
+    const annotationLayerState = this.annotationStates.states.find(
+        x => x.sourceIndex === layerSelectionState.annotationSourceIndex &&
+            (layerSelectionState.annotationSubsource === undefined ||
+             x.subsourceId === layerSelectionState.annotationSubsource));
+    if (annotationLayerState === undefined) return undefined;
+    return {annotationId, annotationLayerState, pin};
+  }, this.layer.manager.root.selectionState, this.layer.manager.root.selectionState.pin);
+
   private updateSelectionView() {
-    const selectedValue = this.state.value;
-    let newSelectedId: string|undefined;
-    let newSelectedAnnotationLayerState: AnnotationLayerState|undefined;
-    if (selectedValue !== undefined) {
-      newSelectedId = selectedValue.id;
-      newSelectedAnnotationLayerState = this.state.selectedAnnotationLayer;
-    }
-    const {previousSelectedId, previousSelectedAnnotationLayerState} = this;
-    if (newSelectedId === previousSelectedId &&
-        previousSelectedAnnotationLayerState === newSelectedAnnotationLayerState) {
+    const selectionState = this.selectedAnnotationState.value;
+    const {previousSelectedState} = this;
+    if (previousSelectedState === selectionState ||
+        (previousSelectedState !== undefined && selectionState !== undefined &&
+         previousSelectedState.annotationId === selectionState.annotationId &&
+         previousSelectedState.annotationLayerState === selectionState.annotationLayerState &&
+         previousSelectedState.pin === selectionState.pin)) {
       return;
     }
     this.clearSelectionClass();
-    this.previousSelectedId = newSelectedId;
-    this.previousSelectedAnnotationLayerState = newSelectedAnnotationLayerState;
-    if (newSelectedId === undefined) return;
-    const attached = this.attachedAnnotationStates.get(newSelectedAnnotationLayerState!);
-    if (attached === undefined) return;
-    const element = attached.listElements.get(newSelectedId);
-    if (element === undefined) return;
-    element.classList.add('neuroglancer-annotation-selected');
-    element.scrollIntoView();
+    this.previousSelectedState = selectionState;
+    if (selectionState === undefined) return;
+    const element = this.getRenderedAnnotationListElement(
+        selectionState.annotationLayerState, selectionState.annotationId,
+        /*scrollIntoView=*/ selectionState.pin);
+    if (element !== undefined) {
+      element.classList.add('neuroglancer-annotation-selected');
+    }
   }
 
   private updateHoverView() {
@@ -643,19 +677,36 @@ export class AnnotationLayerView extends Tab {
     this.previousHoverId = newHoverId;
     this.previousHoverAnnotationLayerState = newAnnotationLayerState;
     if (newHoverId === undefined) return;
-    const attached = this.attachedAnnotationStates.get(newAnnotationLayerState!);
-    if (attached === undefined) return;
-    const element = attached.listElements.get(newHoverId);
+    const element = this.getRenderedAnnotationListElement(newAnnotationLayerState!, newHoverId);
     if (element === undefined) return;
     element.classList.add('neuroglancer-annotation-hover');
   }
 
+  private render(index: number) {
+    const {annotation, state} = this.listElements[index];
+    return this.makeAnnotationListElement(annotation, state);
+  }
+
+  private setColumnWidth(column: number, width: number) {
+    // Padding
+    width += 2;
+    const {columnWidths} = this;
+    if (columnWidths[column] > width) {
+      // False if `columnWidths[column] === undefined`.
+      return;
+    }
+    columnWidths[column] = width;
+    this.element.style.setProperty(`--neuroglancer-column-${column}-width`, `${width}ch`);
+  }
+
   private updateView() {
     if (!this.visible) {
       return;
     }
     if (this.curCoordinateSpaceGeneration !== this.prevCoordinateSpaceGeneration) {
       this.updated = false;
+      const {columnWidths} = this;
+      columnWidths.length = 0;
       const {headerRow} = this;
       const symbolPlaceholder = document.createElement('div');
       symbolPlaceholder.style.gridColumn = `symbol`;
@@ -666,6 +717,7 @@ export class AnnotationLayerView extends Tab {
       removeChildren(headerRow);
       headerRow.appendChild(symbolPlaceholder);
       let i = 0;
+      let gridTemplate = '[symbol] 2ch';
       const addDimension = (coordinateSpace: CoordinateSpace, dimIndex: number) => {
         const dimWidget = document.createElement('div');
         dimWidget.classList.add('neuroglancer-annotations-view-dimension');
@@ -679,6 +731,8 @@ export class AnnotationLayerView extends Tab {
         dimWidget.appendChild(name);
         dimWidget.appendChild(scale);
         dimWidget.style.gridColumn = `dim ${i + 1}`;
+        this.setColumnWidth(i, scale.textContent.length + name.textContent.length + 3);
+        gridTemplate += ` [dim] var(--neuroglancer-column-${i}-width)`;
         ++i;
         headerRow.appendChild(dimWidget);
       };
@@ -691,8 +745,9 @@ export class AnnotationLayerView extends Tab {
         addDimension(localCoordinateSpace, localDim);
       }
       headerRow.appendChild(deletePlaceholder);
-      this.listContainer.style.gridTemplateColumns =
-          `[symbol] min-content repeat(${i}, [dim] min-content) [delete] min-content`;
+      gridTemplate += ` [delete] 2ch`;
+      this.gridTemplate = gridTemplate;
+      headerRow.style.gridTemplateColumns = gridTemplate;
       this.prevCoordinateSpaceGeneration = this.curCoordinateSpaceGeneration;
     }
     if (this.updated) {
@@ -700,25 +755,40 @@ export class AnnotationLayerView extends Tab {
     }
 
     let isMutable = false;
-    const self = this;
-    function* sublistContainers() {
-      yield self.headerRow;
-      for (const [state, {sublistContainer, listElements}] of self.attachedAnnotationStates) {
-        if (!state.source.readonly) isMutable = true;
-        removeChildren(sublistContainer);
-        listElements.clear();
-        if (state.chunkTransform.value.error !== undefined) continue;
-        for (const annotation of state.source) {
-          sublistContainer.appendChild(self.makeAnnotationListElement(annotation, state));
-        }
-        yield sublistContainer;
+    const {listElements} = this;
+    listElements.length = 0;
+    for (const [state, info] of this.attachedAnnotationStates) {
+      if (!state.source.readonly) isMutable = true;
+      if (state.chunkTransform.value.error !== undefined) continue;
+      const {source} = state;
+      const annotations = Array.from(source);
+      info.annotations = annotations;
+      const {idToIndex} = info;
+      idToIndex.clear();
+      for (let i = 0, length = annotations.length; i < length; ++i) {
+        idToIndex.set(annotations[i].id, i);
+      }
+      for (const annotation of annotations) {
+        listElements.push({state, annotation});
       }
     }
-    updateChildren(this.listContainer, sublistContainers());
+    const oldLength = this.virtualListSource.length;
+    this.updateListLength();
+    this.virtualListSource.changed!.dispatch(
+        [{retainCount: 0, deleteCount: oldLength, insertCount: listElements.length}]);
     this.mutableControls.style.display = isMutable ? 'contents' : 'none';
     this.resetOnUpdate();
   }
 
+  private updateListLength() {
+    let length = 0;
+    for (const info of this.attachedAnnotationStates.values()) {
+      info.listOffset = length;
+      length += info.annotations.length;
+    }
+    this.virtualListSource.length = length;
+  }
+
   private addAnnotationElement(annotation: Annotation, state: AnnotationLayerState) {
     if (!this.visible) {
       this.updated = false;
@@ -730,7 +800,14 @@ export class AnnotationLayerView extends Tab {
     }
     const info = this.attachedAnnotationStates.get(state);
     if (info !== undefined) {
-      info.sublistContainer.appendChild(this.makeAnnotationListElement(annotation, state));
+      const index = info.annotations.length;
+      info.annotations.push(annotation);
+      info.idToIndex.set(annotation.id, index);
+      const spliceStart = info.listOffset + index;
+      this.listElements.splice(spliceStart, 0, {state, annotation});
+      this.updateListLength();
+      this.virtualListSource.changed!.dispatch(
+          [{retainCount: spliceStart, deleteCount: 0, insertCount: 1}]);
     }
     this.resetOnUpdate();
   }
@@ -746,12 +823,13 @@ export class AnnotationLayerView extends Tab {
     }
     const info = this.attachedAnnotationStates.get(state);
     if (info !== undefined) {
-      const {listElements} = info;
-      const element = listElements.get(annotation.id);
-      if (element !== undefined) {
-        const newElement = this.makeAnnotationListElement(annotation, state);
-        info.sublistContainer.replaceChild(newElement, element);
-        listElements.set(annotation.id, newElement);
+      const index = info.idToIndex.get(annotation.id);
+      if (index !== undefined) {
+        const updateStart = info.listOffset + index;
+        info.annotations[index] = annotation;
+        this.listElements[updateStart].annotation = annotation;
+        this.virtualListSource.changed!.dispatch(
+            [{retainCount: updateStart, deleteCount: 1, insertCount: 1}]);
       }
     }
     this.resetOnUpdate();
@@ -766,12 +844,22 @@ export class AnnotationLayerView extends Tab {
       this.updateView();
       return;
     }
-    const attached = this.attachedAnnotationStates.get(state);
-    if (attached !== undefined) {
-      let element = attached.listElements.get(annotationId);
-      if (element !== undefined) {
-        removeFromParent(element);
-        attached.listElements.delete(annotationId);
+    const info = this.attachedAnnotationStates.get(state);
+    if (info !== undefined) {
+      const {idToIndex} = info;
+      const index = idToIndex.get(annotationId);
+      if (index !== undefined) {
+        const spliceStart = info.listOffset + index;
+        const {annotations} = info;
+        annotations.splice(index, 1);
+        idToIndex.delete(annotationId);
+        for (let i = index, length = annotations.length; i < length; ++i) {
+          idToIndex.set(annotations[i].id, i);
+        }
+        this.listElements.splice(spliceStart, 1);
+        this.updateListLength();
+        this.virtualListSource.changed!.dispatch(
+            [{retainCount: spliceStart, deleteCount: 1, insertCount: 0}]);
       }
     }
     this.resetOnUpdate();
@@ -789,12 +877,13 @@ export class AnnotationLayerView extends Tab {
     const chunkTransform = state.chunkTransform.value as ChunkTransformParameters;
     const element = document.createElement('div');
     element.classList.add('neuroglancer-annotation-list-entry');
+    element.style.gridTemplateColumns = this.gridTemplate;
+
     element.title = 'Click to select, right click to recenter view.';
 
     const icon = document.createElement('div');
     icon.className = 'neuroglancer-annotation-icon';
     icon.textContent = annotationTypeHandlers[annotation.type].icon;
-    icon.classList.add('neuroglancer-annotation-list-entry-highlight');
     element.appendChild(icon);
 
     let deleteButton: HTMLElement|undefined;
@@ -832,10 +921,11 @@ export class AnnotationLayerView extends Tab {
               if (layerDim !== -1) {
                 const coord = Math.floor(layerPosition[layerDim]);
                 const coordElement = document.createElement('div');
-                coordElement.textContent = coord.toString();
+                const text = coord.toString()
+                coordElement.textContent = text;
                 coordElement.classList.add('neuroglancer-annotation-coordinate');
-                coordElement.classList.add('neuroglancer-annotation-list-entry-highlight');
                 coordElement.style.gridColumn = `dim ${i + 1}`;
+                this.setColumnWidth(i, text.length);
                 position.appendChild(coordElement);
               }
               ++i;
@@ -851,7 +941,6 @@ export class AnnotationLayerView extends Tab {
       ++numRows;
       const description = document.createElement('div');
       description.classList.add('neuroglancer-annotation-description');
-      description.classList.add('neuroglancer-annotation-list-entry-highlight');
       description.textContent = annotation.description;
       element.appendChild(description);
     }
@@ -859,10 +948,6 @@ export class AnnotationLayerView extends Tab {
     if (deleteButton !== undefined) {
       deleteButton.style.gridRow = `span ${numRows}`;
     }
-
-
-    const info = this.attachedAnnotationStates.get(state)!;
-    info.listElements.set(annotation.id, element);
     element.addEventListener('mouseenter', () => {
       this.displayState.hoverState.value = {
         id: annotation.id,
@@ -888,7 +973,11 @@ export class AnnotationLayerView extends Tab {
         setLayerPosition(this.layer, chunkTransform, layerPosition);
       }
     });
-
+    const selectionState = this.selectedAnnotationState.value;
+    if (selectionState !== undefined && selectionState.annotationLayerState === state &&
+        selectionState.annotationId === annotation.id) {
+      element.classList.add('neuroglancer-annotation-selected');
+    }
     return element;
   }
 }
@@ -1264,7 +1353,7 @@ function makeRelatedSegmentList(
                   addContextDisposer();
                 },
               });
-            deleteButton.classList.add('neuroglancer-segment-list-entry-delete');
+              deleteButton.classList.add('neuroglancer-segment-list-entry-delete');
               newRow.appendChild(deleteButton);
               const idElement = document.createElement('input');
               idElement.autocomplete = 'off';
@@ -1337,18 +1426,18 @@ function makeRelatedSegmentList(
         }
         if (segmentationDisplayState != null) {
           const updateSegments = context.registerCancellable(animationFrameDebounce(() => {
-                const {visibleSegments} = segmentationDisplayState;
-                let numVisible = 0;
-                for (const id of segments) {
-                  if (visibleSegments.has(id)) {
-                    ++numVisible;
-                  }
-                }
-                for (const row of rows) {
-                  segmentWidgetFactory.update(row);
-                }
-                headerCheckbox!.checked = numVisible === segments.length && numVisible > 0;
-                headerCheckbox!.indeterminate = (numVisible > 0) && (numVisible < segments.length);
+            const {visibleSegments} = segmentationDisplayState;
+            let numVisible = 0;
+            for (const id of segments) {
+              if (visibleSegments.has(id)) {
+                ++numVisible;
+              }
+            }
+            for (const row of rows) {
+              segmentWidgetFactory.update(row);
+            }
+            headerCheckbox!.checked = numVisible === segments.length && numVisible > 0;
+            headerCheckbox!.indeterminate = (numVisible > 0) && (numVisible < segments.length);
           }));
           updateSegments();
           updateSegments.flush();
@@ -1637,7 +1726,8 @@ export function UserLayerWithAnnotationsMixin<TBase extends {new (...args: any[]
                           const hex = serializeColor(colorVec);
                           valueElement.textContent = hex;
                           valueElement.style.backgroundColor = hex;
-                          valueElement.style.color = useWhiteBackground(colorVec) ? 'white' : 'black';
+                          valueElement.style.color =
+                              useWhiteBackground(colorVec) ? 'white' : 'black';
                           break;
                         }
                         case 'rgba': {

fix(annotations): improve performance of annotation list using virtual_list