mirror of
https://github.com/atom/atom.git
synced 2026-01-22 21:38:10 -05:00
Measure DOM dimensions before rendering elements that depend on them
This commit breaks the initial render of the editor component into two stages. The first stage just renders the shell of the editor so the height, width, line height, and default character width can be measured. Nothing that depends on these values is rendered on the first render pass. Once the editor component is mounted, all these values are measured and we force another update, which fills in the lines, line numbers, selections, etc. We also refrain from assigning an explicit height and width on the model if these values aren't explicitly styled in the DOM, and just assume the editor will stretch to accommodate its contents.
This commit is contained in:
@@ -38,7 +38,7 @@ describe "EditorComponent", ->
|
||||
describe "line rendering", ->
|
||||
it "renders only the currently-visible lines", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
lines = node.querySelectorAll('.line')
|
||||
expect(lines.length).toBe 6
|
||||
@@ -121,7 +121,7 @@ describe "EditorComponent", ->
|
||||
describe "gutter rendering", ->
|
||||
it "renders the currently-visible line numbers", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
lines = node.querySelectorAll('.line-number')
|
||||
expect(lines.length).toBe 6
|
||||
@@ -146,7 +146,7 @@ describe "EditorComponent", ->
|
||||
editor.setSoftWrap(true)
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
node.style.width = 30 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
lines = node.querySelectorAll('.line-number')
|
||||
expect(lines.length).toBe 6
|
||||
@@ -163,7 +163,7 @@ describe "EditorComponent", ->
|
||||
cursor1.setScreenPosition([0, 5])
|
||||
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
cursorNodes = node.querySelectorAll('.cursor')
|
||||
expect(cursorNodes.length).toBe 1
|
||||
@@ -258,7 +258,7 @@ describe "EditorComponent", ->
|
||||
inputNode = node.querySelector('.hidden-input')
|
||||
node.style.height = 5 * lineHeightInPixels + 'px'
|
||||
node.style.width = 10 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
|
||||
editor.setScrollTop(3 * lineHeightInPixels)
|
||||
@@ -292,6 +292,7 @@ describe "EditorComponent", ->
|
||||
# 1-line selection
|
||||
editor.setSelectedScreenRange([[1, 6], [1, 10]])
|
||||
regions = node.querySelectorAll('.selection .region')
|
||||
|
||||
expect(regions.length).toBe 1
|
||||
regionRect = regions[0].getBoundingClientRect()
|
||||
expect(regionRect.top).toBe 1 * lineHeightInPixels
|
||||
@@ -355,7 +356,7 @@ describe "EditorComponent", ->
|
||||
it "moves the cursor to the nearest screen position", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
node.style.width = 10 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
editor.setScrollTop(3.5 * lineHeightInPixels)
|
||||
editor.setScrollLeft(2 * charWidth)
|
||||
|
||||
@@ -468,7 +469,7 @@ describe "EditorComponent", ->
|
||||
describe "scrolling", ->
|
||||
it "updates the vertical scrollbar when the scrollTop is changed in the model", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
expect(verticalScrollbarNode.scrollTop).toBe 0
|
||||
|
||||
@@ -477,7 +478,7 @@ describe "EditorComponent", ->
|
||||
|
||||
it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", ->
|
||||
node.style.width = 30 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
scrollViewContentNode = node.querySelector('.scroll-view-content')
|
||||
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate(0px, 0px)"
|
||||
@@ -489,7 +490,7 @@ describe "EditorComponent", ->
|
||||
|
||||
it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", ->
|
||||
node.style.width = 30 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
expect(editor.getScrollLeft()).toBe 0
|
||||
horizontalScrollbarNode.scrollLeft = 100
|
||||
@@ -501,7 +502,7 @@ describe "EditorComponent", ->
|
||||
it "updates the horizontal or vertical scrollbar depending on which delta is greater (x or y)", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
node.style.width = 20 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
expect(verticalScrollbarNode.scrollTop).toBe 0
|
||||
expect(horizontalScrollbarNode.scrollLeft).toBe 0
|
||||
@@ -517,7 +518,7 @@ describe "EditorComponent", ->
|
||||
it "preserves the target of the mousewheel event when scrolling vertically", ->
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
node.style.width = 20 * charWidth + 'px'
|
||||
component.updateModelDimensions()
|
||||
component.measureHeightAndWidth()
|
||||
|
||||
lineNodes = node.querySelectorAll('.line')
|
||||
expect(lineNodes.length).toBe 6
|
||||
|
||||
@@ -17,10 +17,11 @@ CursorsComponent = React.createClass
|
||||
blinkOff = @state.blinkCursorsOff
|
||||
|
||||
div className: 'cursors',
|
||||
for selection in editor.getSelections()
|
||||
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
{cursor} = selection
|
||||
CursorComponent({key: cursor.id, cursor, blinkOff})
|
||||
if @isMounted()
|
||||
for selection in editor.getSelections()
|
||||
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
{cursor} = selection
|
||||
CursorComponent({key: cursor.id, cursor, blinkOff})
|
||||
|
||||
getInitialState: ->
|
||||
blinkCursorsOff: false
|
||||
|
||||
@@ -111,10 +111,10 @@ class DisplayBuffer extends Model
|
||||
getHorizontalScrollMargin: -> @horizontalScrollMargin
|
||||
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
|
||||
|
||||
getHeight: -> @height
|
||||
getHeight: -> @height ? @getScrollHeight()
|
||||
setHeight: (@height) -> @height
|
||||
|
||||
getWidth: -> @width
|
||||
getWidth: -> @width ? @getScrollWidth()
|
||||
setWidth: (newWidth) ->
|
||||
oldWidth = @width
|
||||
@width = newWidth
|
||||
@@ -172,18 +172,21 @@ class DisplayBuffer extends Model
|
||||
@charWidthsByScope = {}
|
||||
|
||||
getScrollHeight: ->
|
||||
unless @getLineHeight() > 0
|
||||
throw new Error("You must assign lineHeight before calling ::getScrollHeight()")
|
||||
|
||||
@getLineCount() * @getLineHeight()
|
||||
|
||||
getScrollWidth: ->
|
||||
@getMaxLineLength() * @getDefaultCharWidth()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
return [0, 0] unless @getLineHeight() > 0
|
||||
return [0, @getLineCount()] if @getHeight() is 0
|
||||
unless @getLineHeight() > 0
|
||||
throw new Error("You must assign a non-zero lineHeight before calling ::getVisibleRowRange()")
|
||||
|
||||
heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1
|
||||
startRow = Math.floor(@getScrollTop() / @getLineHeight())
|
||||
endRow = Math.ceil(startRow + heightInLines)
|
||||
endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines))
|
||||
[startRow, endRow]
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
|
||||
@@ -23,8 +23,12 @@ EditorComponent = React.createClass
|
||||
render: ->
|
||||
{focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
|
||||
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
visibleRowRange = editor.getVisibleRowRange()
|
||||
scrollTop = editor.getScrollTop()
|
||||
if @isMounted()
|
||||
visibleRowRange = editor.getVisibleRowRange()
|
||||
scrollHeight = editor.getScrollHeight()
|
||||
scrollWidth = editor.getScrollWidth()
|
||||
scrollTop = editor.getScrollTop()
|
||||
scrollLeft = editor.getScrollLeft()
|
||||
|
||||
className = 'editor editor-colors react'
|
||||
className += ' is-focused' if focused
|
||||
@@ -44,16 +48,16 @@ EditorComponent = React.createClass
|
||||
className: 'vertical-scrollbar'
|
||||
orientation: 'vertical'
|
||||
onScroll: @onVerticalScroll
|
||||
scrollTop: editor.getScrollTop()
|
||||
scrollHeight: editor.getScrollHeight()
|
||||
scrollTop: scrollTop
|
||||
scrollHeight: scrollHeight
|
||||
|
||||
ScrollbarComponent
|
||||
ref: 'horizontalScrollbar'
|
||||
className: 'horizontal-scrollbar'
|
||||
orientation: 'horizontal'
|
||||
onScroll: @onHorizontalScroll
|
||||
scrollLeft: editor.getScrollLeft()
|
||||
scrollWidth: editor.getScrollWidth()
|
||||
scrollLeft: scrollLeft
|
||||
scrollWidth: scrollWidth
|
||||
|
||||
getInitialState: -> {}
|
||||
|
||||
@@ -61,16 +65,17 @@ EditorComponent = React.createClass
|
||||
cursorBlinkPeriod: 800
|
||||
cursorBlinkResumeDelay: 200
|
||||
|
||||
componentDidMount: ->
|
||||
componentWillMount: ->
|
||||
@pendingChanges = []
|
||||
@props.editor.manageScrollPosition = true
|
||||
|
||||
@listenForDOMEvents()
|
||||
@listenForCommands()
|
||||
@observeEditor()
|
||||
@observeConfig()
|
||||
|
||||
componentDidMount: ->
|
||||
@observeEditor()
|
||||
@listenForDOMEvents()
|
||||
@listenForCommands()
|
||||
@props.editor.setVisible(true)
|
||||
@requestUpdate()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@getDOMNode().removeEventListener 'mousewheel', @onMouseWheel
|
||||
@@ -317,8 +322,8 @@ EditorComponent = React.createClass
|
||||
else
|
||||
@forceUpdate()
|
||||
|
||||
updateModelDimensions: ->
|
||||
@refs.scrollView.updateModelDimensions()
|
||||
measureHeightAndWidth: ->
|
||||
@refs.scrollView.measureHeightAndWidth()
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
||||
|
||||
@@ -13,11 +13,13 @@ EditorScrollViewComponent = React.createClass
|
||||
render: ->
|
||||
{editor, fontSize, fontFamily, lineHeight, showIndentGuide, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||
{visibleRowRange, preservedScreenRow, pendingChanges, cursorsMoved, onInputFocused, onInputBlurred} = @props
|
||||
contentStyle =
|
||||
height: editor.getScrollHeight()
|
||||
WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)"
|
||||
|
||||
div className: 'scroll-view', ref: 'scrollView',
|
||||
if @isMounted()
|
||||
contentStyle =
|
||||
height: editor.getScrollHeight()
|
||||
WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)"
|
||||
|
||||
div className: 'scroll-view',
|
||||
InputComponent
|
||||
ref: 'input'
|
||||
className: 'hidden-input'
|
||||
@@ -36,8 +38,8 @@ EditorScrollViewComponent = React.createClass
|
||||
SelectionsComponent({editor})
|
||||
|
||||
componentDidMount: ->
|
||||
@getDOMNode().addEventListener 'overflowchanged', @updateModelDimensions
|
||||
@updateModelDimensions()
|
||||
@getDOMNode().addEventListener 'overflowchanged', @measureHeightAndWidth
|
||||
@measureHeightAndWidth()
|
||||
|
||||
onInput: (char, replaceLastCharacter) ->
|
||||
{editor} = @props
|
||||
@@ -109,12 +111,14 @@ EditorScrollViewComponent = React.createClass
|
||||
{editor} = @props
|
||||
{clientX, clientY} = event
|
||||
|
||||
editorClientRect = @refs.scrollView.getDOMNode().getBoundingClientRect()
|
||||
editorClientRect = @getDOMNode().getBoundingClientRect()
|
||||
top = clientY - editorClientRect.top + editor.getScrollTop()
|
||||
left = clientX - editorClientRect.left + editor.getScrollLeft()
|
||||
{top, left}
|
||||
|
||||
getHiddenInputPosition: ->
|
||||
return {top: 0, left: 0} unless @isMounted()
|
||||
|
||||
{editor} = @props
|
||||
|
||||
if cursor = editor.getCursor()
|
||||
@@ -129,11 +133,20 @@ EditorScrollViewComponent = React.createClass
|
||||
|
||||
{top, left}
|
||||
|
||||
updateModelDimensions: ->
|
||||
# Measure explicitly-styled height and width and relay them to the model. If
|
||||
# these values aren't explicitly styled, we assume the editor is unconstrained
|
||||
# and use the scrollHeight / scrollWidth as its height and width in
|
||||
# calculations.
|
||||
measureHeightAndWidth: ->
|
||||
{editor} = @props
|
||||
node = @getDOMNode()
|
||||
editor.setHeight(node.clientHeight)
|
||||
editor.setWidth(node.clientWidth)
|
||||
computedStyle = getComputedStyle(node)
|
||||
|
||||
unless computedStyle.height is '0px'
|
||||
editor.setHeight(node.clientHeight)
|
||||
|
||||
unless computedStyle.width is '0px'
|
||||
editor.setWidth(node.clientWidth)
|
||||
|
||||
focus: ->
|
||||
@refs.input.focus()
|
||||
|
||||
@@ -9,6 +9,10 @@ GutterComponent = React.createClass
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
render: ->
|
||||
div className: 'gutter',
|
||||
@renderLineNumbers() if @isMounted()
|
||||
|
||||
renderLineNumbers: ->
|
||||
{editor, visibleRowRange, preservedScreenRow, scrollTop} = @props
|
||||
[startRow, endRow] = visibleRowRange
|
||||
lineHeightInPixels = editor.getLineHeight()
|
||||
@@ -21,6 +25,7 @@ GutterComponent = React.createClass
|
||||
|
||||
lineNumbers = []
|
||||
tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1)
|
||||
tokenizedLines.push({id: 0}) if tokenizedLines.length is 0
|
||||
for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||
if bufferRow is lastBufferRow
|
||||
lineNumber = '•'
|
||||
@@ -28,7 +33,7 @@ GutterComponent = React.createClass
|
||||
lastBufferRow = bufferRow
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
|
||||
key = tokenizedLines[i]?.id ? 0
|
||||
key = tokenizedLines[i]?.id
|
||||
screenRow = startRow + i
|
||||
lineNumbers.push(LineNumberComponent({key, lineNumber, maxDigits, bufferRow, screenRow}))
|
||||
lastBufferRow = bufferRow
|
||||
@@ -36,9 +41,8 @@ GutterComponent = React.createClass
|
||||
if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow)
|
||||
lineNumbers.push(LineNumberComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true}))
|
||||
|
||||
div className: 'gutter',
|
||||
div className: 'line-numbers', style: style,
|
||||
lineNumbers
|
||||
div className: 'line-numbers', style: style,
|
||||
lineNumbers
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unsubscribe()
|
||||
|
||||
@@ -11,26 +11,28 @@ LinesComponent = React.createClass
|
||||
displayName: 'LinesComponent'
|
||||
|
||||
render: ->
|
||||
{editor, visibleRowRange, preservedScreenRow, showIndentGuide} = @props
|
||||
if @isMounted()
|
||||
{editor, visibleRowRange, preservedScreenRow, showIndentGuide} = @props
|
||||
[startRow, endRow] = visibleRowRange
|
||||
|
||||
[startRow, endRow] = visibleRowRange
|
||||
lineHeightInPixels = editor.getLineHeight()
|
||||
paddingTop = startRow * lineHeightInPixels
|
||||
paddingBottom = (editor.getScreenLineCount() - endRow) * lineHeightInPixels
|
||||
style =
|
||||
paddingTop: startRow * editor.getLineHeight()
|
||||
paddingBottom: (editor.getScreenLineCount() - endRow) * editor.getLineHeight()
|
||||
|
||||
lines =
|
||||
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
||||
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, screenRow: startRow + i})
|
||||
lines =
|
||||
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
||||
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, screenRow: startRow + i})
|
||||
|
||||
if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow)
|
||||
lines.push(LineComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true}))
|
||||
if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow)
|
||||
lines.push(LineComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true}))
|
||||
|
||||
div className: 'lines', ref: 'lines', style: {paddingTop, paddingBottom},
|
||||
lines
|
||||
div {className: 'lines', style}, lines
|
||||
|
||||
componentWillMount: ->
|
||||
@measuredLines = new WeakSet
|
||||
|
||||
componentDidMount: ->
|
||||
@measuredLines = new WeakSet
|
||||
@updateModelDimensions()
|
||||
@measureLineHeightAndCharWidth()
|
||||
|
||||
shouldComponentUpdate: (newProps) ->
|
||||
return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'preservedScreenRow', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
|
||||
@@ -42,31 +44,28 @@ LinesComponent = React.createClass
|
||||
false
|
||||
|
||||
componentDidUpdate: (prevProps) ->
|
||||
@updateModelDimensions() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
|
||||
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
|
||||
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
|
||||
@measureCharactersInNewLines() unless @props.preservedScreenRow?
|
||||
|
||||
updateModelDimensions: ->
|
||||
{editor} = @props
|
||||
{lineHeightInPixels, charWidth} = @measureLineDimensions()
|
||||
editor.setLineHeight(lineHeightInPixels)
|
||||
editor.setDefaultCharWidth(charWidth)
|
||||
|
||||
measureLineDimensions: ->
|
||||
linesNode = @refs.lines.getDOMNode()
|
||||
linesNode.appendChild(DummyLineNode)
|
||||
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
|
||||
measureLineHeightAndCharWidth: ->
|
||||
node = @getDOMNode()
|
||||
node.appendChild(DummyLineNode)
|
||||
lineHeight = DummyLineNode.getBoundingClientRect().height
|
||||
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
||||
linesNode.removeChild(DummyLineNode)
|
||||
{lineHeightInPixels, charWidth}
|
||||
node.removeChild(DummyLineNode)
|
||||
|
||||
{editor} = @props
|
||||
editor.setLineHeight(lineHeight)
|
||||
editor.setDefaultCharWidth(charWidth)
|
||||
|
||||
measureCharactersInNewLines: ->
|
||||
[visibleStartRow, visibleEndRow] = @props.visibleRowRange
|
||||
linesNode = @refs.lines.getDOMNode()
|
||||
node = @getDOMNode()
|
||||
|
||||
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
||||
unless @measuredLines.has(tokenizedLine)
|
||||
lineNode = linesNode.children[i]
|
||||
lineNode = node.children[i]
|
||||
@measureCharactersInLine(tokenizedLine, lineNode)
|
||||
|
||||
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
||||
|
||||
@@ -10,6 +10,7 @@ SelectionsComponent = React.createClass
|
||||
{editor} = @props
|
||||
|
||||
div className: 'selections',
|
||||
for selection in editor.getSelections()
|
||||
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
SelectionComponent({key: selection.id, selection})
|
||||
if @isMounted()
|
||||
for selection in editor.getSelections()
|
||||
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||
SelectionComponent({key: selection.id, selection})
|
||||
|
||||
Reference in New Issue
Block a user