Compute ranges of cursors & selections in EditorComponent and pass down

Previously, SelectionsComponenet::shouldComponentUpdate was storing the
ranges for selections as a side effect. We also were passing boolean
values (cursorMoved and selectionUpdated) to determine if these
components should update.

Now, we compute a simple hash of screen ranges for selections and
cursors in the root component and pass them down. This simplifies
shouldComponentUpdate for selections and allows us to implement one
for cursors.
This commit is contained in:
Nathan Sobo
2014-06-02 11:09:29 +09:00
parent e222998f82
commit 1bce626324
6 changed files with 84 additions and 73 deletions

View File

@@ -317,7 +317,7 @@ describe "EditorComponent", ->
expect(cursorNodes[0].offsetWidth).toBe charWidth
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)"
cursor2 = editor.addCursorAtScreenPosition([6, 11])
cursor2 = editor.addCursorAtScreenPosition([8, 11])
cursor3 = editor.addCursorAtScreenPosition([4, 10])
cursorNodes = node.querySelectorAll('.cursor')
@@ -326,15 +326,15 @@ describe "EditorComponent", ->
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{5 * charWidth}px, #{0 * lineHeightInPixels}px, 0px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{10 * charWidth}px, #{4 * lineHeightInPixels}px, 0px)"
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
horizontalScrollbarNode.scrollLeft = 3.5 * charWidth
horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(6 - 2.5) * lineHeightInPixels}px, 0px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 2.5) * lineHeightInPixels}px, 0px)"
expect(cursorNodes[0].style['-webkit-transform']).toBe "translate3d(#{(11 - 3.5) * charWidth}px, #{(8 - 4.5) * lineHeightInPixels}px, 0px)"
expect(cursorNodes[1].style['-webkit-transform']).toBe "translate3d(#{(10 - 3.5) * charWidth}px, #{(4 - 4.5) * lineHeightInPixels}px, 0px)"
cursor3.destroy()
cursorNodes = node.querySelectorAll('.cursor')
@@ -364,6 +364,7 @@ describe "EditorComponent", ->
expect(cursorsNode.classList.contains('blink-off')).toBe false
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorsNode.classList.contains('blink-off')).toBe true
advanceClock(component.props.cursorBlinkPeriod / 2)
expect(cursorsNode.classList.contains('blink-off')).toBe false

View File

@@ -6,8 +6,8 @@ CursorComponent = React.createClass
displayName: 'CursorComponent'
render: ->
{cursor, scrollTop, scrollLeft} = @props
{top, left, height, width} = cursor.getPixelRect()
{editor, screenRange, scrollTop, scrollLeft} = @props
{top, left, height, width} = editor.pixelRectForScreenRange(screenRange)
top -= scrollTop
left -= scrollLeft
WebkitTransform = "translate3d(#{left}px, #{top}px, 0px)"

View File

@@ -1,6 +1,6 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{debounce, toArray} = require 'underscore-plus'
{debounce, toArray, isEqualForProperties, isEqual} = require 'underscore-plus'
SubscriberMixin = require './subscriber-mixin'
CursorComponent = require './cursor-component'
@@ -12,7 +12,7 @@ CursorsComponent = React.createClass
cursorBlinkIntervalHandle: null
render: ->
{editor, scrollTop, scrollLeft} = @props
{editor, cursorScreenRanges, scrollTop, scrollLeft} = @props
{blinkOff} = @state
className = 'cursors'
@@ -20,10 +20,8 @@ CursorsComponent = React.createClass
div {className},
if @isMounted()
for selection in editor.getSelections()
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
{cursor} = selection
CursorComponent({key: cursor.id, cursor, scrollTop, scrollLeft})
for key, screenRange of cursorScreenRanges
CursorComponent({key, editor, screenRange, scrollTop, scrollLeft})
getInitialState: ->
blinkOff: false
@@ -34,8 +32,12 @@ CursorsComponent = React.createClass
componentWillUnmount: ->
@stopBlinkingCursors()
componentWillUpdate: ({cursorsMoved}) ->
@pauseCursorBlinking() if cursorsMoved
shouldComponentUpdate: (newProps, newState) ->
not isEqualForProperties(newProps, @props, 'cursorScreenRanges', 'scrollTop', 'scrollLeft') or
not newState.blinkOff is @state.blinkOff
componentWillUpdate: (newProps) ->
@pauseCursorBlinking() if @props.cursorScreenRanges and not isEqual(newProps.cursorScreenRanges, @props.cursorScreenRanges)
startBlinkingCursors: ->
@toggleCursorBlinkHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2) if @isMounted()

