Add horizontal autoscroll

This commit is contained in:
Nathan Sobo
2017-03-02 14:59:41 -07:00
committed by Antonio Scandurra
parent 2ef29dee88
commit 3e87f9f889
3 changed files with 148 additions and 23 deletions

View File

@@ -273,6 +273,57 @@ describe('TextEditorComponent', () => {
await component.getNextUpdatePromise()
expect(scroller.scrollTop).toBe((4 - scrollMarginInLines) * component.measurements.lineHeight)
})
it('automatically scrolls horizontally when the cursor is within horizontal scroll margin of the right edge of the gutter or right edge of the screen', async () => {
const {component, element, editor} = buildComponent()
const {scroller} = component.refs
element.style.width =
component.getGutterContainerWidth() +
3 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px'
await component.getNextUpdatePromise()
editor.scrollToScreenRange([[1, 12], [2, 28]])
await component.getNextUpdatePromise()
let expectedScrollLeft = Math.floor(
clientLeftForCharacter(component, 1, 12) -
lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth)
)
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
editor.scrollToScreenRange([[1, 12], [2, 28]], {reversed: false})
await component.getNextUpdatePromise()
expectedScrollLeft = Math.floor(
component.getGutterContainerWidth() +
clientLeftForCharacter(component, 2, 28) -
lineNodeForScreenRow(component, 2).getBoundingClientRect().left +
(editor.horizontalScrollMargin * component.measurements.baseCharacterWidth) -
scroller.clientWidth
)
expect(scroller.scrollLeft).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()
const {scroller, gutterContainer} = component.refs
element.style.width =
component.getGutterContainerWidth() +
1.5 * editor.horizontalScrollMargin * component.measurements.baseCharacterWidth + 'px'
await component.getNextUpdatePromise()
const contentWidth = scroller.clientWidth - gutterContainer.offsetWidth
const contentWidthInCharacters = Math.floor(contentWidth / component.measurements.baseCharacterWidth)
expect(contentWidthInCharacters).toBe(9)
editor.scrollToScreenRange([[6, 10], [6, 15]])
await component.getNextUpdatePromise()
let expectedScrollLeft = Math.floor(
clientLeftForCharacter(component, 6, 10) -
lineNodeForScreenRow(component, 1).getBoundingClientRect().left -
(4 * component.measurements.baseCharacterWidth)
)
expect(scroller.scrollLeft).toBe(expectedScrollLeft)
})
})
})

View File

