chromium/chrome/browser/resources/pdf/toolbar_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 {isRTL} from 'chrome://resources/js/util.js';

import type {ViewerZoomToolbarElement} from './elements/viewer_zoom_toolbar.js';

/**
 * Idle time in ms before the UI is hidden.
 */
const HIDE_TIMEOUT: number = 2000;

/**
 * Velocity required in a mousemove to reveal the UI (pixels/ms). This is
 * intended to be high enough that a fast flick of the mouse is required to
 * reach it.
 */
const SHOW_VELOCITY: number = 10;

/**
 * Distance from right of the screen required to reveal toolbars.
 */
const TOOLBAR_REVEAL_DISTANCE_RIGHT: number = 150;

/**
 * Distance from bottom of the screen required to reveal toolbars.
 */
const TOOLBAR_REVEAL_DISTANCE_BOTTOM: number = 250;

/**
 * @param e Event to test.
 * @param window Window to test against.
 * @return True if the mouse is close to the bottom-right of the screen.
 */
function isMouseNearToolbar(e: MouseEvent, window: Window): boolean {
  const atSide = isRTL() ?
      e.x > window.innerWidth - TOOLBAR_REVEAL_DISTANCE_RIGHT :
      e.x < TOOLBAR_REVEAL_DISTANCE_RIGHT;
  const atBottom = e.y > window.innerHeight - TOOLBAR_REVEAL_DISTANCE_BOTTOM;
  return atSide && atBottom;
}

// Responsible for showing and hiding the zoom toolbar.
export class ToolbarManager {
  private window_: Window;
  private zoomToolbar_: ViewerZoomToolbarElement;
  private toolbarTimeout_: number|null = null;
  private isMouseNearToolbar_: boolean = false;
  private keyboardNavigationActive_: boolean = false;
  private lastMovementTimestamp_: number|null = null;

  /**
   * @param window The window containing the UI.
   */
  constructor(window: Window, zoomToolbar: ViewerZoomToolbarElement) {
    this.window_ = window;
    this.zoomToolbar_ = zoomToolbar;

    document.addEventListener('mousemove', e => this.handleMouseMove_(e));
    document.addEventListener('mouseout', () => this.hideToolbarForMouseOut_());

    this.zoomToolbar_.addEventListener('keyboard-navigation-active', e => {
      this.keyboardNavigationActive_ = (e as CustomEvent).detail;
    });
  }

  private handleMouseMove_(e: MouseEvent) {
    this.isMouseNearToolbar_ = isMouseNearToolbar(e, this.window_);

    this.keyboardNavigationActive_ = false;

    const touchInteractionActive =
        e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents;

    // Tapping the screen with toolbars open tries to close them.
    if (touchInteractionActive && this.zoomToolbar_.isVisible()) {
      this.hideToolbarIfAllowed_();
      return;
    }

    // Show the toolbars if the mouse is near the top or bottom-right of the
    // screen, if the mouse moved fast, or if the touchscreen was tapped.
    if (this.isMouseNearToolbar_ || this.isHighVelocityMouseMove_(e) ||
        touchInteractionActive) {
      this.zoomToolbar_.show();
    }
    this.hideToolbarAfterTimeout();
  }

  /**
   * Whether a mousemove event is high enough velocity to reveal the toolbars.
   */
  private isHighVelocityMouseMove_(e: MouseEvent): boolean {
    if (e.type === 'mousemove') {
      if (this.lastMovementTimestamp_ == null) {
        this.lastMovementTimestamp_ = this.getCurrentTimestamp();
      } else {
        const movement =
            Math.sqrt(e.movementX * e.movementX + e.movementY * e.movementY);
        const newTime = this.getCurrentTimestamp();
        const interval = newTime - this.lastMovementTimestamp_;
        this.lastMovementTimestamp_ = newTime;

        if (interval !== 0) {
          return movement / interval > SHOW_VELOCITY;
        }
      }
    }
    return false;
  }

  /**
   * Wrapper around Date.now() to make it easily replaceable for testing.
   */
  getCurrentTimestamp(): number {
    return Date.now();
  }

  /**
   * Show toolbar and mark that navigation is being performed with
   * tab/shift-tab. This disables toolbar hiding until the mouse is moved or
   * escape is pressed.
   */
  showToolbarForKeyboardNavigation() {
    this.keyboardNavigationActive_ = true;
    this.zoomToolbar_.show();
  }

  /**
   * Hide toolbars after a delay, regardless of the position of the mouse.
   * Intended to be called when the mouse has moved out of the parent window.
   */
  private hideToolbarForMouseOut_() {
    this.isMouseNearToolbar_ = false;
    this.hideToolbarAfterTimeout();
  }

  /**
   * Check if the toolbar is able to be closed, and close it if it is.
   * Toolbar may be kept open based on mouse/keyboard activity and active
   * elements.
   */
  private hideToolbarIfAllowed_() {
    if (this.isMouseNearToolbar_ || this.keyboardNavigationActive_) {
      return;
    }

    // Remove focus to make any visible tooltips disappear -- otherwise they'll
    // still be visible on screen when the toolbar is off screen.
    if (document.activeElement === this.zoomToolbar_) {
      this.zoomToolbar_.blur();
    }

    this.zoomToolbar_.hide();
  }

  /** Hide the toolbar after the HIDE_TIMEOUT has elapsed. */
  hideToolbarAfterTimeout() {
    if (this.toolbarTimeout_) {
      this.window_.clearTimeout(this.toolbarTimeout_);
    }
    this.toolbarTimeout_ = this.window_.setTimeout(
        this.hideToolbarIfAllowed_.bind(this), HIDE_TIMEOUT);
  }

  /**
   * Clears the keyboard navigation state and hides the toolbars after a delay.
   */
  resetKeyboardNavigationAndHideToolbar() {
    this.keyboardNavigationActive_ = false;
    this.hideToolbarAfterTimeout();
  }
}