mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
Re-measure and update rendered content when editor styles change
This commit is contained in:
committed by
Antonio Scandurra
parent
b6cd473c16
commit
95c8950004
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -805,6 +805,7 @@ class AtomEnvironment extends Model
|
||||
@windowEventHandler = null
|
||||
|
||||
didChangeStyles: (styleElement) ->
|
||||
TextEditor.didUpdateStyles()
|
||||
if styleElement.textContent.indexOf('scrollbar') >= 0
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
|
||||
|
||||
@@ -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'})
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user