lib/capturer.js
'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 }
}
}