diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 572c42f1f..0366c7415 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -901,6 +901,65 @@ describe('TextEditorComponent', () => { didDrag(clientPositionForCharacter(component, 4, 10)) expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [5, 0]]) }) + + it('autoscrolls the content when dragging near the edge of the screen', async () => { + const {component, editor} = buildComponent({width: 200, height: 200}) + const {scroller} = component.refs + spyOn(component, 'handleMouseDragUntilMouseUp') + + let previousScrollTop = 0 + let previousScrollLeft = 0 + function assertScrolledDownAndRight () { + expect(scroller.scrollTop).toBeGreaterThan(previousScrollTop) + previousScrollTop = scroller.scrollTop + expect(scroller.scrollLeft).toBeGreaterThan(previousScrollLeft) + previousScrollLeft = scroller.scrollLeft + } + + function assertScrolledUpAndLeft () { + expect(scroller.scrollTop).toBeLessThan(previousScrollTop) + previousScrollTop = scroller.scrollTop + expect(scroller.scrollLeft).toBeLessThan(previousScrollLeft) + previousScrollLeft = scroller.scrollLeft + } + + component.didMouseDownOnContent({detail: 1, clientX: 100, clientY: 100}) + const [didDrag, didStopDragging] = component.handleMouseDragUntilMouseUp.argsForCall[0] + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: 199, clientY: 199}) + assertScrolledDownAndRight() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + assertScrolledUpAndLeft() + + // Don't artificially update scroll measurements beyond the minimum or + // maximum possible scroll positions + expect(scroller.scrollTop).toBe(0) + expect(scroller.scrollLeft).toBe(0) + didDrag({clientX: component.getGutterContainerWidth() + 1, clientY: 1}) + expect(component.measurements.scrollTop).toBe(0) + expect(scroller.scrollTop).toBe(0) + expect(component.measurements.scrollLeft).toBe(0) + expect(scroller.scrollLeft).toBe(0) + + const maxScrollTop = scroller.scrollHeight - scroller.clientHeight + const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth + scroller.scrollTop = maxScrollTop + scroller.scrollLeft = maxScrollLeft + await component.getNextUpdatePromise() + + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + didDrag({clientX: 199, clientY: 199}) + expect(component.measurements.scrollTop).toBe(maxScrollTop) + expect(component.measurements.scrollLeft).toBe(maxScrollLeft) + }) }) }) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 02e49ecba..0a86629b0 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -14,6 +14,11 @@ const DOUBLE_WIDTH_CHARACTER = '我' const HALF_WIDTH_CHARACTER = 'ハ' const KOREAN_CHARACTER = '세' const NBSP_CHARACTER = '\u00a0' +const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40 + +function scaleMouseDragAutoscrollDelta (delta) { + return Math.pow(delta / 3, 3) / 280 +} module.exports = class TextEditorComponent { @@ -789,6 +794,7 @@ class TextEditorComponent { this.handleMouseDragUntilMouseUp( (event) => { + this.autoscrollOnMouseDrag(event) const screenPosition = this.screenPositionForMouseEvent(event) model.selectToScreenPosition(screenPosition, {suppressSelectionMerge: true, autoscroll: false}) this.updateSync() @@ -835,7 +841,59 @@ class TextEditorComponent { window.addEventListener('mouseup', didMouseUp) } + autoscrollOnMouseDrag ({clientX, clientY}) { + let {top, bottom, left, right} = this.refs.scroller.getBoundingClientRect() + top += MOUSE_DRAG_AUTOSCROLL_MARGIN + bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN + left += this.getGutterContainerWidth() + MOUSE_DRAG_AUTOSCROLL_MARGIN + right -= MOUSE_DRAG_AUTOSCROLL_MARGIN + + let yDelta, yDirection + if (clientY < top) { + yDelta = top - clientY + yDirection = -1 + } else if (clientY > bottom) { + yDelta = clientY - bottom + yDirection = 1 + } + + let xDelta, xDirection + if (clientX < left) { + xDelta = left - clientX + xDirection = -1 + } else if (clientX > right) { + xDelta = clientX - right + xDirection = 1 + } + + let scrolled = false + if (yDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection + const newScrollTop = this.constrainScrollTop(this.measurements.scrollTop + scaledDelta) + if (newScrollTop !== this.measurements.scrollTop) { + this.measurements.scrollTop += scaledDelta + this.refs.scroller.scrollTop += scaledDelta + scrolled = true + } + } + + if (xDelta != null) { + const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection + const newScrollLeft = this.constrainScrollLeft(this.measurements.scrollLeft + scaledDelta) + if (newScrollLeft !== this.measurements.scrollLeft) { + this.measurements.scrollLeft += scaledDelta + this.refs.scroller.scrollLeft += scaledDelta + scrolled = true + } + } + + if (scrolled) this.updateSync() + } + screenPositionForMouseEvent ({clientX, clientY}) { + const scrollerRect = this.refs.scroller.getBoundingClientRect() + clientX = Math.min(scrollerRect.right, Math.max(scrollerRect.left, clientX)) + clientY = Math.min(scrollerRect.bottom, Math.max(scrollerRect.top, clientY)) const linesRect = this.refs.lineTiles.getBoundingClientRect() return this.screenPositionForPixelPosition({ top: clientY - linesRect.top, @@ -871,11 +929,11 @@ class TextEditorComponent { } if (desiredScrollTop != null) { - desiredScrollTop = Math.max(0, Math.min(desiredScrollTop, this.getScrollHeight() - this.getClientHeight())) + desiredScrollTop = this.constrainScrollTop(desiredScrollTop) } if (desiredScrollBottom != null) { - desiredScrollBottom = Math.max(this.getClientHeight(), Math.min(desiredScrollBottom, this.getScrollHeight())) + desiredScrollBottom = this.constrainScrollTop(desiredScrollBottom - this.getClientHeight()) + this.getClientHeight() } if (!options || options.reversed !== false) { @@ -961,6 +1019,18 @@ class TextEditorComponent { return marginInBaseCharacters * baseCharacterWidth } + constrainScrollTop (desiredScrollTop) { + return Math.max( + 0, Math.min(desiredScrollTop, this.getScrollHeight() - this.getClientHeight()) + ) + } + + constrainScrollLeft (desiredScrollLeft) { + return Math.max( + 0, Math.min(desiredScrollLeft, this.getScrollWidth() - this.getClientWidth()) + ) + } + performInitialMeasurements () { this.measurements = {} this.measureGutterDimensions()