Render selections

This commit is contained in:
Nathan Sobo
2014-04-03 17:04:19 -06:00
parent 724babdcb6
commit 3d3b72a954
7 changed files with 176 additions and 55 deletions

View File

@@ -42,45 +42,115 @@ describe "EditorComponent", ->
expect(spacers[0].offsetHeight).toBe 2 * lineHeightInPixels
expect(spacers[1].offsetHeight).toBe (editor.getScreenLineCount() - 7) * lineHeightInPixels
it "renders the currently visible cursors", ->
cursor1 = editor.getCursor()
cursor1.setScreenPosition([0, 5])
describe "cursor rendering", ->
it "renders the currently visible cursors", ->
cursor1 = editor.getCursor()
cursor1.setScreenPosition([0, 5])
node.style.height = 4.5 * lineHeightInPixels + 'px'
component.updateAllDimensions()
node.style.height = 4.5 * lineHeightInPixels + 'px'
component.updateAllDimensions()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels
expect(cursorNodes[0].offsetWidth).toBe charWidth
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels
expect(cursorNodes[0].offsetWidth).toBe charWidth
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
cursor2 = editor.addCursorAtScreenPosition([6, 11])
cursor3 = editor.addCursorAtScreenPosition([4, 10])
cursor2 = editor.addCursorAtScreenPosition([6, 11])
cursor3 = editor.addCursorAtScreenPosition([4, 10])
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 0
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeightInPixels
component.onVerticalScroll()
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeightInPixels
component.onVerticalScroll()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 2
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
cursor3.destroy()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
it "accounts for character widths when positioning cursors", ->
atom.config.set('editor.fontFamily', 'sans-serif')
editor.setCursorScreenPosition([0, 16])
cursor = node.querySelector('.cursor')
cursorRect = cursor.getBoundingClientRect()
cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild
range = document.createRange()
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
rangeRect = range.getBoundingClientRect()
expect(cursorRect.left).toBe rangeRect.left
expect(cursorRect.width).toBe rangeRect.width
describe "selection rendering", ->
it "renders 1 region for 1-line selections", ->
# 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
expect(regionRect.height).toBe 1 * lineHeightInPixels
expect(regionRect.left).toBe 6 * charWidth
expect(regionRect.width).toBe 4 * charWidth
it "renders 2 regions for 2-line selections", ->
editor.setSelectedScreenRange([[1, 6], [2, 10]])
regions = node.querySelectorAll('.selection .region')
expect(regions.length).toBe 2
region1Rect = regions[0].getBoundingClientRect()
expect(region1Rect.top).toBe 1 * lineHeightInPixels
expect(region1Rect.height).toBe 1 * lineHeightInPixels
expect(region1Rect.left).toBe 6 * charWidth
expect(region1Rect.right).toBe node.clientWidth
region2Rect = regions[1].getBoundingClientRect()
expect(region2Rect.top).toBe 2 * lineHeightInPixels
expect(region2Rect.height).toBe 1 * lineHeightInPixels
expect(region2Rect.left).toBe 0
expect(region2Rect.width).toBe 10 * charWidth
it "renders 3 regions for selections with more than 2 lines", ->
editor.setSelectedScreenRange([[1, 6], [5, 10]])
regions = node.querySelectorAll('.selection .region')
expect(regions.length).toBe 3
region1Rect = regions[0].getBoundingClientRect()
expect(region1Rect.top).toBe 1 * lineHeightInPixels
expect(region1Rect.height).toBe 1 * lineHeightInPixels
expect(region1Rect.left).toBe 6 * charWidth
expect(region1Rect.right).toBe node.clientWidth
region2Rect = regions[1].getBoundingClientRect()
expect(region2Rect.top).toBe 2 * lineHeightInPixels
expect(region2Rect.height).toBe 3 * lineHeightInPixels
expect(region2Rect.left).toBe 0
expect(region2Rect.right).toBe node.clientWidth
region3Rect = regions[2].getBoundingClientRect()
expect(region3Rect.top).toBe 5 * lineHeightInPixels
expect(region3Rect.height).toBe 1 * lineHeightInPixels
expect(region3Rect.left).toBe 0
expect(region3Rect.width).toBe 10 * charWidth
cursor3.destroy()
cursorNodes = node.querySelectorAll('.cursor')
expect(cursorNodes.length).toBe 1
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
it "updates the scroll bar when the scrollTop is changed in the model", ->
node.style.height = 4.5 * lineHeightInPixels + 'px'
@@ -92,22 +162,6 @@ describe "EditorComponent", ->
editor.setScrollTop(10)
expect(scrollbarNode.scrollTop).toBe 10
it "accounts for character widths when positioning cursors", ->
atom.config.set('editor.fontFamily', 'sans-serif')
editor.setCursorScreenPosition([0, 16])
cursor = node.querySelector('.cursor')
cursorRect = cursor.getBoundingClientRect()
cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild
range = document.createRange()
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
rangeRect = range.getBoundingClientRect()
expect(cursorRect.left).toBe rangeRect.left
expect(cursorRect.width).toBe rangeRect.width
it "transfers focus to the hidden input", ->
expect(document.activeElement).toBe document.body
node.focus()

