mirror of
https://github.com/atom/atom.git
synced 2026-01-25 23:08:18 -05:00
Render cursors after measuring horizontal positions
This commit is contained in:
committed by
Antonio Scandurra
parent
43386b0483
commit
c8166c1bb3
@@ -21,7 +21,14 @@ class TextEditorComponent {
|
||||
this.refs = {}
|
||||
|
||||
this.updateScheduled = false
|
||||
this.measurements = null
|
||||
this.visible = false
|
||||
this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
|
||||
this.horizontalPixelPositionsByScreenLine = new WeakMap() // Values are maps from column to horiontal pixel positions
|
||||
this.lineNodesByScreenLine = new WeakMap()
|
||||
this.textNodesByScreenLine = new WeakMap()
|
||||
this.cursorsToRender = []
|
||||
|
||||
resizeDetector.listenTo(this.element, this.didResize.bind(this))
|
||||
|
||||
etch.updateSync(this)
|
||||
@@ -61,8 +68,14 @@ class TextEditorComponent {
|
||||
measureLongestLine = true
|
||||
}
|
||||
|
||||
this.horizontalPositionsToMeasure.clear()
|
||||
this.populateCursorPositionsToMeasure()
|
||||
|
||||
etch.updateSync(this)
|
||||
if (measureLongestLine) this.measureLongestLineWidth()
|
||||
if (measureLongestLine) this.measureLongestLineWidth(longestLine)
|
||||
this.measureHorizontalPositions()
|
||||
this.updateCursorsToRender()
|
||||
|
||||
etch.updateSync(this)
|
||||
}
|
||||
|
||||
@@ -83,7 +96,7 @@ class TextEditorComponent {
|
||||
}
|
||||
},
|
||||
this.renderGutterContainer(),
|
||||
this.renderLines()
|
||||
this.renderContent()
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -185,16 +198,21 @@ class TextEditorComponent {
|
||||
return $.div(props, children)
|
||||
}
|
||||
|
||||
renderLines () {
|
||||
renderContent () {
|
||||
let children
|
||||
let style = {
|
||||
contain: 'strict',
|
||||
overflow: 'hidden'
|
||||
}
|
||||
if (this.measurements) {
|
||||
style.width = this.measurements.scrollWidth + 'px',
|
||||
style.height = this.getScrollHeight() + 'px'
|
||||
children = this.renderLineTiles()
|
||||
const width = this.measurements.scrollWidth + 'px'
|
||||
const height = this.getScrollHeight() + 'px'
|
||||
style.width = width
|
||||
style.height = height
|
||||
children = [
|
||||
this.renderCursors(width, height),
|
||||
this.renderLineTiles(width, height)
|
||||
]
|
||||
} else {
|
||||
children = $.div({ref: 'characterMeasurementLine', className: 'line'},
|
||||
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
|
||||
@@ -207,9 +225,11 @@ class TextEditorComponent {
|
||||
return $.div({ref: 'lines', className: 'lines', style}, children)
|
||||
}
|
||||
|
||||
renderLineTiles () {
|
||||
renderLineTiles (width, height) {
|
||||
if (!this.measurements) return []
|
||||
|
||||
const {lineNodesByScreenLine, textNodesByScreenLine} = this
|
||||
|
||||
const firstTileStartRow = this.getTileStartRow(this.getFirstVisibleRow())
|
||||
const visibleTileCount = Math.floor((this.getLastVisibleRow() - this.getFirstVisibleRow()) / this.getRowsPerTile()) + 2
|
||||
const lastTileStartRow = firstTileStartRow + ((visibleTileCount - 1) * this.getRowsPerTile())
|
||||
@@ -224,13 +244,16 @@ class TextEditorComponent {
|
||||
for (let row = tileStartRow; row < tileEndRow; row++) {
|
||||
const screenLine = screenLines[row - firstTileStartRow]
|
||||
if (!screenLine) break
|
||||
|
||||
const lineProps = {key: screenLine.id, displayLayer, screenLine}
|
||||
lineNodes.push($(LineComponent, {
|
||||
key: screenLine.id,
|
||||
screenLine,
|
||||
displayLayer,
|
||||
lineNodesByScreenLine,
|
||||
textNodesByScreenLine
|
||||
}))
|
||||
if (screenLine === this.longestLineToMeasure) {
|
||||
lineProps.ref = 'longestLineToMeasure'
|
||||
this.longestLineToMeasure = null
|
||||
}
|
||||
lineNodes.push($(LineComponent, lineProps))
|
||||
}
|
||||
|
||||
const tileHeight = this.getRowsPerTile() * this.measurements.lineHeight
|
||||
@@ -252,15 +275,72 @@ class TextEditorComponent {
|
||||
|
||||
if (this.longestLineToMeasure) {
|
||||
tileNodes.push($(LineComponent, {
|
||||
ref: 'longestLineToMeasure',
|
||||
key: this.longestLineToMeasure.id,
|
||||
screenLine: this.longestLineToMeasure,
|
||||
displayLayer,
|
||||
screenLine: this.longestLineToMeasure
|
||||
lineNodesByScreenLine,
|
||||
textNodesByScreenLine
|
||||
}))
|
||||
this.longestLineToMeasure = null
|
||||
}
|
||||
|
||||
return tileNodes
|
||||
return $.div({
|
||||
key: 'lineTiles',
|
||||
style: {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
width, height
|
||||
}
|
||||
}, tileNodes)
|
||||
}
|
||||
|
||||
renderCursors (width, height) {
|
||||
return $.div({
|
||||
key: 'cursors',
|
||||
className: 'cursors',
|
||||
style: {
|
||||
position: 'absolute',
|
||||
contain: 'strict',
|
||||
width, height
|
||||
}
|
||||
},
|
||||
this.cursorsToRender.map(style => $.div({className: 'cursor', style}))
|
||||
)
|
||||
}
|
||||
|
||||
populateCursorPositionsToMeasure () {
|
||||
const model = this.getModel()
|
||||
for (let i = 0; i < model.cursors.length; i++) {
|
||||
const cursor = model.cursors[i]
|
||||
const position = cursor.getScreenPosition()
|
||||
let columns = this.horizontalPositionsToMeasure.get(position.row)
|
||||
if (columns == null) {
|
||||
columns = []
|
||||
this.horizontalPositionsToMeasure.set(position.row, columns)
|
||||
}
|
||||
columns.push(position.column)
|
||||
columns.push(position.column + 1)
|
||||
}
|
||||
|
||||
this.horizontalPositionsToMeasure.forEach((value) => value.sort((a, b) => a - b))
|
||||
}
|
||||
|
||||
updateCursorsToRender () {
|
||||
const model = this.getModel()
|
||||
const height = this.measurements.lineHeight + 'px'
|
||||
this.cursorsToRender.length = 0
|
||||
for (let i = 0; i < model.cursors.length; i++) {
|
||||
const cursor = model.cursors[i]
|
||||
const position = cursor.getScreenPosition()
|
||||
const top = this.pixelTopForScreenRow(position.row)
|
||||
const left = this.pixelLeftForScreenPosition(position)
|
||||
const right = this.pixelLeftForScreenRowAndColumn(position.row, position.column + 1)
|
||||
this.cursorsToRender.push({
|
||||
height,
|
||||
width: (right - left) + 'px',
|
||||
transform: `translate(${top}px, ${left}px)`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
didAttach () {
|
||||
@@ -335,14 +415,87 @@ class TextEditorComponent {
|
||||
this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().widt
|
||||
}
|
||||
|
||||
measureLongestLineWidth () {
|
||||
this.measurements.scrollWidth = this.refs.longestLineToMeasure.element.firstChild.offsetWidth
|
||||
measureLongestLineWidth (screenLine) {
|
||||
this.measurements.scrollWidth = this.lineNodesByScreenLine.get(screenLine).firstChild.offsetWidth
|
||||
}
|
||||
|
||||
measureGutterDimensions () {
|
||||
this.measurements.lineNumberGutterWidth = this.refs.lineNumberGutter.offsetWidth
|
||||
}
|
||||
|
||||
measureHorizontalPositions () {
|
||||
this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => {
|
||||
const screenLine = this.getModel().displayLayer.getScreenLine(row)
|
||||
|
||||
const lineNode = this.lineNodesByScreenLine.get(screenLine)
|
||||
const textNodes = this.textNodesByScreenLine.get(screenLine)
|
||||
let positionsForLine = this.horizontalPixelPositionsByScreenLine.get(screenLine)
|
||||
if (positionsForLine == null) {
|
||||
positionsForLine = new Map()
|
||||
this.horizontalPixelPositionsByScreenLine.set(screenLine, positionsForLine)
|
||||
}
|
||||
|
||||
this.measureHorizontalPositionsOnLine(lineNode, textNodes, columnsToMeasure, positionsForLine)
|
||||
})
|
||||
}
|
||||
|
||||
measureHorizontalPositionsOnLine (lineNode, textNodes, columnsToMeasure, positions) {
|
||||
let lineNodeClientLeft = -1
|
||||
let textNodeStartColumn = 0
|
||||
let textNodesIndex = 0
|
||||
|
||||
columnLoop:
|
||||
for (let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++) {
|
||||
while (textNodesIndex < textNodes.length) {
|
||||
const nextColumnToMeasure = columnsToMeasure[columnsIndex]
|
||||
if (nextColumnToMeasure === 0) {
|
||||
positions.set(0, 0)
|
||||
continue columnLoop
|
||||
}
|
||||
if (positions.has(nextColumnToMeasure)) continue columnLoop
|
||||
const textNode = textNodes[textNodesIndex]
|
||||
const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
|
||||
|
||||
if (nextColumnToMeasure <= textNodeEndColumn) {
|
||||
let clientPixelPosition
|
||||
if (nextColumnToMeasure === textNodeStartColumn) {
|
||||
const range = getRangeForMeasurement()
|
||||
range.selectNode(textNode)
|
||||
clientPixelPosition = range.getBoundingClientRect().left
|
||||
} else if (nextColumnToMeasure === textNodeEndColumn) {
|
||||
const range = getRangeForMeasurement()
|
||||
range.selectNode(textNode)
|
||||
clientPixelPosition = range.getBoundingClientRect().right
|
||||
} else {
|
||||
const range = getRangeForMeasurement()
|
||||
range.setStart(textNode, 0)
|
||||
range.setEnd(textNode, nextColumnToMeasure - textNodeStartColumn)
|
||||
clientPixelPosition = range.getBoundingClientRect().right
|
||||
}
|
||||
if (lineNodeClientLeft === -1) lineNodeClientLeft = lineNode.getBoundingClientRect().left
|
||||
positions.set(nextColumnToMeasure, clientPixelPosition - lineNodeClientLeft)
|
||||
continue columnLoop
|
||||
} else {
|
||||
textNodesIndex++
|
||||
textNodeStartColumn = textNodeEndColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pixelTopForScreenRow (row) {
|
||||
return row * this.measurements.lineHeight
|
||||
}
|
||||
|
||||
pixelLeftForScreenPosition ({row, column}) {
|
||||
return this.pixelLeftForScreenRowAndColumn(row, column)
|
||||
}
|
||||
|
||||
pixelLeftForScreenRowAndColumn (row, column) {
|
||||
const screenLine = this.getModel().displayLayer.getScreenLine(row)
|
||||
return this.horizontalPixelPositionsByScreenLine.get(screenLine).get(column)
|
||||
}
|
||||
|
||||
getModel () {
|
||||
if (!this.props.model) {
|
||||
const TextEditor = require('./text-editor')
|
||||
@@ -416,12 +569,15 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
class LineComponent {
|
||||
constructor ({displayLayer, screenLine}) {
|
||||
const {lineText, tagCodes} = screenLine
|
||||
constructor ({displayLayer, screenLine, lineNodesByScreenLine, textNodesByScreenLine}) {
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('line')
|
||||
lineNodesByScreenLine.set(screenLine, this.element)
|
||||
|
||||
const textNodes = []
|
||||
textNodesByScreenLine.set(screenLine, textNodes)
|
||||
|
||||
const {lineText, tagCodes} = screenLine
|
||||
let startIndex = 0
|
||||
let openScopeNode = document.createElement('span')
|
||||
this.element.appendChild(openScopeNode)
|
||||
@@ -459,8 +615,6 @@ class LineComponent {
|
||||
this.element.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
|
||||
// this.textNodesByLineId[id] = textNodes
|
||||
}
|
||||
|
||||
update () {}
|
||||
@@ -475,3 +629,9 @@ function classNameForScopeName (scopeName) {
|
||||
}
|
||||
return classString
|
||||
}
|
||||
|
||||
let rangeForMeasurement
|
||||
function getRangeForMeasurement () {
|
||||
if (!rangeForMeasurement) rangeForMeasurement = document.createRange()
|
||||
return rangeForMeasurement
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user