From 5a47f179e3cad7d68c2bf323372d035cc985c6e6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Mar 2017 22:53:26 -0700 Subject: [PATCH] Introduce synthetic scrolling We previously thought scroll events had changed somehow to become synchronous, but were wrong. This introduces synthetic scrolling where we use GPU translation of the contents of the gutter and scroll containers to simulate scrolling and explicitly capture mousewheel events. Still need to add dummy scrollbars and deal with their footprint in clientHeight and clientWidth. --- spec/text-editor-component-spec.js | 202 +++++------ src/text-editor-component.js | 524 ++++++++++++++--------------- src/text-editor-element.js | 2 +- 3 files changed, 351 insertions(+), 377 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 6bc8cb363..e96d5ecd9 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -37,7 +37,7 @@ describe('TextEditorComponent', () => { expect(element.querySelectorAll('.line-number').length).toBe(9) expect(element.querySelectorAll('.line').length).toBe(9) - component.refs.scroller.scrollTop = 5 * component.measurements.lineHeight + component.setScrollTop(5 * component.getLineHeight()) await component.getNextUpdatePromise() // After scrolling down beyond > 3 rows, the order of line numbers and lines @@ -58,7 +58,7 @@ describe('TextEditorComponent', () => { editor.lineTextForScreenRow(8) ]) - component.refs.scroller.scrollTop = 2.5 * component.measurements.lineHeight + component.setScrollTop(2.5 * component.getLineHeight()) await component.getNextUpdatePromise() expect(Array.from(element.querySelectorAll('.line-number')).map(element => element.textContent.trim())).toEqual([ '1', '2', '3', '4', '5', '6', '7', '8', '9' @@ -88,25 +88,24 @@ describe('TextEditorComponent', () => { it('honors the scrollPastEnd option by adding empty space equivalent to the clientHeight to the end of the content area', async () => { const {component, element, editor} = buildComponent({autoHeight: false, autoWidth: false}) - const {scroller} = component.refs + const {scrollContainer} = component.refs await editor.update({scrollPastEnd: true}) await setEditorHeightInLines(component, 6) // scroll to end - scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight + component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight) await component.getNextUpdatePromise() expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3) editor.update({scrollPastEnd: false}) await component.getNextUpdatePromise() // wait for scrollable content resize - await component.getNextUpdatePromise() // wait for async scroll event due to scrollbar shrinking expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 6) // Always allows at least 3 lines worth of overscroll if the editor is short await setEditorHeightInLines(component, 2) await editor.update({scrollPastEnd: true}) - scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight + component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight) await component.getNextUpdatePromise() expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1) }) @@ -125,18 +124,9 @@ describe('TextEditorComponent', () => { expect(gutterElement.firstChild.style.contain).toBe('strict') }) - it('translates the gutter so it is always visible when scrolling to the right', async () => { - const {component, element, editor} = buildComponent({width: 100}) - - expect(component.refs.gutterContainer.style.transform).toBe('translateX(0px)') - component.refs.scroller.scrollLeft = 100 - await component.getNextUpdatePromise() - expect(component.refs.gutterContainer.style.transform).toBe('translateX(100px)') - }) - it('renders cursors within the visible row range', async () => { const {component, element, editor} = buildComponent({height: 40, rowsPerTile: 2}) - component.refs.scroller.scrollTop = 100 + component.setScrollTop(100) await component.getNextUpdatePromise() expect(component.getRenderedStartRow()).toBe(4) @@ -180,8 +170,8 @@ describe('TextEditorComponent', () => { it('places the hidden input element at the location of the last cursor if it is visible', async () => { const {component, element, editor} = buildComponent({height: 60, width: 120, rowsPerTile: 2}) const {hiddenInput} = component.refs - component.refs.scroller.scrollTop = 100 - component.refs.scroller.scrollLeft = 40 + component.setScrollTop(100) + component.setScrollLeft(40) await component.getNextUpdatePromise() expect(component.getRenderedStartRow()).toBe(4) @@ -205,6 +195,7 @@ describe('TextEditorComponent', () => { jasmine.attachToDOM(element) expect(getBaseCharacterWidth(component)).toBe(55) + console.log('running expectation'); expect(lineNodeForScreenRow(component, 3).textContent).toBe( ' var pivot = items.shift(), current, left = [], ' ) @@ -220,8 +211,8 @@ describe('TextEditorComponent', () => { ' = [], right = [];' ) - const {scroller} = component.refs - expect(scroller.clientWidth).toBe(scroller.scrollWidth) + const {scrollContainer} = component.refs + expect(scrollContainer.clientWidth).toBe(scrollContainer.scrollWidth) }) it('decorates the line numbers of folded lines', async () => { @@ -231,29 +222,30 @@ describe('TextEditorComponent', () => { expect(lineNumberNodeForScreenRow(component, 1).classList.contains('folded')).toBe(true) }) - it('makes lines at least as wide as the scroller', async () => { + it('makes lines at least as wide as the scrollContainer', async () => { const {component, element, editor} = buildComponent() - const {scroller, gutterContainer} = component.refs + const {scrollContainer, gutterContainer} = component.refs editor.setText('a') await component.getNextUpdatePromise() - expect(element.querySelector('.line').offsetWidth).toBe(scroller.offsetWidth - gutterContainer.offsetWidth) + expect(element.querySelector('.line').offsetWidth).toBe(scrollContainer.offsetWidth) }) it('resizes based on the content when the autoHeight and/or autoWidth options are true', async () => { const {component, element, editor} = buildComponent({autoHeight: true, autoWidth: true}) + const {gutterContainer, scrollContainer} = component.refs const initialWidth = element.offsetWidth const initialHeight = element.offsetHeight - expect(initialWidth).toBe(component.refs.scroller.scrollWidth) - expect(initialHeight).toBe(component.refs.scroller.scrollHeight) + expect(initialWidth).toBe(gutterContainer.offsetWidth + scrollContainer.scrollWidth) + expect(initialHeight).toBe(scrollContainer.scrollHeight) editor.setCursorScreenPosition([6, Infinity]) editor.insertText('x'.repeat(50)) await component.getNextUpdatePromise() - expect(element.offsetWidth).toBe(component.refs.scroller.scrollWidth) + expect(element.offsetWidth).toBe(gutterContainer.offsetWidth + scrollContainer.scrollWidth) expect(element.offsetWidth).toBeGreaterThan(initialWidth) editor.insertText('\n'.repeat(5)) await component.getNextUpdatePromise() - expect(element.offsetHeight).toBe(component.refs.scroller.scrollHeight) + expect(element.offsetHeight).toBe(scrollContainer.scrollHeight) expect(element.offsetHeight).toBeGreaterThan(initialHeight) }) @@ -369,32 +361,29 @@ describe('TextEditorComponent', () => { describe('autoscroll on cursor movement', () => { it('automatically scrolls vertically when the requested range is within the vertical scroll margin of the top or bottom', async () => { - const {component, element, editor} = buildComponent({height: 120}) - const {scroller} = component.refs + const {component, editor} = buildComponent({height: 120}) expect(component.getLastVisibleRow()).toBe(8) editor.scrollToScreenRange([[4, 0], [6, 0]]) await component.getNextUpdatePromise() - let scrollBottom = scroller.scrollTop + scroller.clientHeight - expect(scrollBottom).toBe((6 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight) + expect(component.getScrollBottom()).toBe((6 + 1 + editor.verticalScrollMargin) * component.getLineHeight()) editor.scrollToScreenPosition([8, 0]) await component.getNextUpdatePromise() - scrollBottom = scroller.scrollTop + scroller.clientHeight - expect(scrollBottom).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight) + expect(component.getScrollBottom()).toBe((8 + 1 + editor.verticalScrollMargin) * component.measurements.lineHeight) editor.scrollToScreenPosition([3, 0]) await component.getNextUpdatePromise() - expect(scroller.scrollTop).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight) + expect(component.getScrollTop()).toBe((3 - editor.verticalScrollMargin) * component.measurements.lineHeight) editor.scrollToScreenPosition([2, 0]) await component.getNextUpdatePromise() - expect(scroller.scrollTop).toBe(0) + expect(component.getScrollTop()).toBe(0) }) it('does not vertically autoscroll by more than half of the visible lines if the editor is shorter than twice the scroll margin', async () => { const {component, element, editor} = buildComponent({autoHeight: false}) - const {scroller} = component.refs + const {scrollContainer} = component.refs element.style.height = 5.5 * component.measurements.lineHeight + 'px' await component.getNextUpdatePromise() expect(component.getLastVisibleRow()).toBe(6) @@ -402,26 +391,24 @@ describe('TextEditorComponent', () => { editor.scrollToScreenPosition([6, 0]) await component.getNextUpdatePromise() - let scrollBottom = scroller.scrollTop + scroller.clientHeight - expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) editor.scrollToScreenPosition([6, 4]) await component.getNextUpdatePromise() - scrollBottom = scroller.scrollTop + scroller.clientHeight - expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) editor.scrollToScreenRange([[4, 4], [6, 4]]) await component.getNextUpdatePromise() - expect(scroller.scrollTop).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight) + expect(component.getScrollTop()).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight) editor.scrollToScreenRange([[4, 4], [6, 4]], {reversed: false}) await component.getNextUpdatePromise() - expect(scrollBottom).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) + expect(component.getScrollBottom()).toBe((6 + 1 + scrollMarginInLines) * component.measurements.lineHeight) }) - it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the screen', async () => { + it('automatically scrolls horizontally when the requested range is within the horizontal scroll margin of the right edge of the gutter or right edge of the scroll container', async () => { const {component, element, editor} = buildComponent() - const {scroller} = component.refs + const {scrollContainer} = component.refs element.style.width = component.getGutterContainerWidth() + 3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px' @@ -429,32 +416,30 @@ describe('TextEditorComponent', () => { editor.scrollToScreenRange([[1, 12], [2, 28]]) await component.getNextUpdatePromise() - let expectedScrollLeft = Math.floor( + let expectedScrollLeft = Math.round( clientLeftForCharacter(component, 1, 12) - lineNodeForScreenRow(component, 1).getBoundingClientRect().left - (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) ) - expect(scroller.scrollLeft).toBe(expectedScrollLeft) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false}) await component.getNextUpdatePromise() - expectedScrollLeft = Math.floor( + expectedScrollLeft = Math.round( component.getGutterContainerWidth() + clientLeftForCharacter(component, 2, 28) - lineNodeForScreenRow(component, 2).getBoundingClientRect().left + (editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) - - scroller.clientWidth + scrollContainer.clientWidth ) - expect(scroller.scrollLeft).toBe(expectedScrollLeft) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) }) it('does not horizontally autoscroll by more than half of the visible "base-width" characters if the editor is narrower than twice the scroll margin', async () => { - const {component, element, editor} = buildComponent({autoHeight: false}) - const {scroller, gutterContainer} = component.refs + const {component, editor} = buildComponent({autoHeight: false}) await setEditorWidthInCharacters(component, 1.5 * editor.horizontalScrollMargin) - const contentWidth = scroller.clientWidth - gutterContainer.offsetWidth - const contentWidthInCharacters = Math.floor(contentWidth / component.measurements.baseCharacterWidth) + const contentWidthInCharacters = Math.floor(component.getScrollContainerClientWidth() / component.getBaseCharacterWidth()) expect(contentWidthInCharacters).toBe(9) editor.scrollToScreenRange([[6, 10], [6, 15]]) @@ -462,27 +447,26 @@ describe('TextEditorComponent', () => { let expectedScrollLeft = Math.floor( clientLeftForCharacter(component, 6, 10) - lineNodeForScreenRow(component, 1).getBoundingClientRect().left - - (4 * component.measurements.baseCharacterWidth) + (4 * component.getBaseCharacterWidth()) ) - expect(scroller.scrollLeft).toBe(expectedScrollLeft) + expect(component.getScrollLeft()).toBe(expectedScrollLeft) }) it('correctly autoscrolls after inserting a line that exceeds the current content width', async () => { const {component, element, editor} = buildComponent() - const {scroller} = component.refs - element.style.width = component.getGutterContainerWidth() + component.measurements.longestLineWidth + 'px' + element.style.width = component.getGutterContainerWidth() + component.getContentWidth() + 'px' await component.getNextUpdatePromise() editor.setCursorScreenPosition([0, Infinity]) editor.insertText('x'.repeat(100)) await component.getNextUpdatePromise() - expect(scroller.scrollLeft).toBe(component.getScrollWidth() - scroller.clientWidth) + expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth()) }) it('accounts for the presence of horizontal scrollbars that appear during the same frame as the autoscroll', async () => { const {component, element, editor} = buildComponent() - const {scroller} = component.refs + const {scrollContainer} = component.refs element.style.height = component.getScrollHeight() + 'px' element.style.width = component.getScrollWidth() + 'px' await component.getNextUpdatePromise() @@ -491,8 +475,8 @@ describe('TextEditorComponent', () => { editor.insertText('\n\n' + 'x'.repeat(100)) await component.getNextUpdatePromise() - expect(scroller.scrollTop).toBe(component.getScrollHeight() - scroller.clientHeight) - expect(scroller.scrollLeft).toBe(component.getScrollWidth() - scroller.clientWidth) + expect(component.getScrollTop()).toBe(component.getScrollHeight() - component.getScrollContainerClientHeight()) + expect(component.getScrollLeft()).toBe(component.getScrollWidth() - component.getScrollContainerClientWidth()) }) }) @@ -735,7 +719,7 @@ describe('TextEditorComponent', () => { ) // Don't flash on next update if another flash wasn't requested - component.refs.scroller.scrollTop = 100 + component.setScrollTop(100) await component.getNextUpdatePromise() expect(highlights[0].classList.contains('b')).toBe(false) expect(highlights[1].classList.contains('b')).toBe(false) @@ -1156,29 +1140,29 @@ describe('TextEditorComponent', () => { expect(editor.getCursorScreenPosition()).toEqual([0, 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 + it('autoscrolls the content when dragging near the edge of the scroll container', async () => { + const {component, element, editor} = buildComponent({width: 200, height: 200}) 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 + expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBeGreaterThan(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() } function assertScrolledUpAndLeft () { - expect(scroller.scrollTop).toBeLessThan(previousScrollTop) - previousScrollTop = scroller.scrollTop - expect(scroller.scrollLeft).toBeLessThan(previousScrollLeft) - previousScrollLeft = scroller.scrollLeft + expect(component.getScrollTop()).toBeLessThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBeLessThan(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() } component.didMouseDownOnContent({detail: 1, button: 0, clientX: 100, clientY: 100}) const {didDrag, didStopDragging} = component.handleMouseDragUntilMouseUp.argsForCall[0][0] + didDrag({clientX: 199, clientY: 199}) assertScrolledDownAndRight() didDrag({clientX: 199, clientY: 199}) @@ -1192,27 +1176,24 @@ describe('TextEditorComponent', () => { 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) + // Don't artificially update scroll position beyond possible values + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).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) + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) - const maxScrollTop = scroller.scrollHeight - scroller.clientHeight - const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth - scroller.scrollTop = maxScrollTop - scroller.scrollLeft = maxScrollLeft + const maxScrollTop = component.getMaxScrollTop() + const maxScrollLeft = component.getMaxScrollLeft() + component.setScrollTop(maxScrollTop) + component.setScrollLeft(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) + expect(component.getScrollTop()).toBe(maxScrollTop) + expect(component.getScrollLeft()).toBe(maxScrollLeft) }) }) @@ -1387,25 +1368,25 @@ describe('TextEditorComponent', () => { expect(editor.isFoldedAtScreenRow(1)).toBe(false) }) - it('autoscrolls the content when dragging near the edge of the screen', async () => { + it('autoscrolls when dragging near the top or bottom of the gutter', async () => { const {component, editor} = buildComponent({width: 200, height: 200}) - const {scroller} = component.refs + const {scrollContainer} = component.refs spyOn(component, 'handleMouseDragUntilMouseUp') let previousScrollTop = 0 let previousScrollLeft = 0 function assertScrolledDown () { - expect(scroller.scrollTop).toBeGreaterThan(previousScrollTop) - previousScrollTop = scroller.scrollTop - expect(scroller.scrollLeft).toBe(previousScrollLeft) - previousScrollLeft = scroller.scrollLeft + expect(component.getScrollTop()).toBeGreaterThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBe(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() } function assertScrolledUp () { - expect(scroller.scrollTop).toBeLessThan(previousScrollTop) - previousScrollTop = scroller.scrollTop - expect(scroller.scrollLeft).toBe(previousScrollLeft) - previousScrollLeft = scroller.scrollLeft + expect(component.getScrollTop()).toBeLessThan(previousScrollTop) + previousScrollTop = component.getScrollTop() + expect(component.getScrollLeft()).toBe(previousScrollLeft) + previousScrollLeft = component.getScrollLeft() } component.didMouseDownOnLineNumberGutter({detail: 1, button: 0, clientX: 0, clientY: 100}) @@ -1425,25 +1406,23 @@ describe('TextEditorComponent', () => { // Don't artificially update scroll measurements beyond the minimum or // maximum possible scroll positions - expect(scroller.scrollTop).toBe(0) - expect(scroller.scrollLeft).toBe(0) + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).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) + expect(component.getScrollTop()).toBe(0) + expect(component.getScrollLeft()).toBe(0) - const maxScrollTop = scroller.scrollHeight - scroller.clientHeight - const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth - scroller.scrollTop = maxScrollTop - scroller.scrollLeft = maxScrollLeft + const maxScrollTop = component.getMaxScrollTop() + const maxScrollLeft = component.getMaxScrollLeft() + component.setScrollTop(maxScrollTop) + component.setScrollLeft(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) + expect(component.getScrollTop()).toBe(maxScrollTop) + expect(component.getScrollLeft()).toBe(maxScrollLeft) }) }) }) @@ -1475,10 +1454,7 @@ function buildComponent (params = {}) { } function getBaseCharacterWidth (component) { - return Math.round( - (component.refs.scroller.clientWidth - component.getGutterContainerWidth()) / - component.measurements.baseCharacterWidth - ) + return Math.round(component.getScrollContainerWidth() / component.getBaseCharacterWidth()) } async function setEditorHeightInLines(component, heightInLines) { diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 3b09ba7d1..1016dcb7c 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -16,6 +16,7 @@ const KOREAN_CHARACTER = '세' const NBSP_CHARACTER = '\u00a0' const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff' const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40 +const MOUSE_WHEEL_SCROLL_SENSITIVITY = 0.8 function scaleMouseDragAutoscrollDelta (delta) { return Math.pow(delta / 3, 3) / 280 @@ -47,7 +48,8 @@ class TextEditorComponent { this.lineNodesByScreenLineId = new Map() this.textNodesByScreenLineId = new Map() this.pendingAutoscroll = null - this.autoscrollTop = null + this.scrollTop = 0 + this.scrollLeft = 0 this.previousScrollWidth = 0 this.previousScrollHeight = 0 this.lastKeydown = null @@ -97,7 +99,7 @@ class TextEditorComponent { } this.horizontalPositionsToMeasure.clear() - if (this.pendingAutoscroll) this.initiateAutoscroll() + if (this.pendingAutoscroll) this.autoscrollVertically() this.populateVisibleRowRange() const longestLineToMeasure = this.checkForNewLongestLine() this.queryScreenLinesToRender() @@ -111,19 +113,10 @@ class TextEditorComponent { etch.updateSync(this) - // If scrollHeight or scrollWidth changed, we may have shown or hidden - // scrollbars, affecting the clientWidth or clientHeight - if (this.checkIfScrollDimensionsChanged()) { - this.measureClientDimensions() - // If the clientHeight changed, our previous vertical autoscroll may have - // been off by the height of the horizontal scrollbar. If we *still* need - // to autoscroll, just re-render the frame. - if (this.pendingAutoscroll && this.initiateAutoscroll()) { - this.updateSync() - return - } + if (this.pendingAutoscroll) { + this.autoscrollHorizontally() + this.pendingAutoscroll = null } - if (this.pendingAutoscroll) this.finalizeAutoscroll() this.currentFrameLineNumberGutterProps = null } @@ -142,103 +135,82 @@ class TextEditorComponent { render () { const {model} = this.props - const style = { - overflow: 'hidden', - } + const style = {} if (!model.getAutoHeight() && !model.getAutoWidth()) { style.contain = 'strict' } - if (this.measurements) { if (model.getAutoHeight()) { - style.height = this.getScrollHeight() + 'px' + style.height = this.getContentHeight() + 'px' } if (model.getAutoWidth()) { - style.width = this.getScrollWidth() + 'px' + style.width = this.getGutterContainerWidth() + this.getContentWidth() + 'px' } } let attributes = null let className = 'editor' - if (this.focused) { - className += ' is-focused' - } + if (this.focused) className += ' is-focused' if (model.isMini()) { attributes = {mini: ''} className += ' mini' } - const scrollerOverflowX = (model.isMini() || model.isSoftWrapped()) ? 'hidden' : 'auto' - const scrollerOverflowY = model.isMini() ? 'hidden' : 'auto' - return $('atom-text-editor', { className, - attributes, style, + attributes, tabIndex: -1, on: { focus: this.didFocus, - blur: this.didBlur + blur: this.didBlur, + mousewheel: this.didMouseWheel } }, $.div( { + ref: 'clientContainer', style: { position: 'relative', + contain: 'strict', + overflow: 'hidden', + backgroundColor: 'inherit', width: '100%', - height: '100%', - backgroundColor: 'inherit' + height: '100%' } }, - $.div( - { - ref: 'scroller', - className: 'scroll-view', - on: {scroll: this.didScroll}, - style: { - position: 'absolute', - contain: 'strict', - top: 0, - right: 0, - bottom: 0, - left: 0, - overflowX: scrollerOverflowX, - overflowY: scrollerOverflowY, - backgroundColor: 'inherit' - } - }, - $.div( - { - style: { - isolate: 'content', - width: 'max-content', - height: 'max-content', - backgroundColor: 'inherit' - } - }, - this.renderGutterContainer(), - this.renderContent() - ) - ) + this.renderGutterContainer(), + this.renderScrollContainer() ) ) } renderGutterContainer () { if (this.props.model.isMini()) return null - const props = {ref: 'gutterContainer', className: 'gutter-container'} + const innerStyle = { + willChange: 'transform', + backgroundColor: 'inherit' + } if (this.measurements) { - props.style = { - position: 'relative', - willChange: 'transform', - transform: `translateX(${this.measurements.scrollLeft}px)`, - zIndex: 1 - } + innerStyle.transform = `translateY(${-this.getScrollTop()}px)` } - return $.div(props, this.renderLineNumberGutter()) + return $.div( + { + ref: 'gutterContainer', + className: 'gutter-container', + style: { + position: 'relative', + zIndex: 1, + backgroundColor: 'inherit' + } + }, + $.div({style: innerStyle}, + this.renderLineNumberGutter() + ) + ) } renderLineNumberGutter () { @@ -278,8 +250,8 @@ class TextEditorComponent { ref: 'lineNumberGutter', parentComponent: this, height: this.getScrollHeight(), - width: this.measurements.lineNumberGutterWidth, - lineHeight: this.measurements.lineHeight, + width: this.getLineNumberGutterWidth(), + lineHeight: this.getLineHeight(), startRow, endRow, rowsPerTile, maxLineNumberDigits, bufferRows, lineNumberDecorations, softWrappedFlags, foldableFlags @@ -301,6 +273,31 @@ class TextEditorComponent { } } + renderScrollContainer () { + const style = { + position: 'absolute', + contain: 'strict', + overflow: 'hidden', + top: 0, + bottom: 0, + backgroundColor: 'inherit' + } + + if (this.measurements) { + style.left = this.getGutterContainerWidth() + 'px' + style.width = this.getScrollContainerWidth() + 'px' + } + + return $.div( + { + ref: 'scrollContainer', + className: 'scroll-view', + style + }, + this.renderContent() + ) + } + renderContent () { let children let style = { @@ -309,15 +306,13 @@ class TextEditorComponent { backgroundColor: 'inherit' } if (this.measurements) { - const contentWidth = this.getContentWidth() - const scrollHeight = this.getScrollHeight() - const width = contentWidth + 'px' - const height = scrollHeight + 'px' - style.width = width - style.height = height + style.width = this.getScrollWidth() + 'px' + style.height = this.getScrollHeight() + 'px' + style.willChange = 'transform' + style.transform = `translate(${-this.getScrollLeft()}px, ${-this.getScrollTop()}px)` children = [ - this.renderCursorsAndInput(width, height), - this.renderLineTiles(width, height), + this.renderCursorsAndInput(), + this.renderLineTiles(), this.renderPlaceholderText() ] } else { @@ -339,7 +334,7 @@ class TextEditorComponent { ) } - renderLineTiles (width, height) { + renderLineTiles () { if (!this.measurements) return [] const {lineNodesByScreenLineId, textNodesByScreenLineId} = this @@ -347,8 +342,8 @@ class TextEditorComponent { const startRow = this.getRenderedStartRow() const endRow = this.getRenderedEndRow() const rowsPerTile = this.getRowsPerTile() - const tileHeight = this.measurements.lineHeight * rowsPerTile - const tileWidth = this.getContentWidth() + const tileHeight = this.getLineHeight() * rowsPerTile + const tileWidth = this.getScrollWidth() const displayLayer = this.props.model.displayLayer const tileNodes = new Array(this.getRenderedTileCount()) @@ -368,7 +363,7 @@ class TextEditorComponent { height: tileHeight, width: tileWidth, top: this.topPixelPositionForRow(tileStartRow), - lineHeight: this.measurements.lineHeight, + lineHeight: this.getLineHeight(), screenLines: this.renderedScreenLines.slice(tileStartRow - startRow, tileEndRow - startRow), lineDecorations, highlightDecorations, @@ -395,14 +390,16 @@ class TextEditorComponent { style: { position: 'absolute', contain: 'strict', - width, height, + overflow: 'hidden', + width: this.getScrollWidth() + 'px', + height: this.getScrollHeight() + 'px', backgroundColor: 'inherit' } }, tileNodes) } - renderCursorsAndInput (width, height) { - const cursorHeight = this.measurements.lineHeight + 'px' + renderCursorsAndInput () { + const cursorHeight = this.getLineHeight() + 'px' const children = [this.renderHiddenInput()] @@ -425,7 +422,8 @@ class TextEditorComponent { position: 'absolute', contain: 'strict', zIndex: 1, - width, height + width: this.getScrollWidth() + 'px', + height: this.getScrollHeight() + 'px' } }, children) } @@ -470,7 +468,7 @@ class TextEditorComponent { style: { position: 'absolute', width: '1px', - height: this.measurements.lineHeight + 'px', + height: this.getLineHeight() + 'px', top: top + 'px', left: left + 'px', opacity: 0, @@ -646,7 +644,7 @@ class TextEditorComponent { updateCursorsToRender () { this.decorationsToRender.cursors.length = 0 - const height = this.measurements.lineHeight + 'px' + const height = this.getLineHeight() + 'px' for (let i = 0; i < this.decorationsToMeasure.cursors.length; i++) { const cursor = this.decorationsToMeasure.cursors[i] const {row, column} = cursor.screenPosition @@ -765,15 +763,20 @@ class TextEditorComponent { } } - didScroll () { - if (this.measureScrollPosition(true)) { - this.updateSync() - } + didMouseWheel (eveWt) { + let {deltaX, deltaY} = event + deltaX = deltaX * MOUSE_WHEEL_SCROLL_SENSITIVITY + deltaY = deltaY * MOUSE_WHEEL_SCROLL_SENSITIVITY + + const scrollPositionChanged = + this.setScrollLeft(this.getScrollLeft() + deltaX) || + this.setScrollTop(this.getScrollTop() + deltaY) + + if (scrollPositionChanged) this.updateSync() } didResize () { - if (this.measureEditorDimensions()) { - this.measureClientDimensions() + if (this.measureClientContainerDimensions()) { this.scheduleUpdate() } } @@ -1023,10 +1026,10 @@ class TextEditorComponent { } autoscrollOnMouseDrag ({clientX, clientY}, verticalOnly = false) { - let {top, bottom, left, right} = this.refs.scroller.getBoundingClientRect() + let {top, bottom, left, right} = this.refs.scrollContainer.getBoundingClientRect() top += MOUSE_DRAG_AUTOSCROLL_MARGIN bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN - left += this.getGutterContainerWidth() + MOUSE_DRAG_AUTOSCROLL_MARGIN + left += MOUSE_DRAG_AUTOSCROLL_MARGIN right -= MOUSE_DRAG_AUTOSCROLL_MARGIN let yDelta, yDirection @@ -1050,31 +1053,21 @@ class TextEditorComponent { 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 = newScrollTop - this.refs.scroller.scrollTop = newScrollTop - scrolled = true - } + scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta) } if (!verticalOnly && xDelta != null) { const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection - const newScrollLeft = this.constrainScrollLeft(this.measurements.scrollLeft + scaledDelta) - if (newScrollLeft !== this.measurements.scrollLeft) { - this.measurements.scrollLeft = newScrollLeft - this.refs.scroller.scrollLeft = newScrollLeft - scrolled = true - } + scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta) } 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 scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect() + clientX = Math.min(scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX)) + clientY = Math.min(scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY)) const linesRect = this.refs.lineTiles.getBoundingClientRect() return this.screenPositionForPixelPosition({ top: clientY - linesRect.top, @@ -1087,12 +1080,12 @@ class TextEditorComponent { this.scheduleUpdate() } - initiateAutoscroll () { + autoscrollVertically () { const {screenRange, options} = this.pendingAutoscroll const screenRangeTop = this.pixelTopForRow(screenRange.start.row) - const screenRangeBottom = this.pixelTopForRow(screenRange.end.row) + this.measurements.lineHeight - const verticalScrollMargin = this.getVerticalScrollMargin() + const screenRangeBottom = this.pixelTopForRow(screenRange.end.row) + this.getLineHeight() + const verticalScrollMargin = this.getVerticalAutoscrollMargin() this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) @@ -1109,43 +1102,27 @@ class TextEditorComponent { desiredScrollBottom = screenRangeBottom + verticalScrollMargin } - if (desiredScrollTop != null) { - desiredScrollTop = this.constrainScrollTop(desiredScrollTop) - } - - if (desiredScrollBottom != null) { - desiredScrollBottom = this.constrainScrollTop(desiredScrollBottom - this.getClientHeight()) + this.getClientHeight() - } - if (!options || options.reversed !== false) { if (desiredScrollBottom > this.getScrollBottom()) { - this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight - this.measurements.scrollTop = this.autoscrollTop - return true + return this.setScrollBottom(desiredScrollBottom, true) } if (desiredScrollTop < this.getScrollTop()) { - this.autoscrollTop = desiredScrollTop - this.measurements.scrollTop = this.autoscrollTop - return true + return this.setScrollTop(desiredScrollTop, true) } } else { if (desiredScrollTop < this.getScrollTop()) { - this.autoscrollTop = desiredScrollTop - this.measurements.scrollTop = this.autoscrollTop - return true + return this.setScrollTop(desiredScrollTop, true) } if (desiredScrollBottom > this.getScrollBottom()) { - this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight - this.measurements.scrollTop = this.autoscrollTop - return true + return this.setScrollBottom(desiredScrollBottom, true) } } return false } - finalizeAutoscroll () { - const horizontalScrollMargin = this.getHorizontalScrollMargin() + autoscrollHorizontally () { + const horizontalScrollMargin = this.getHorizontalAutoscrollMargin() const {screenRange, options} = this.pendingAutoscroll const gutterContainerWidth = this.getGutterContainerWidth() @@ -1154,121 +1131,70 @@ class TextEditorComponent { const desiredScrollLeft = Math.max(0, left - horizontalScrollMargin - gutterContainerWidth) const desiredScrollRight = Math.min(this.getScrollWidth(), right + horizontalScrollMargin) - let autoscrollLeft if (!options || options.reversed !== false) { if (desiredScrollRight > this.getScrollRight()) { - autoscrollLeft = desiredScrollRight - this.getClientWidth() - this.measurements.scrollLeft = autoscrollLeft + this.setScrollRight(desiredScrollRight, true) } if (desiredScrollLeft < this.getScrollLeft()) { - autoscrollLeft = desiredScrollLeft - this.measurements.scrollLeft = autoscrollLeft + this.setScrollLeft(desiredScrollLeft, true) } } else { if (desiredScrollLeft < this.getScrollLeft()) { - autoscrollLeft = desiredScrollLeft - this.measurements.scrollLeft = autoscrollLeft + this.setScrollLeft(desiredScrollLeft, true) } if (desiredScrollRight > this.getScrollRight()) { - autoscrollLeft = desiredScrollRight - this.getClientWidth() - this.measurements.scrollLeft = autoscrollLeft + this.setScrollRight(desiredScrollRight, true) } } - - if (this.autoscrollTop != null) { - this.refs.scroller.scrollTop = this.autoscrollTop - this.autoscrollTop = null - } - - if (autoscrollLeft != null) { - this.refs.scroller.scrollLeft = autoscrollLeft - } - - this.pendingAutoscroll = null } - getVerticalScrollMargin () { - const {clientHeight, lineHeight} = this.measurements + getVerticalAutoscrollMargin () { + const maxMarginInLines = Math.floor( + (this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2 + ) const marginInLines = Math.min( this.props.model.verticalScrollMargin, - Math.floor(((clientHeight / lineHeight) - 1) / 2) + maxMarginInLines ) - return marginInLines * lineHeight + return marginInLines * this.getLineHeight() } - getHorizontalScrollMargin () { - const {clientWidth, baseCharacterWidth} = this.measurements - const contentClientWidth = clientWidth - this.getGutterContainerWidth() + getHorizontalAutoscrollMargin () { + const maxMarginInBaseCharacters = Math.floor( + (this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2 + ) const marginInBaseCharacters = Math.min( this.props.model.horizontalScrollMargin, - Math.floor(((contentClientWidth / baseCharacterWidth) - 1) / 2) - ) - 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()) + maxMarginInBaseCharacters ) + return marginInBaseCharacters * this.getBaseCharacterWidth() } performInitialMeasurements () { this.measurements = {} - this.measureGutterDimensions() - this.measureEditorDimensions() - this.measureClientDimensions() - this.measureScrollPosition() this.measureCharacterDimensions() + this.measureGutterDimensions() + this.measureClientContainerDimensions() } - measureEditorDimensions () { + measureClientContainerDimensions () { if (!this.measurements) return false let dimensionsChanged = false - const scrollerHeight = this.refs.scroller.offsetHeight - const scrollerWidth = this.refs.scroller.offsetWidth - if (scrollerHeight !== this.measurements.scrollerHeight) { - this.measurements.scrollerHeight = scrollerHeight + const clientContainerHeight = this.refs.clientContainer.offsetHeight + const clientContainerWidth = this.refs.clientContainer.offsetWidth + if (clientContainerHeight !== this.measurements.clientContainerHeight) { + this.measurements.clientContainerHeight = clientContainerHeight dimensionsChanged = true } - if (scrollerWidth !== this.measurements.scrollerWidth) { - this.measurements.scrollerWidth = scrollerWidth + if (clientContainerWidth !== this.measurements.clientContainerWidth) { + this.measurements.clientContainerWidth = clientContainerWidth + this.props.model.setEditorWidthInChars(this.getScrollContainerWidth() / this.getBaseCharacterWidth()) dimensionsChanged = true } return dimensionsChanged } - measureScrollPosition () { - let scrollPositionChanged = false - const {scrollTop, scrollLeft} = this.refs.scroller - if (scrollTop !== this.measurements.scrollTop) { - this.measurements.scrollTop = scrollTop - scrollPositionChanged = true - } - if (scrollLeft !== this.measurements.scrollLeft) { - this.measurements.scrollLeft = scrollLeft - scrollPositionChanged = true - } - return scrollPositionChanged - } - - measureClientDimensions () { - const {clientHeight, clientWidth} = this.refs.scroller - if (clientHeight !== this.measurements.clientHeight) { - this.measurements.clientHeight = clientHeight - } - if (clientWidth !== this.measurements.clientWidth) { - this.measurements.clientWidth = clientWidth - this.props.model.setWidth(clientWidth - this.getGutterContainerWidth(), true) - } - } - measureCharacterDimensions () { this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width @@ -1383,7 +1309,7 @@ class TextEditorComponent { } pixelTopForRow (row) { - return row * this.measurements.lineHeight + return row * this.getLineHeight() } pixelLeftForRowAndColumn (row, column) { @@ -1478,73 +1404,91 @@ class TextEditorComponent { return this.element.offsetWidth > 0 || this.element.offsetHeight > 0 } + getLineHeight () { + return this.measurements.lineHeight + } + getBaseCharacterWidth () { return this.measurements ? this.measurements.baseCharacterWidth : null } - getScrollTop () { - if (this.measurements != null) { - return this.measurements.scrollTop + getLongestLineWidth () { + return this.measurements.longestLineWidth + } + + getClientContainerHeight () { + return this.measurements.clientContainerHeight + } + + getClientContainerWidth () { + return this.measurements.clientContainerWidth + } + + getScrollContainerWidth () { + if (this.props.model.getAutoWidth()) { + return this.getScrollWidth() + } else { + return this.getClientContainerWidth() - this.getGutterContainerWidth() } } - getScrollBottom () { - return this.measurements - ? this.measurements.scrollTop + this.measurements.clientHeight - : null + getScrollContainerHeight () { + if (this.props.model.getAutoHeight()) { + return this.getScrollHeight() + } else { + return this.getClientContainerHeight() + } } - getScrollLeft () { - return this.measurements ? this.measurements.scrollLeft : null + getScrollContainerHeightInLines () { + return Math.ceil(this.getScrollContainerHeight() / this.getLineHeight()) } - getScrollRight () { - return this.measurements - ? this.measurements.scrollLeft + this.measurements.clientWidth - : null + getScrollContainerClientWidth () { + return this.getScrollContainerWidth() + } + + getScrollContainerClientHeight () { + return this.getScrollContainerHeight() } getScrollHeight () { - const {model} = this.props - const contentHeight = model.getApproximateScreenLineCount() * this.measurements.lineHeight - if (model.getScrollPastEnd()) { - const extraScrollHeight = Math.max( - 3 * this.measurements.lineHeight, - this.getClientHeight() - 3 * this.measurements.lineHeight + if (this.props.model.getScrollPastEnd()) { + return this.getContentHeight() + Math.max( + 3 * this.getLineHeight(), + this.getScrollContainerClientHeight() - (3 * this.getLineHeight()) ) - return contentHeight + extraScrollHeight } else { - return contentHeight + return this.getContentHeight() } } getScrollWidth () { - return this.getContentWidth() + this.getGutterContainerWidth() + const {model} = this.props + + if (model.isSoftWrapped()) { + return this.getScrollContainerClientWidth() + } else if (model.getAutoWidth()) { + return this.getContentWidth() + } else { + return Math.max(this.getContentWidth(), this.getScrollContainerClientWidth()) + } } - getClientHeight () { - return this.measurements.clientHeight - } - - getClientWidth () { - return this.measurements.clientWidth - } - - getGutterContainerWidth () { - return this.measurements.lineNumberGutterWidth + getContentHeight () { + return this.props.model.getApproximateScreenLineCount() * this.getLineHeight() } getContentWidth () { - if (this.props.model.isSoftWrapped()) { - return this.getClientWidth() - this.getGutterContainerWidth() - } else if (this.props.model.getAutoWidth()) { - return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth) - } else { - return Math.max( - Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth), - this.measurements.scrollerWidth - this.getGutterContainerWidth() - ) - } + return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth()) + } + + getGutterContainerWidth () { + return this.getLineNumberGutterWidth() + } + + getLineNumberGutterWidth () { + return this.measurements.lineNumberGutterWidth } getRowsPerTile () { @@ -1583,16 +1527,13 @@ class TextEditorComponent { } getFirstVisibleRow () { - const scrollTop = this.getScrollTop() - const lineHeight = this.measurements.lineHeight - return Math.floor(scrollTop / lineHeight) + return Math.floor(this.getScrollTop() / this.getLineHeight()) } getLastVisibleRow () { - const {scrollerHeight, lineHeight} = this.measurements return Math.min( this.props.model.getApproximateScreenLineCount() - 1, - this.getFirstVisibleRow() + Math.ceil(scrollerHeight / lineHeight) + this.getFirstVisibleRow() + this.getScrollContainerHeightInLines() ) } @@ -1600,6 +1541,63 @@ class TextEditorComponent { return Math.floor((this.getLastVisibleRow() - this.getFirstVisibleRow()) / this.getRowsPerTile()) + 2 } + + getScrollTop () { + this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop) + return this.scrollTop + } + + setScrollTop (scrollTop, suppressUpdate = false) { + scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop))) + if (scrollTop !== this.scrollTop) { + this.scrollTop = scrollTop + if (!suppressUpdate) this.scheduleUpdate() + return true + } else { + return false + } + } + + getMaxScrollTop () { + return Math.max(0, this.getScrollHeight() - this.getScrollContainerClientHeight()) + } + + getScrollBottom () { + return this.getScrollTop() + this.getScrollContainerClientHeight() + } + + setScrollBottom (scrollBottom, suppressUpdate = false) { + return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight(), suppressUpdate) + } + + getScrollLeft () { + // this.scrollLeft = Math.min(this.getMaxScrollLeft(), this.scrollLeft) + return this.scrollLeft + } + + setScrollLeft (scrollLeft, suppressUpdate = false) { + scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft))) + if (scrollLeft !== this.scrollLeft) { + this.scrollLeft = scrollLeft + if (!suppressUpdate) this.scheduleUpdate() + return true + } else { + return false + } + } + + getMaxScrollLeft () { + return Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth()) + } + + getScrollRight () { + return this.getScrollLeft() + this.getScrollContainerClientWidth() + } + + setScrollRight (scrollRight, suppressUpdate = false) { + return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth(), suppressUpdate) + } + // Ensure the spatial index is populated with rows that are currently // visible so we *at least* get the longest row in the visible range. populateVisibleRowRange () { @@ -1608,7 +1606,7 @@ class TextEditorComponent { } topPixelPositionForRow (row) { - return row * this.measurements.lineHeight + return row * this.getLineHeight() } getNextUpdatePromise () { @@ -1829,7 +1827,7 @@ class LinesTileComponent { position: 'absolute', contain: 'strict', height: height + 'px', - width: width + 'px', + width: width + 'px' } }, children) } diff --git a/src/text-editor-element.js b/src/text-editor-element.js index 38bedfe0e..eb64e5fa7 100644 --- a/src/text-editor-element.js +++ b/src/text-editor-element.js @@ -18,7 +18,7 @@ class TextEditorElement extends HTMLElement { } getModel () { - return this.getComponent().getModel() + return this.getComponent().props.model } setModel (model) {