@@ -31,7 +31,7 @@ class TextEditorComponent {
this.textNodesByScreenLineId = new Map()
this.pendingAutoscroll = null
this.autoscrollTop = null
this.scrollWidthOrHeightChanged = false
this.contentWidthOrHeightChanged = false
this.previousScrollWidth = 0
this.previousScrollHeight = 0
this.lastKeydown = null
@@ -69,26 +69,22 @@ class TextEditorComponent {
this.resolveNextUpdatePromise = null
}
if (this.scrollWidthOrHeightChanged) this.measureClientDimensions()
this.horizontalPositionsToMeasure.clear()
if (this.contentWidthOrHeightChanged) this.measureClientDimensions()
if (this.pendingAutoscroll) this.initiateAutoscroll()
this.populateVisibleRowRange()
const longestLineToMeasure = this.checkForNewLongestLine()
if (this.pendingAutoscroll) this.autoscrollVertically()
this.horizontalPositionsToMeasure.clear()
this.queryCursorsToRender()
etch.updateSync(this)
if (this.autoscrollTop != null) {
this.refs.scroller.scrollTop = this.autoscrollTop
this.autoscrollTop = null
}
if (longestLineToMeasure) this.measureLongestLineWidth(longestLineToMeasure)
this.queryCursorsToRender()
this.measureHorizontalPositions()
if (longestLineToMeasure) this.measureLongestLineWidth(longestLineToMeasure)
if (this.pendingAutoscroll) this.finalizeAutoscroll()
this.positionCursorsToRender()
etch.updateSync(this)
this.pendingAutoscroll = null
this.currentFrameLineNumberGutterProps = null
}
@@ -198,15 +194,15 @@ class TextEditorComponent {
overflow: 'hidden'
}
if (this.measurements) {
const scrollWidth = this.getScrollWidth()
const contentWidth = this.getContentWidth()
const scrollHeight = this.getScrollHeight()
if (scrollWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) {
this.scrollWidthOrHeightChanged = true
this.previousScrollWidth = scrollWidth
if (contentWidth !== this.previousScrollWidth || scrollHeight !== this.previousScrollHeight) {
this.contentWidthOrHeightChanged = true
this.previousScrollWidth = contentWidth
this.previousScrollHeight = scrollHeight
}
const width = scrollWidth + 'px'
const width = contentWidth + 'px'
const height = scrollHeight + 'px'
style.width = width
style.height = height
@@ -238,7 +234,7 @@ class TextEditorComponent {
// const lastTileStartRow = this.getLastTileStartRow()
const rowsPerTile = this.getRowsPerTile()
const tileHeight = this.measurements.lineHeight * rowsPerTile
const tileWidth = this.getScrollWidth()
const tileWidth = this.getContentWidth()
const displayLayer = this.getModel().displayLayer
const screenLines = displayLayer.getScreenLines(startRow, endRow)
@@ -565,13 +561,16 @@ class TextEditorComponent {
this.scheduleUpdate()
}
autoscrollVertically () {
initiateAutoscroll () {
const {screenRange, options} = this.pendingAutoscroll
const screenRangeTop = this.pixelTopForScreenRow(screenRange.start.row)
const screenRangeBottom = this.pixelTopForScreenRow(screenRange.end.row) + this.measurements.lineHeight
const verticalScrollMargin = this.getVerticalScrollMargin()
this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column)
this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column)
let desiredScrollTop, desiredScrollBottom
if (options && options.center) {
const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2
@@ -595,22 +594,65 @@ class TextEditorComponent {
if (!options || options.reversed !== false) {
if (desiredScrollBottom > this.getScrollBottom()) {
this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
this.measurements.scrollTop = this.autoscrollTop
}
if (desiredScrollTop < this.getScrollTop()) {
this.autoscrollTop = desiredScrollTop
this.measurements.scrollTop = this.autoscrollTop
}
} else {
if (desiredScrollTop < this.getScrollTop()) {
this.autoscrollTop = desiredScrollTop
this.measurements.scrollTop = this.autoscrollTop
}
if (desiredScrollBottom > this.getScrollBottom()) {
this.autoscrollTop = desiredScrollBottom - this.measurements.clientHeight
this.measurements.scrollTop = this.autoscrollTop
}
}
}
finalizeAutoscroll () {
const horizontalScrollMargin = this.getHorizontalScrollMargin()
const {screenRange, options} = this.pendingAutoscroll
const gutterContainerWidth = this.getGutterContainerWidth()
let left = this.pixelLeftForScreenRowAndColumn(screenRange.start.row, screenRange.start.column) + gutterContainerWidth
let right = this.pixelLeftForScreenRowAndColumn(screenRange.end.row, screenRange.end.column) + gutterContainerWidth
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
}
if (desiredScrollLeft < this.getScrollLeft()) {
autoscrollLeft = desiredScrollLeft
this.measurements.scrollLeft = autoscrollLeft
}
} else {
if (desiredScrollLeft < this.getScrollLeft()) {
autoscrollLeft = desiredScrollLeft
this.measurements.scrollLeft = autoscrollLeft
}
if (desiredScrollRight > this.getScrollRight()) {
autoscrollLeft = desiredScrollRight - this.getClientWidth()
this.measurements.scrollLeft = autoscrollLeft
}
}
if (this.autoscrollTop != null) {
this.measurements.scrollTop = this.autoscrollTop
this.refs.scroller.scrollTop = this.autoscrollTop
this.autoscrollTop = null
}
if (autoscrollLeft != null) {
this.refs.scroller.scrollLeft = autoscrollLeft
}
this.pendingAutoscroll = null
}
getVerticalScrollMargin () {
@@ -619,7 +661,17 @@ class TextEditorComponent {
this.getModel().verticalScrollMargin,
Math.floor(((clientHeight / lineHeight) - 1) / 2)
)
return marginInLines * this.measurements.lineHeight
return marginInLines * lineHeight
}
getHorizontalScrollMargin () {
const {clientWidth, baseCharacterWidth} = this.measurements
const contentClientWidth = clientWidth - this.getGutterContainerWidth()
const marginInBaseCharacters = Math.min(
this.getModel().horizontalScrollMargin,
Math.floor(((contentClientWidth / baseCharacterWidth) - 1) / 2)
)
return marginInBaseCharacters * baseCharacterWidth
}
performInitialMeasurements () {
@@ -671,7 +723,7 @@ class TextEditorComponent {
this.measurements.clientWidth = clientWidth
clientDimensionsChanged = true
}
this.scrollWidthOrHeightChanged = false
this.contentWidthOrHeightChanged = false
return clientDimensionsChanged
}
@@ -743,6 +795,9 @@ class TextEditorComponent {
if (nextColumnToMeasure === 0) {
positions.set(0, 0)
continue columnLoop
}
if (nextColumnToMeasure >= lineNode.textContent.length) {
}
if (positions.has(nextColumnToMeasure)) continue columnLoop
const textNode = textNodes[textNodesIndex]
@@ -815,7 +870,7 @@ class TextEditorComponent {
getScrollBottom () {
return this.measurements
? this.getScrollTop() + this.measurements.clientHeight
? this.measurements.scrollTop + this.measurements.clientHeight
: null
}
@@ -823,18 +878,36 @@ class TextEditorComponent {
return this.measurements ? this.measurements.scrollLeft : null
}
getScrollRight () {
return this.measurements
? this.measurements.scrollLeft + this.measurements.clientWidth
: null
}
getScrollHeight () {
return this.getModel().getApproximateScreenLineCount() * this.measurements.lineHeight
}
getScrollWidth () {
return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth)
return this.getContentWidth() + this.getGutterContainerWidth()
}
getClientHeight () {
return this.measurements.clientHeight
}
getClientWidth () {
return this.measurements.clientWidth
}
getGutterContainerWidth () {
return this.measurements.lineNumberGutterWidth
}
getContentWidth () {
return Math.round(this.measurements.longestLineWidth + this.measurements.baseCharacterWidth)
}
getRowsPerTile () {
return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE
}

View File

@@ -3426,6 +3426,7 @@ class TextEditor extends Model
@getElement().scrollToBottom()
scrollToScreenRange: (screenRange, options = {}) ->
screenRange = @clipScreenRange(screenRange)
scrollEvent = {screenRange, options}
@emitter.emit "did-request-autoscroll", scrollEvent