View File

@@ -1,7 +1,16 @@
{React, div} = require 'reactionary'
SubscriberMixin = require './subscriber-mixin'
module.exports =
CursorComponent = React.createClass
mixins: [SubscriberMixin]
render: ->
{top, left, height, width} = @props.cursor.getPixelRect()
div className: 'cursor', style: {top, left, height, width}
componentDidMount: ->
@subscribe @props.cursor, 'moved', => @forceUpdate()
componentWillUnmount: ->
@unsubscribe()

View File

@@ -109,7 +109,7 @@ class DisplayBuffer extends Model
getScrollTop: -> @scrollTop
setScrollTop: (scrollTop) ->
@scrollTop = Math.min(@getScrollHeight(), Math.max(0, scrollTop))
@scrollTop = Math.min(@getScrollHeight() - @getHeight(), Math.max(0, scrollTop))
getScrollBottom: -> @scrollTop + @height
setScrollBottom: (scrollBottom) ->

View File

@@ -1,8 +1,9 @@
{React, div, span} = require 'reactionary'
{$$} = require 'space-pen'
SelectionComponent = require './selection-component'
InputComponent = require './input-component'
SelectionComponent = require './selection-component'
CursorComponent = require './cursor-component'
CustomEventMixin = require './custom-event-mixin'
SubscriberMixin = require './subscriber-mixin'
@@ -23,7 +24,7 @@ EditorCompont = React.createClass
{fontSize, lineHeight, fontFamily} = @state
{editor} = @props
div className: 'editor', tabIndex: -1, style: {fontSize, lineHeight, fontFamily},
div className: 'editor react', tabIndex: -1, style: {fontSize, lineHeight, fontFamily},
div className: 'scroll-view', ref: 'scrollView',
InputComponent ref: 'hiddenInput', className: 'hidden-input', onInput: @onInput
@renderScrollableContent()
@@ -38,13 +39,14 @@ EditorCompont = React.createClass
div className: 'scrollable-content', style: {height, WebkitTransform},
@renderOverlayer()
@renderVisibleLines()
@renderUnderlayer()
renderOverlayer: ->
{editor} = @props
div className: 'overlayer',
for selection in @props.editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
SelectionComponent({selection})
for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
CursorComponent(cursor: selection.cursor)
renderVisibleLines: ->
{editor} = @props
@@ -60,6 +62,13 @@ EditorCompont = React.createClass
div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight}
]
renderUnderlayer: ->
{editor} = @props
div className: 'underlayer',
for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection)
SelectionComponent({selection})
getInitialState: -> {}
getDefaultProps: -> updateSync: true

View File

@@ -1,6 +1,5 @@
{React, div} = require 'reactionary'
SubscriberMixin = require './subscriber-mixin'
CursorComponent = require './cursor-component'
module.exports =
SelectionComponent = React.createClass
@@ -8,7 +7,8 @@ SelectionComponent = React.createClass
render: ->
div className: 'selection',
CursorComponent(cursor: @props.selection.cursor)
for regionRect, i in @props.selection.getRegionRects()
div className: 'region', key: i, style: regionRect
componentDidMount: ->
@subscribe @props.selection, 'screen-range-changed', => @forceUpdate()

View File

@@ -570,6 +570,47 @@ class Selection
compare: (otherSelection) ->
@getBufferRange().compare(otherSelection.getBufferRange())
# Get the pixel dimensions of rectangular regions that cover selection's area
# on the screen. Used by SelectionComponent for rendering.
getRegionRects: ->
lineHeight = @editor.getLineHeight()
{start, end} = @getScreenRange()
rowCount = end.row - start.row + 1
startPixelPosition = @editor.pixelPositionForScreenPosition(start)
endPixelPosition = @editor.pixelPositionForScreenPosition(end)
if rowCount is 1
# Single line selection
rects = [{
top: startPixelPosition.top
height: lineHeight
left: startPixelPosition.left
width: endPixelPosition.left - startPixelPosition.left
}]
else
# Multi-line selection
rects = []
# First row, extending from selection start to the right side of screen
rects.push {
top: startPixelPosition.top
left: startPixelPosition.left
height: lineHeight
right: 0
}
if rowCount > 2
# Middle rows, extending from left side to right side of screen
rects.push {
top: startPixelPosition.top + lineHeight
height: (rowCount - 2) * lineHeight
left: 0
right: 0
}
# Last row, extending from left side of screen to selection end
rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left }
rects
screenRangeChanged: ->
screenRange = @getScreenRange()
@emit 'screen-range-changed', screenRange

View File

@@ -2,6 +2,14 @@
@import "octicon-utf-codes";
@import "octicon-mixins";
.editor.react .underlayer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.editor {
overflow: hidden;
cursor: text;