chromium/chrome/browser/resources/pdf/zoom_manager.ts

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {ZoomBehavior} from './browser_api.js';


const MIN_ZOOM_DELTA = 0.01;

/** @return Whether two numbers are approximately equal. */
function floatingPointEquals(a: number, b: number): boolean {
  // If the zoom level is close enough to the current zoom level, don't
  // change it. This avoids us getting into an infinite loop of zoom changes
  // due to floating point error.
  return Math.abs(a - b) <= MIN_ZOOM_DELTA;
}

// Abstract parent of classes that manage updating the browser with zoom changes
// and/or updating the viewer's zoom when the browser zoom changes.
export abstract class ZoomManager {
  protected browserZoom: number;
  protected getViewportZoom: () => number;
  private eventTarget_ = new EventTarget();

  /**
   * @param getViewportZoomCallback Callback to get the viewport's current zoom
   *     level.
   * @param initialZoom The initial browser zoom level.
   */
  constructor(getViewportZoomCallback: () => number, initialZoom: number) {
    this.browserZoom = initialZoom;
    this.getViewportZoom = getViewportZoomCallback;
  }

  getEventTarget(): EventTarget {
    return this.eventTarget_;
  }

  /**
   * Creates the appropriate kind of zoom manager given the zoom behavior.
   * @param zoomBehavior How to manage zoom.
   * @param getViewportZoom A function that gets the current viewport zoom.
   * @param setBrowserZoomFunction A function that sets the browser zoom to the
   *     provided value.
   * @param initialZoom The initial browser zoom level.
   */
  static create(
      zoomBehavior: ZoomBehavior, getViewportZoom: () => number,
      setBrowserZoomFunction: (zoom: number) => Promise<void>,
      initialZoom: number): ZoomManager {
    switch (zoomBehavior) {
      case ZoomBehavior.MANAGE:
        return new ActiveZoomManager(
            getViewportZoom, setBrowserZoomFunction, initialZoom);
      case ZoomBehavior.PROPAGATE_PARENT:
        return new EmbeddedZoomManager(getViewportZoom, initialZoom);
      default:
        return new InactiveZoomManager(getViewportZoom, initialZoom);
    }
  }

  /**
   * Invoked when a browser-initiated zoom-level change occurs.
   * @param newZoom the zoom level to zoom to.
   */
  abstract onBrowserZoomChange(newZoom: number): void;

  /** Invoked when an extension-initiated zoom-level change occurs. */
  abstract onPdfZoomChange(): void;

  /**
   * Combines the internal pdf zoom and the browser zoom to
   * produce the total zoom level for the viewer.
   * @param internalZoom the zoom level internal to the viewer.
   * @return the total zoom level.
   */
  applyBrowserZoom(internalZoom: number): number {
    return this.browserZoom * internalZoom;
  }

  /**
   * Given a zoom level, return the internal zoom level needed to
   * produce that zoom level.
   * @param totalZoom the total zoom level.
   * @return the zoom level internal to the viewer.
   */
  internalZoomComponent(totalZoom: number): number {
    return totalZoom / this.browserZoom;
  }
}

// Has no control over the browser's zoom and does not respond to browser zoom
// changes.
export class InactiveZoomManager extends ZoomManager {
  override onBrowserZoomChange(_newZoom: number) {}
  override onPdfZoomChange() {}
}

// ActiveZoomManager controls the browser's zoom.
class ActiveZoomManager extends ZoomManager {
  private setBrowserZoomFunction_: (zoom: number) => Promise<void>;
  private changingBrowserZoom_: Promise<void>|null = null;

  /**
   * Constructs a ActiveZoomManager.
   * @param getViewportZoom A function that gets the current viewport zoom level
   * @param setBrowserZoomFunction A function that sets the browser zoom to the
   *     provided value.
   * @param initialZoom The initial browser zoom level.
   */
  constructor(
      getViewportZoom: () => number,
      setBrowserZoomFunction: (zoom: number) => Promise<void>,
      initialZoom: number) {
    super(getViewportZoom, initialZoom);
    this.setBrowserZoomFunction_ = setBrowserZoomFunction;
  }

  onBrowserZoomChange(newZoom: number) {
    // If we are changing the browser zoom level, ignore any browser zoom level
    // change events. Either, the change occurred before our update and will be
    // overwritten, or the change being reported is the change we are making,
    // which we have already handled.
    if (this.changingBrowserZoom_) {
      return;
    }

    if (floatingPointEquals(this.browserZoom, newZoom)) {
      return;
    }

    this.browserZoom = newZoom;
    this.getEventTarget().dispatchEvent(
        new CustomEvent('set-zoom', {detail: newZoom}));
  }

  override onPdfZoomChange() {
    // If we are already changing the browser zoom level in response to a
    // previous extension-initiated zoom-level change, ignore this zoom change.
    // Once the browser zoom level is changed, we check whether the extension's
    // zoom level matches the most recently sent zoom level.
    if (this.changingBrowserZoom_) {
      return;
    }

    const viewportZoom = this.getViewportZoom();
    if (floatingPointEquals(this.browserZoom, viewportZoom)) {
      return;
    }

    this.changingBrowserZoom_ =
        this.setBrowserZoomFunction_(viewportZoom).then(() => {
          this.browserZoom = viewportZoom;
          this.changingBrowserZoom_ = null;

          // The extension's zoom level may have changed while the browser zoom
          // change was in progress. We call back into onPdfZoomChange to ensure
          // the browser zoom is up to date.
          this.onPdfZoomChange();
        });
  }

  /**
   * Combines the internal pdf zoom and the browser zoom to
   * produce the total zoom level for the viewer.
   * @param internalZoom the zoom level internal to the viewer.
   * @return the total zoom level.
   */
  override applyBrowserZoom(internalZoom: number): number {
    // The internal zoom and browser zoom are changed together, so the
    // browser zoom is already applied.
    return internalZoom;
  }

  /**
   * Given a zoom level, return the internal zoom level needed to
   * produce that zoom level.
   * @param totalZoom the total zoom level.
   * @return the zoom level internal to the viewer.
   */
  override internalZoomComponent(totalZoom: number): number {
    // The internal zoom and browser zoom are changed together, so the
    // internal zoom is the total zoom.
    return totalZoom;
  }
}

// Responds to changes in the browser zoom, but does not control the browser
// zoom.
class EmbeddedZoomManager extends ZoomManager {
  /**
   * Invoked when a browser-initiated zoom-level change occurs.
   * @param newZoom the new browser zoom level.
   */
  override onBrowserZoomChange(newZoom: number) {
    const oldZoom = this.browserZoom;
    this.browserZoom = newZoom;
    this.getEventTarget().dispatchEvent(
        new CustomEvent('update-zoom-from-browser', {detail: oldZoom}));
  }

  override onPdfZoomChange() {}
}