Re-measure and update rendered content when editor styles change

This commit is contained in:
Nathan Sobo
2017-04-11 15:05:31 -06:00
committed by Antonio Scandurra
parent b6cd473c16
commit 95c8950004
4 changed files with 99 additions and 26 deletions

View File

@@ -28,23 +28,23 @@ describe('TextEditorComponent', () => {
it('renders lines and line numbers for the visible region', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false})
expect(element.querySelectorAll('.line-number').length).toBe(13 + 1) // +1 for placeholder line number
expect(element.querySelectorAll('.line').length).toBe(13)
expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13)
expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13)
element.style.height = 4 * component.measurements.lineHeight + 'px'
await component.getNextUpdatePromise()
expect(element.querySelectorAll('.line-number').length).toBe(9 + 1) // +1 for placeholder line number
expect(element.querySelectorAll('.line').length).toBe(9)
expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9)
expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9)
await setScrollTop(component, 5 * component.getLineHeight())
// After scrolling down beyond > 3 rows, the order of line numbers and lines
// in the DOM is a bit weird because the first tile is recycled to the bottom
// when it is scrolled out of view
expect(Array.from(element.querySelectorAll('.line-number')).slice(1).map(element => element.textContent.trim())).toEqual([
expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([
'10', '11', '12', '4', '5', '6', '7', '8', '9'
])
expect(Array.from(element.querySelectorAll('.line')).map(element => element.textContent)).toEqual([
expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([
editor.lineTextForScreenRow(9),
' ', // this line is blank in the model, but we render a space to prevent the line from collapsing vertically
editor.lineTextForScreenRow(11),
@@ -57,10 +57,10 @@ describe('TextEditorComponent', () => {
])
await setScrollTop(component, 2.5 * component.getLineHeight())
expect(Array.from(element.querySelectorAll('.line-number')).slice(1).map(element => element.textContent.trim())).toEqual([
expect(Array.from(element.querySelectorAll('.line-number:not(.dummy)')).map(element => element.textContent.trim())).toEqual([
'1', '2', '3', '4', '5', '6', '7', '8', '9'
])
expect(Array.from(element.querySelectorAll('.line')).map(element => element.textContent)).toEqual([
expect(Array.from(element.querySelectorAll('.line:not(.dummy)')).map(element => element.textContent)).toEqual([
editor.lineTextForScreenRow(0),
editor.lineTextForScreenRow(1),
editor.lineTextForScreenRow(2),
@@ -2191,6 +2191,53 @@ describe('TextEditorComponent', () => {
})
})
})
describe('styling changes', () => {
it('updates the rendered content based on new measurements when the font dimensions change', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 1, autoHeight: false})
await setEditorHeightInLines(component, 3)
editor.setCursorScreenPosition([1, 29], {autoscroll: false})
await component.getNextUpdatePromise()
let cursorNode = element.querySelector('.cursor')
const initialBaseCharacterWidth = editor.getDefaultCharWidth()
const initialDoubleCharacterWidth = editor.getDoubleWidthCharWidth()
const initialHalfCharacterWidth = editor.getHalfWidthCharWidth()
const initialKoreanCharacterWidth = editor.getKoreanCharWidth()
const initialRenderedLineCount = element.querySelectorAll('.line:not(.dummy)').length
const initialFontSize = parseInt(getComputedStyle(element).fontSize)
expect(initialKoreanCharacterWidth).toBeDefined()
expect(initialDoubleCharacterWidth).toBeDefined()
expect(initialHalfCharacterWidth).toBeDefined()
expect(initialBaseCharacterWidth).toBeDefined()
expect(initialDoubleCharacterWidth).not.toBe(initialBaseCharacterWidth)
expect(initialHalfCharacterWidth).not.toBe(initialBaseCharacterWidth)
expect(initialKoreanCharacterWidth).not.toBe(initialBaseCharacterWidth)
verifyCursorPosition(component, cursorNode, 1, 29)
console.log(initialFontSize);
element.style.fontSize = initialFontSize - 5 + 'px'
TextEditor.didUpdateStyles()
await component.getNextUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeLessThan(initialBaseCharacterWidth)
expect(editor.getDoubleWidthCharWidth()).toBeLessThan(initialDoubleCharacterWidth)
expect(editor.getHalfWidthCharWidth()).toBeLessThan(initialHalfCharacterWidth)
expect(editor.getKoreanCharWidth()).toBeLessThan(initialKoreanCharacterWidth)
expect(element.querySelectorAll('.line:not(.dummy)').length).toBeGreaterThan(initialRenderedLineCount)
verifyCursorPosition(component, cursorNode, 1, 29)
element.style.fontSize = initialFontSize + 5 + 'px'
TextEditor.didUpdateStyles()
await component.getNextUpdatePromise()
expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialBaseCharacterWidth)
expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(initialDoubleCharacterWidth)
expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(initialHalfCharacterWidth)
expect(editor.getKoreanCharWidth()).toBeGreaterThan(initialKoreanCharacterWidth)
expect(element.querySelectorAll('.line:not(.dummy)').length).toBeLessThan(initialRenderedLineCount)
verifyCursorPosition(component, cursorNode, 1, 29)
})
})
})
function buildEditor (params = {}) {
@@ -2227,7 +2274,7 @@ function getBaseCharacterWidth (component) {
}
async function setEditorHeightInLines(component, heightInLines) {
component.element.style.height = component.measurements.lineHeight * heightInLines + 'px'
component.element.style.height = component.getLineHeight() * heightInLines + 'px'
await component.getNextUpdatePromise()
}

View File

@@ -805,6 +805,7 @@ class AtomEnvironment extends Model
@windowEventHandler = null
didChangeStyles: (styleElement) ->
TextEditor.didUpdateStyles()
if styleElement.textContent.indexOf('scrollbar') >= 0
TextEditor.didUpdateScrollbarStyles()

View File

@@ -29,6 +29,13 @@ const BLOCK_DECORATION_MEASUREMENT_AREA_VNODE = $.div({
visibility: 'hidden'
}
})
const CHARACTER_MEASUREMENT_LINE_VNODE = $.div(
{key: 'characterMeasurementLine', ref: 'characterMeasurementLine', className: 'line dummy'},
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
$.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
$.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
$.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
)
function scaleMouseDragAutoscrollDelta (delta) {
return Math.pow(delta / 3, 3) / 280
@@ -40,6 +47,14 @@ class TextEditorComponent {
etch.setScheduler(scheduler)
}
static didUpdateStyles () {
if (this.attachedComponents) {
this.attachedComponents.forEach((component) => {
component.didUpdateStyles()
})
}
}
static didUpdateScrollbarStyles () {
if (this.attachedComponents) {
this.attachedComponents.forEach((component) => {
@@ -79,7 +94,7 @@ class TextEditorComponent {
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.shouldRenderDummyScrollbars = true
this.refreshedScrollbarStyle = false
this.remeasureScrollbars = false
this.pendingAutoscroll = null
this.scrollTopPending = false
this.scrollLeftPending = false
@@ -161,6 +176,12 @@ class TextEditorComponent {
return
}
if (this.remeasureCharacterDimensions) {
this.measureCharacterDimensions()
this.measureGutterDimensions()
this.remeasureCharacterDimensions = false
}
this.measureBlockDecorations()
this.measuredContent = false
@@ -254,7 +275,7 @@ class TextEditorComponent {
this.queryLineNumbersToRender()
this.queryGuttersToRender()
this.queryDecorationsToRender()
this.shouldRenderDummyScrollbars = !this.refreshedScrollbarStyle
this.shouldRenderDummyScrollbars = !this.remeasureScrollbars
etch.updateSync(this)
this.shouldRenderDummyScrollbars = true
this.didMeasureVisibleBlockDecoration = false
@@ -286,9 +307,9 @@ class TextEditorComponent {
this.currentFrameLineNumberGutterProps = null
this.scrollTopPending = false
this.scrollLeftPending = false
if (this.refreshedScrollbarStyle) {
if (this.remeasureScrollbars) {
this.measureScrollbarDimensions()
this.refreshedScrollbarStyle = false
this.remeasureScrollbars = false
etch.updateSync(this)
}
}
@@ -480,17 +501,13 @@ class TextEditorComponent {
this.renderCursorsAndInput(),
this.renderLineTiles(),
BLOCK_DECORATION_MEASUREMENT_AREA_VNODE,
CHARACTER_MEASUREMENT_LINE_VNODE,
this.renderPlaceholderText()
]
} else {
children = [
BLOCK_DECORATION_MEASUREMENT_AREA_VNODE,
$.div({ref: 'characterMeasurementLine', className: 'line'},
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
$.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
$.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
$.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
)
CHARACTER_MEASUREMENT_LINE_VNODE
]
}
@@ -505,8 +522,6 @@ class TextEditorComponent {
}
renderLineTiles () {
if (!this.measurements) return []
const {lineNodesByScreenLineId, textNodesByScreenLineId} = this
const startRow = this.getRenderedStartRow()
@@ -676,7 +691,7 @@ class TextEditorComponent {
this.isVerticalScrollbarVisible()
? this.getVerticalScrollbarWidth()
: 0
forceScrollbarVisible = this.refreshedScrollbarStyle
forceScrollbarVisible = this.remeasureScrollbars
} else {
forceScrollbarVisible = true
}
@@ -1117,7 +1132,7 @@ class TextEditorComponent {
}
didShow () {
if (!this.visible) {
if (!this.visible && this.isVisible()) {
this.visible = true
if (!this.measurements) this.performInitialMeasurements()
this.props.model.setVisible(true)
@@ -1235,8 +1250,14 @@ class TextEditorComponent {
if (scrollTopChanged || scrollLeftChanged) this.updateSync()
}
didUpdateStyles () {
this.remeasureCharacterDimensions = true
this.horizontalPixelPositionsByScreenLineId.clear()
this.scheduleUpdate()
}
didUpdateScrollbarStyles () {
this.refreshedScrollbarStyle = true
this.remeasureScrollbars = true
this.scheduleUpdate()
}
@@ -1680,7 +1701,7 @@ class TextEditorComponent {
this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width
this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width
this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width
this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().widt
this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width
this.props.model.setDefaultCharWidth(
this.measurements.baseCharacterWidth,
@@ -2444,7 +2465,7 @@ class LineNumberGutterComponent {
mousedown: this.didMouseDown
},
},
$.div({key: 'placeholder', className: 'line-number', style: {visibility: 'hidden'}},
$.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
'0'.repeat(maxDigits),
$.div({className: 'icon-right'})
),

View File

@@ -65,6 +65,10 @@ class TextEditor extends Model
TextEditorComponent ?= require './text-editor-component'
TextEditorComponent.setScheduler(scheduler)
@didUpdateStyles: ->
TextEditorComponent ?= require './text-editor-component'
TextEditorComponent.didUpdateStyles()
@didUpdateScrollbarStyles: ->
TextEditorComponent ?= require './text-editor-component'
TextEditorComponent.didUpdateScrollbarStyles()