meyfa/atom-screenshot

View on GitHub
lib/capturer.js

Summary

Maintainability
A
0 mins
Test Coverage
'use babel'

import CaptureResult from './capture-result'
import stitch from './stitch'
import asyncPoll from './util/async-poll'

import config from './config'

/**
 * Interval between scroll position checks, in milliseconds.
 *
 * @type {number}
 */
const SCROLL_INTERVAL = 50

/**
 * Timeout for scrolling operations, in milliseconds.
 *
 * @type {number}
 */
const SCROLL_TIMEOUT = 1000

/**
 * Instances of this class are used to screen-capture ranges in editor panes.
 */
export default class Capturer {
  /**
   * @param {object} window The electron remote window.
   * @param {object} editor The editor pane from which to capture.
   */
  constructor (window, editor) {
    this.window = window
    this.editor = editor
  }

  /**
   * Capture a subset of the whole editor range.
   *
   * @param {object} range The range object, i.e. start/end dictionary.
   * @returns {Promise} Resolves to the capture result.
   */
  async captureRange (range) {
    const captures = []

    let next = range.start
    while (next <= range.end) {
      const result = await this.captureStartingAt(next, range.end)
      captures.push(result)
      next = result.lastLine + 1
    }

    return stitch(captures)
  }

  /**
   * Capture everything the editor has to offer, starting at the given line,
   * and at most up to the limit line.
   *
   * @param {number} start The first line that should be on the capture.
   * @param {number} limit The maximum, after which no more lines may be captured.
   * @returns {Promise} Resolves to the capture result.
   */
  async captureStartingAt (start, limit) {
    await this.scrollToPosition(start)

    if (!config.showWrapGuide) {
      this.setWrapGuideVisibility(false)
    }

    let last = Math.min(limit, this.editor.getLastVisibleScreenRow())
    if (start < limit) {
      // this is necessary because until the last line is definitely
      // reached, it is sometimes covered by the bottom bar
      last -= 1
    }

    const rect = this.computeCaptureRect(start, last)
    const img = await this.window.capturePage(rect)

    if (!config.showWrapGuide) {
      this.setWrapGuideVisibility(true)
    }

    return new CaptureResult(img, start, last)
  }

  /**
   * Show or hide the wrap guide.
   *
   * @param {boolean} visible Whether the wrap guide should be visible.
   * @returns {void}
   */
  setWrapGuideVisibility (visible) {
    const wrapGuide = this.editor.element.querySelector('.wrap-guide')
    if (wrapGuide) {
      wrapGuide.style.visibility = visible ? '' : 'hidden'
    }
  }

  /**
   * Obtain the bounding box of the given line in the viewport.
   *
   * @param {number} lineNumber The line number.
   * @returns {object} The bounds (left, right, top, bottom, width, height).
   */
  getLineBounds (lineNumber) {
    const selector = '.line[data-screen-row="' + lineNumber + '"]'
    const element = this.editor.element.querySelector(selector)

    return element.getBoundingClientRect()
  }

  /**
   * Scroll the editor to the wanted position.
   *
   * @param {number} position The line number that should be at the top.
   * @returns {Promise} Resolves when done, rejects on timeout.
   */
  async scrollToPosition (position) {
    this.editor.scrollToScreenPosition([position, 0])

    await asyncPoll(() => {
      return this.editor.getFirstVisibleScreenRow() <= position
    }, { interval: SCROLL_INTERVAL, timeout: SCROLL_TIMEOUT })
  }

  /**
   * Compute the rect object that should be captured so that it contains all
   * lines from start up to and including last.
   *
   * @param {number} start The first line to include in the capture.
   * @param {number} last The second line to include in the capture.
   * @returns {object} The capture rect (x, y, width, height).
   */
  computeCaptureRect (start, last) {
    // overall bounds
    const bounds = this.editor.element.getBoundingClientRect()

    // limit bounds to left of vertical scrollbar
    const scrollbar = this.editor.element.querySelector('.vertical-scrollbar')
    let scrollbarOffset = 0
    if (scrollbar) {
      const scrollbarBounds = scrollbar.getBoundingClientRect()
      scrollbarOffset = bounds.right - scrollbarBounds.left
    }

    // line bounds
    const startBounds = this.getLineBounds(start)
    const lastBounds = this.getLineBounds(last)

    const includeLineNumbersAndGutter = config.includeLineNumbersAndGutter
    const gutterWidth = Math.max(0, lastBounds.left - bounds.left)

    const x = Math.ceil(includeLineNumbersAndGutter ? bounds.left : lastBounds.left)
    const y = Math.ceil(startBounds.top)
    const width = Math.floor(includeLineNumbersAndGutter
      ? bounds.width - scrollbarOffset
      : Math.min(bounds.width - scrollbarOffset - gutterWidth, lastBounds.width))
    const height = Math.floor(lastBounds.bottom - startBounds.top)

    return { x, y, width, height }
  }
}