Render overlay decorations

This commit is contained in:
Nathan Sobo
2017-03-22 08:38:07 -06:00
committed by Antonio Scandurra
parent c7dc567e62
commit b6f71bc648
2 changed files with 172 additions and 7 deletions

View File

@@ -875,6 +875,73 @@ describe('TextEditorComponent', () => {
})
})
describe('overlay decorations', () => {
it('renders overlay elements at the specified screen position unless it would overflow the window', async () => {
const {component, element, editor} = buildComponent({width: 200, height: 100, attach: false})
const fakeWindow = document.createElement('div')
fakeWindow.style.position = 'absolute'
fakeWindow.style.padding = 20 + 'px'
fakeWindow.style.backgroundColor = 'blue'
fakeWindow.appendChild(element)
jasmine.attachToDOM(fakeWindow)
component.getWindowInnerWidth = () => fakeWindow.getBoundingClientRect().width
component.getWindowInnerHeight = () => fakeWindow.getBoundingClientRect().height
// spyOn(component, 'getWindowInnerWidth').andCallFake(() => fakeWindow.getBoundingClientRect().width)
// spyOn(component, 'getWindowInnerHeight').andCallFake(() => fakeWindow.getBoundingClientRect().height)
await setScrollTop(component, 50)
await setScrollLeft(component, 100)
const marker = editor.markScreenPosition([4, 25])
const overlayElement = document.createElement('div')
overlayElement.style.width = '50px'
overlayElement.style.height = '50px'
overlayElement.style.margin = '3px'
overlayElement.style.backgroundColor = 'red'
editor.decorateMarker(marker, {type: 'overlay', item: overlayElement})
await component.getNextUpdatePromise()
const overlayWrapper = overlayElement.parentElement
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
expect(overlayWrapper.getBoundingClientRect().left).toBe(clientLeftForCharacter(component, 4, 25))
// Updates the horizontal position on scroll
await setScrollLeft(component, 150)
expect(overlayWrapper.getBoundingClientRect().left).toBe(clientLeftForCharacter(component, 4, 25))
// Shifts the overlay horizontally to ensure the overlay element does not
// overflow the window
await setScrollLeft(component, 30)
expect(overlayElement.getBoundingClientRect().right).toBe(fakeWindow.getBoundingClientRect().right)
await setScrollLeft(component, 280)
expect(overlayElement.getBoundingClientRect().left).toBe(fakeWindow.getBoundingClientRect().left)
// Updates the vertical position on scroll
await setScrollTop(component, 60)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
// Flips the overlay vertically to ensure the overlay element does not
// overflow the bottom of the window
setScrollLeft(component, 100)
await setScrollTop(component, 0)
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
// Flips the overlay vertically on overlay resize if necessary
await setScrollTop(component, 20)
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
overlayElement.style.height = 60 + 'px'
await component.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
// Does not flip the overlay vertically if it would overflow the top of the window
overlayElement.style.height = 80 + 'px'
await component.getNextUpdatePromise()
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
})
})
describe('mouse input', () => {
describe('on the lines', () => {
it('positions the cursor on single-click', async () => {

View File

@@ -1,7 +1,7 @@
const etch = require('etch')
const {CompositeDisposable} = require('event-kit')
const {Point, Range} = require('text-buffer')
const resizeDetector = require('element-resize-detector')({strategy: 'scroll'})
const ResizeDetector = require('element-resize-detector')
const TextEditor = require('./text-editor')
const {isPairedCharacter} = require('./text-utils')
const $ = etch.dom
@@ -47,6 +47,9 @@ class TextEditorComponent {
this.virtualNode.domNode = this.element
this.refs = {}
this.updateSync = this.updateSync.bind(this)
this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this)
this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this)
this.disposables = new CompositeDisposable()
this.updateScheduled = false
this.measurements = null
@@ -55,8 +58,6 @@ class TextEditorComponent {
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this)
this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this)
this.scrollbarsVisible = true
this.refreshScrollbarStyling = false
this.pendingAutoscroll = null
@@ -73,7 +74,8 @@ class TextEditorComponent {
lineNumbers: new Map(),
lines: new Map(),
highlights: new Map(),
cursors: []
cursors: [],
overlays: []
}
this.decorationsToMeasure = {
highlights: new Map(),
@@ -81,7 +83,7 @@ class TextEditorComponent {
}
this.observeModel()
resizeDetector.listenTo(this.element, this.didResize.bind(this))
getElementResizeDetector().listenTo(this.element, this.didResize.bind(this))
etch.updateSync(this)
}
@@ -164,7 +166,7 @@ class TextEditorComponent {
const style = {}
if (!model.getAutoHeight() && !model.getAutoWidth()) {
style.contain = 'strict'
style.contain = 'size'
}
if (this.measurements) {
@@ -210,7 +212,8 @@ class TextEditorComponent {
},
this.renderGutterContainer(),
this.renderScrollContainer()
)
),
this.renderOverlayDecorations()
)
}
@@ -571,6 +574,12 @@ class TextEditorComponent {
}
}
renderOverlayDecorations () {
return this.decorationsToRender.overlays.map((overlayProps) =>
$(OverlayComponent, Object.assign({didResize: this.updateSync}, overlayProps))
)
}
// This is easier to mock
getPlatform () {
return process.platform
@@ -590,6 +599,7 @@ class TextEditorComponent {
queryDecorationsToRender () {
this.decorationsToRender.lineNumbers.clear()
this.decorationsToRender.lines.clear()
this.decorationsToRender.overlays.length = 0
this.decorationsToMeasure.highlights.clear()
this.decorationsToMeasure.cursors.length = 0
@@ -626,6 +636,9 @@ class TextEditorComponent {
case 'cursor':
this.addCursorDecorationToMeasure(marker, screenRange, reversed)
break
case 'overlay':
this.addOverlayDecorationToRender(decoration, marker)
break
}
}
}
@@ -714,9 +727,24 @@ class TextEditorComponent {
this.decorationsToMeasure.cursors.push({screenPosition, columnWidth, isLastCursor})
}
addOverlayDecorationToRender (decoration, marker) {
const {class: className, item, position} = decoration
const element = atom.views.getView(item)
const screenPosition = (position === 'tail')
? marker.getTailScreenPosition()
: marker.getHeadScreenPosition()
this.requestHorizontalMeasurement(screenPosition.row, screenPosition.column)
this.decorationsToRender.overlays.push({
key: element,
className, element, screenPosition
})
}
updateAbsolutePositionedDecorations () {
this.updateHighlightsToRender()
this.updateCursorsToRender()
this.updateOverlaysToRender()
}
updateHighlightsToRender () {
@@ -755,6 +783,43 @@ class TextEditorComponent {
}
}
updateOverlaysToRender () {
const overlayCount = this.decorationsToRender.overlays.length
if (overlayCount === 0) return null
const windowInnerHeight = this.getWindowInnerHeight()
const windowInnerWidth = this.getWindowInnerWidth()
const contentClientRect = this.refs.content.getBoundingClientRect()
for (let i = 0; i < overlayCount; i++) {
const decoration = this.decorationsToRender.overlays[i]
const {element, screenPosition} = decoration
const {row, column} = screenPosition
const computedStyle = window.getComputedStyle(element)
let wrapperTop = contentClientRect.top + this.pixelTopForRow(row) + this.getLineHeight()
const elementHeight = element.offsetHeight
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
const elementBottom = elementTop + elementHeight
const flippedElementTop = wrapperTop - this.getLineHeight() - elementHeight - parseInt(computedStyle.marginBottom)
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
wrapperTop -= (elementTop - flippedElementTop)
}
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
const elementRight = elementLeft + element.offsetWidth
if (elementLeft < 0) {
wrapperLeft -= elementLeft
} else if (elementRight > windowInnerWidth) {
wrapperLeft -= (elementRight - windowInnerWidth)
}
decoration.pixelTop = wrapperTop
decoration.pixelLeft = wrapperLeft
}
}
didAttach () {
if (!this.attached) {
this.attached = true
@@ -1525,6 +1590,14 @@ class TextEditorComponent {
return this.element.offsetWidth > 0 || this.element.offsetHeight > 0
}
getWindowInnerHeight () {
return window.innerHeight
}
getWindowInnerWidth () {
return window.innerWidth
}
getLineHeight () {
return this.measurements.lineHeight
}
@@ -2278,6 +2351,25 @@ class HighlightComponent {
}
}
class OverlayComponent {
constructor (props) {
this.props = props
this.element = document.createElement('atom-overlay')
this.element.appendChild(this.props.element)
this.element.style.position = 'fixed'
this.element.style.zIndex = 4
this.element.style.top = (this.props.pixelTop || 0) + 'px'
this.element.style.left = (this.props.pixelLeft || 0) + 'px'
getElementResizeDetector().listenTo(this.element, this.props.didResize)
}
update (props) {
this.props = props
if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'
if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'
}
}
const classNamesByScopeName = new Map()
function classNameForScopeName (scopeName) {
let classString = classNamesByScopeName.get(scopeName)
@@ -2296,6 +2388,12 @@ function clientRectForRange (textNode, startIndex, endIndex) {
return rangeForMeasurement.getBoundingClientRect()
}
let resizeDetector
function getElementResizeDetector () {
if (resizeDetector == null) resizeDetector = ResizeDetector({strategy: 'scroll'})
return resizeDetector
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false
for (let i = 0, length = a.length; i < length; i++) {