Merge pull request #4144 from atom/bo-overlay-decoration

Overlay decorations
This commit is contained in:
Ben Ogle
2014-11-12 15:02:40 -08:00
7 changed files with 301 additions and 6 deletions

View File

@@ -1203,6 +1203,221 @@ describe "TextEditorComponent", ->
expect(componentNode.querySelector('.test-highlight')).toBeFalsy()
expect(componentNode.querySelector('.new-test-highlight')).toBeTruthy()
describe "overlay decoration rendering", ->
[item] = []
beforeEach ->
item = document.createElement('div')
item.classList.add 'overlay-test'
describe "when the marker is empty", ->
it "renders an overlay decoration when added and removes the overlay when the decoration is destroyed", ->
marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test')
expect(overlay).toBe item
decoration.destroy()
nextAnimationFrame()
overlay = component.getTopmostDOMNode().querySelector('atom-overlay .overlay-test')
expect(overlay).toBe null
it "renders in the correct position on initial display and when the marker moves", ->
editor.setCursorBufferPosition([2, 5])
marker = editor.getLastCursor().getMarker()
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([2, 5])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.moveRight()
editor.moveRight()
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([2, 7])
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
describe "when the marker is not empty", ->
it "renders at the head of the marker", ->
marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([2, 10])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
it "renders at the head of the marker when the marker is reversed", ->
marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], invalidate: 'never', reversed: true)
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([2, 5])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
describe "positioning the overlay when near the edge of the editor", ->
[itemWidth, itemHeight] = []
beforeEach ->
itemWidth = 4 * editor.getDefaultCharWidth()
itemHeight = 4 * editor.getLineHeightInPixels()
gutterWidth = componentNode.querySelector('.gutter').offsetWidth
windowWidth = gutterWidth + 30 * editor.getDefaultCharWidth()
windowHeight = 9 * editor.getLineHeightInPixels()
item.style.width = itemWidth + 'px'
item.style.height = itemHeight + 'px'
wrapperNode.style.width = windowWidth + 'px'
wrapperNode.style.height = windowHeight + 'px'
component.measureHeightAndWidth()
nextAnimationFrame()
it "flips horizontally when near the right edge", ->
marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 26])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertText('a')
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 27])
expect(overlay.style.left).toBe position.left - itemWidth + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
it "flips vertically when near the bottom edge", ->
marker = editor.displayBuffer.markBufferRange([[4, 0], [4, 0]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([4, 0])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertNewline()
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([5, 0])
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top - itemHeight + 'px'
describe "when the editor is very small", ->
beforeEach ->
gutterWidth = componentNode.querySelector('.gutter').offsetWidth
windowWidth = gutterWidth + 6 * editor.getDefaultCharWidth()
windowHeight = 6 * editor.getLineHeightInPixels()
wrapperNode.style.width = windowWidth + 'px'
wrapperNode.style.height = windowHeight + 'px'
component.measureHeightAndWidth()
nextAnimationFrame()
it "does not flip horizontally and force the overlay to have a negative left", ->
marker = editor.displayBuffer.markBufferRange([[0, 2], [0, 2]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 2])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertText('a')
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 3])
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
it "does not flip vertically and force the overlay to have a negative top", ->
marker = editor.displayBuffer.markBufferRange([[1, 0], [1, 0]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([1, 0])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertNewline()
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([2, 0])
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
describe "when editor scroll position is not 0", ->
it "flips horizontally when near the right edge", ->
editor.setScrollLeft(2 * editor.getDefaultCharWidth())
marker = editor.displayBuffer.markBufferRange([[0, 28], [0, 28]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 28])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertText('a')
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([0, 29])
expect(overlay.style.left).toBe position.left - itemWidth + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
it "flips vertically when near the bottom edge", ->
editor.setScrollTop(2 * editor.getLineHeightInPixels())
marker = editor.displayBuffer.markBufferRange([[6, 0], [6, 0]], invalidate: 'never')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([6, 0])
overlay = component.getTopmostDOMNode().querySelector('atom-overlay')
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top + editor.getLineHeightInPixels() + 'px'
editor.insertNewline()
nextAnimationFrame()
position = editor.pixelPositionForBufferPosition([7, 0])
expect(overlay.style.left).toBe position.left + 'px'
expect(overlay.style.top).toBe position.top - itemHeight + 'px'
describe "hidden input field", ->
it "renders the hidden input field at the position of the last cursor if the cursor is on screen and the editor is focused", ->
editor.setVerticalScrollMargin(0)

View File

@@ -17,7 +17,7 @@ class Cursor extends Model
visible: true
needsAutoscroll: null
# Instantiated by an {TextEditor}
# Instantiated by a {TextEditor}
constructor: ({@editor, @marker, id}) ->
@emitter = new Emitter
@@ -171,6 +171,10 @@ class Cursor extends Model
Section: Cursor Position Details
###
# Public: Returns the underlying {Marker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
# Public: Identifies if the cursor is surrounded by whitespace.
#
# "Surrounded" here means that the character directly before and after the

View File

@@ -7,6 +7,7 @@ React = require 'react-atom-fork'
Decoration = require './decoration'
CursorsComponent = require './cursors-component'
HighlightsComponent = require './highlights-component'
OverlayManager = require './overlay-manager'
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
@@ -20,7 +21,7 @@ LinesComponent = React.createClass
{performedInitialMeasurement, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
if performedInitialMeasurement
{editor, highlightDecorations, scrollHeight, scrollWidth, placeholderText, backgroundColor} = @props
{editor, overlayDecorations, highlightDecorations, scrollHeight, scrollWidth, placeholderText, backgroundColor} = @props
{lineHeightInPixels, defaultCharWidth, scrollViewHeight, scopedCharacterWidthsChangeCount} = @props
{scrollTop, scrollLeft, cursorPixelRects, mini} = @props
style =
@@ -63,10 +64,17 @@ LinesComponent = React.createClass
insertionPoint.setAttribute('select', '.overlayer')
@getDOMNode().appendChild(insertionPoint)
insertionPoint = document.createElement('content')
insertionPoint.setAttribute('select', 'atom-overlay')
@overlayManager = new OverlayManager(@props.hostElement)
@getDOMNode().appendChild(insertionPoint)
else
@overlayManager = new OverlayManager(@getDOMNode())
shouldComponentUpdate: (newProps) ->
return true unless isEqualForProperties(newProps, @props,
'renderedRowRange', 'lineDecorations', 'highlightDecorations', 'lineHeightInPixels', 'defaultCharWidth',
'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'visible',
'overlayDecorations', 'scrollTop', 'scrollLeft', 'showIndentGuide', 'scrollingVertically', 'visible',
'scrollViewHeight', 'mouseWheelScreenRow', 'scopedCharacterWidthsChangeCount', 'lineWidth', 'useHardwareAcceleration',
'placeholderText', 'performedInitialMeasurement', 'backgroundColor', 'cursorPixelRects'
)
@@ -92,6 +100,8 @@ LinesComponent = React.createClass
@updateLines(@props.lineWidth isnt prevProps.lineWidth)
@measureCharactersInNewLines() if visible and not scrollingVertically
@overlayManager?.render(@props)
clearScreenRowCaches: ->
@screenRowsByLineId = {}
@lineIdsByScreenRow = {}

View File

@@ -0,0 +1,43 @@
module.exports =
class OverlayManager
constructor: (@container) ->
@overlays = {}
render: (props) ->
{editor, overlayDecorations, lineHeightInPixels} = props
existingDecorations = null
for markerId, {isMarkerReversed, headPixelPosition, decorations} of overlayDecorations
for decoration in decorations
@renderOverlay(editor, decoration, headPixelPosition, lineHeightInPixels)
existingDecorations ?= {}
existingDecorations[decoration.id] = true
for id, overlay of @overlays
unless existingDecorations? and id of existingDecorations
@container.removeChild(overlay)
delete @overlays[id]
return
renderOverlay: (editor, decoration, pixelPosition, lineHeightInPixels) ->
item = atom.views.getView(decoration.item)
unless overlay = @overlays[decoration.id]
overlay = @overlays[decoration.id] = document.createElement('atom-overlay')
overlay.appendChild(item)
@container.appendChild(overlay)
itemWidth = item.offsetWidth
itemHeight = item.offsetHeight
left = pixelPosition.left
if left + itemWidth - editor.getScrollLeft() > editor.getWidth() and left - itemWidth >= editor.getScrollLeft()
left -= itemWidth
top = pixelPosition.top + lineHeightInPixels
if top + itemHeight - editor.getScrollTop() > editor.getHeight() and top - itemHeight - lineHeightInPixels >= editor.getScrollTop()
top -= itemHeight + lineHeightInPixels
overlay.style.top = top + 'px'
overlay.style.left = left + 'px'

View File

@@ -50,7 +50,7 @@ TextEditorComponent = React.createClass
render: ->
{focused, showIndentGuide, showLineNumbers, visible} = @state
{editor, mini, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
{editor, mini, cursorBlinkPeriod, cursorBlinkResumeDelay, hostElement} = @props
maxLineNumberDigits = editor.getLineCount().toString().length
hasSelection = editor.getLastSelection()? and !editor.getLastSelection().isEmpty()
style = {}
@@ -66,6 +66,7 @@ TextEditorComponent = React.createClass
decorations = editor.decorationsForScreenRowRange(renderedStartRow, renderedEndRow)
highlightDecorations = @getHighlightDecorations(decorations)
overlayDecorations = @getOverlayDecorations(decorations)
lineDecorations = @getLineDecorations(decorations)
placeholderText = editor.getPlaceholderText() if editor.isEmpty()
visible = @isVisible()
@@ -110,7 +111,8 @@ TextEditorComponent = React.createClass
LinesComponent {
ref: 'lines',
editor, lineHeightInPixels, defaultCharWidth, tokenizedLines, lineDecorations, highlightDecorations,
editor, lineHeightInPixels, defaultCharWidth, tokenizedLines,
lineDecorations, highlightDecorations, overlayDecorations, hostElement,
showIndentGuide, renderedRowRange, @pendingChanges, scrollTop, scrollLeft,
@scrollingVertically, scrollHeight, scrollWidth, mouseWheelScreenRow,
visible, scrollViewHeight, @scopedCharacterWidthsChangeCount, lineWidth, @useHardwareAcceleration,
@@ -351,7 +353,23 @@ TextEditorComponent = React.createClass
endPixelPosition: editor.pixelPositionForScreenPosition(screenRange.end)
decorations: []
filteredDecorations[markerId].decorations.push decorationParams
filteredDecorations
getOverlayDecorations: (decorationsByMarkerId) ->
{editor} = @props
filteredDecorations = {}
for markerId, decorations of decorationsByMarkerId
marker = editor.getMarker(markerId)
headBufferPosition = marker.getHeadBufferPosition()
if marker.isValid()
for decoration in decorations
if decoration.isType('overlay')
decorationParams = decoration.getProperties()
filteredDecorations[markerId] ?=
id: markerId
headPixelPosition: editor.pixelPositionForScreenPosition(headBufferPosition)
decorations: []
filteredDecorations[markerId].decorations.push decorationParams
filteredDecorations
observeEditor: ->

View File

@@ -77,7 +77,6 @@ class TextEditorView extends View
@scrollView = @root.find('.scroll-view')
if atom.config.get('editor.useShadowDOM')
@underlayer = $("<div class='underlayer'></div>").appendTo(this)
@overlayer = $("<div class='overlayer'></div>").appendTo(this)

View File

@@ -12,6 +12,12 @@ atom-text-editor.mini {
max-height: @component-line-height + 2; // +2 for borders
}
atom-overlay {
position: absolute;
display: block;
z-index: 4;
}
// TODO: remove this when the shadow DOM is the default
atom-text-editor .highlight {
background: none;