View File

@@ -45,6 +45,8 @@ EditorComponent = React.createClass
if @isMounted()
renderedRowRange = @getRenderedRowRange()
[renderedStartRow, renderedEndRow] = renderedRowRange
cursorScreenRanges = @getCursorScreenRanges(renderedRowRange)
selectionScreenRanges = @getSelectionScreenRanges(renderedRowRange)
scrollHeight = editor.getScrollHeight()
scrollWidth = editor.getScrollWidth()
scrollTop = editor.getScrollTop()
@@ -79,11 +81,14 @@ EditorComponent = React.createClass
onFocus: @onInputFocused
onBlur: @onInputBlurred
CursorsComponent({editor, scrollTop, scrollLeft, @cursorsMoved, @selectionAdded, cursorBlinkPeriod, cursorBlinkResumeDelay})
CursorsComponent {
editor, scrollTop, scrollLeft, cursorScreenRanges, cursorBlinkPeriod, cursorBlinkResumeDelay,
fontSize, fontFamily, lineHeightInPixels
}
LinesComponent {
ref: 'lines', editor, fontSize, fontFamily, lineHeight, lineHeightInPixels,
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft, @scrollingVertically,
@selectionChanged, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles,
selectionScreenRanges, scrollHeight, scrollWidth, mouseWheelScreenRow, invisibles,
visible, scrollViewHeight
}
@@ -159,13 +164,61 @@ EditorComponent = React.createClass
componentDidUpdate: ->
@pendingChanges.length = 0
@cursorsMoved = false
@selectionChanged = false
@selectionAdded = false
@refreshingScrollbars = false
@measureScrollbars() if @measuringScrollbars
@pauseOverflowChangedEvents()
@props.parentView.trigger 'editor:display-updated'
requestUpdate: ->
if @batchingUpdates
@updateRequested = true
else
@forceUpdate()
getRenderedRowRange: ->
{editor, lineOverdrawMargin} = @props
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin)
[renderedStartRow, renderedEndRow]
getHiddenInputPosition: ->
{editor} = @props
{focused} = @state
return {top: 0, left: 0} unless @isMounted() and focused and editor.getCursor()?
{top, left, height, width} = editor.getCursor().getPixelRect()
width = 2 if width is 0 # Prevent autoscroll at the end of longest line
top -= editor.getScrollTop()
left -= editor.getScrollLeft()
top = Math.max(0, Math.min(editor.getHeight() - height, top))
left = Math.max(0, Math.min(editor.getWidth() - width, left))
{top, left}
getCursorScreenRanges: (renderedRowRange) ->
{editor} = @props
[renderedStartRow, renderedEndRow] = renderedRowRange
cursorScreenRanges = {}
for selection in editor.getSelections() when selection.isEmpty()
{cursor} = selection
screenRange = cursor.getScreenRange()
if renderedStartRow <= screenRange.start.row < renderedEndRow
cursorScreenRanges[cursor.id] = screenRange
cursorScreenRanges
getSelectionScreenRanges: (renderedRowRange) ->
{editor} = @props
[renderedStartRow, renderedEndRow] = renderedRowRange
selectionScreenRanges = {}
for selection, index in editor.getSelections()
# Rendering artifacts occur on the lines GPU layer if we remove the last selection
screenRange = selection.getScreenRange()
if index is 0 or (not screenRange.isEmpty() and screenRange.intersectsRowRange(renderedStartRow, renderedEndRow))
selectionScreenRanges[selection.id] = screenRange
selectionScreenRanges
observeEditor: ->
{editor} = @props
@subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted
@@ -549,12 +602,6 @@ EditorComponent = React.createClass
# if the editor's content and dimensions require them to be visible.
@requestUpdate()
requestUpdate: ->
if @batchingUpdates
@updateRequested = true
else
@forceUpdate()
pauseOverflowChangedEvents: ->
@overflowChangedEventsPaused = true
@resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500)
@@ -672,23 +719,3 @@ EditorComponent = React.createClass
top = clientY - scrollViewClientRect.top + editor.getScrollTop()
left = clientX - scrollViewClientRect.left + editor.getScrollLeft()
{top, left}
getHiddenInputPosition: ->
{editor} = @props
{focused} = @state
return {top: 0, left: 0} unless @isMounted() and focused and editor.getCursor()?
{top, left, height, width} = editor.getCursor().getPixelRect()
width = 2 if width is 0 # Prevent autoscroll at the end of longest line
top -= editor.getScrollTop()
left -= editor.getScrollLeft()
top = Math.max(0, Math.min(editor.getHeight() - height, top))
left = Math.max(0, Math.min(editor.getWidth() - width, left))
{top, left}
getRenderedRowRange: ->
{editor, lineOverdrawMargin} = @props
[visibleStartRow, visibleEndRow] = editor.getVisibleRowRange()
renderedStartRow = Math.max(0, visibleStartRow - lineOverdrawMargin)
renderedEndRow = Math.min(editor.getScreenLineCount(), visibleEndRow + lineOverdrawMargin)
[renderedStartRow, renderedEndRow]

