mirror of
https://github.com/atom/atom.git
synced 2026-01-24 22:38:20 -05:00
Render overlay decorations
This commit is contained in:
committed by
Antonio Scandurra
parent
c7dc567e62
commit
b6f71bc648
@@ -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 () => {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user