View File

@@ -17,14 +17,14 @@ LinesComponent = React.createClass
render: ->
if @isMounted()
{editor, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeightInPixels, scrollViewHeight} = @props
{editor, selectionScreenRanges, scrollTop, scrollLeft, scrollHeight, scrollWidth, lineHeightInPixels, scrollViewHeight} = @props
style =
height: Math.max(scrollHeight, scrollViewHeight)
width: scrollWidth
WebkitTransform: "translate3d(#{-scrollLeft}px, #{-scrollTop}px, 0px)"
div {className: 'lines', style},
SelectionsComponent({editor, lineHeightInPixels}) if @isMounted()
SelectionsComponent({editor, selectionScreenRanges, lineHeightInPixels}) if @isMounted()
componentWillMount: ->
@measuredLines = new WeakSet
@@ -36,11 +36,10 @@ LinesComponent = React.createClass
@measureLineHeightInPixelsAndCharWidth()
shouldComponentUpdate: (newProps) ->
return true if newProps.selectionChanged
return true unless isEqualForProperties(newProps, @props,
'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'lineHeightInPixels',
'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'invisibles',
'visible', 'scrollViewHeight', 'mouseWheelScreenRow'
'renderedRowRange', 'selectionScreenRanges', 'fontSize', 'fontFamily', 'lineHeight',
'lineHeightInPixels', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically',
'invisibles', 'visible', 'scrollViewHeight', 'mouseWheelScreenRow'
)
{renderedRowRange, pendingChanges} = newProps

View File

@@ -1,5 +1,6 @@
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
{isEqual} = require 'underscore-plus'
SelectionComponent = require './selection-component'
module.exports =
@@ -10,34 +11,15 @@ SelectionsComponent = React.createClass
div className: 'selections', @renderSelections()
renderSelections: ->
{editor, lineHeightInPixels} = @props
{editor, selectionScreenRanges, lineHeightInPixels} = @props
selectionComponents = []
for selectionId, screenRange of @selectionRanges
for selectionId, screenRange of selectionScreenRanges
selectionComponents.push(SelectionComponent({key: selectionId, screenRange, editor, lineHeightInPixels}))
selectionComponents
componentWillMount: ->
@selectionRanges = {}
shouldComponentUpdate: ->
{editor} = @props
oldSelectionRanges = @selectionRanges
newSelectionRanges = {}
@selectionRanges = newSelectionRanges
for selection, index in editor.getSelections()
# Rendering artifacts occur on the lines GPU layer if we remove the last selection
if index is 0 or (not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection))
newSelectionRanges[selection.id] = selection.getScreenRange()
for id, range of newSelectionRanges
if oldSelectionRanges.hasOwnProperty(id)
return true unless range.isEqual(oldSelectionRanges[id])
else
return true
for id of oldSelectionRanges
return true unless newSelectionRanges.hasOwnProperty(id)
false
shouldComponentUpdate: (newProps) ->
not isEqual(newProps.selectionScreenRanges, @props.selectionScreenRanges)