1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-12-26 07:13:24 +03:00
vimr/SwiftNeoVim/NeoVimView.swift
2016-07-14 20:18:30 +02:00

293 lines
9.9 KiB
Swift

/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Cocoa
/// Contiguous piece of cells of a row that has the same attributes.
private struct RowRun: CustomStringConvertible {
let row: Int
let range: Range<Int>
let attrs: CellAttributes
var description: String {
return "RowRun<\(row): \(range)\n\(attrs)>"
}
}
public class NeoVimView: NSView {
public let uuid = NSUUID().UUIDString
public var delegate: NeoVimViewDelegate?
let agent: NeoVimAgent
let grid = Grid()
var markedText: String?
/// We store the last marked text because Cocoa's text input system does the following:
/// -> hanja popup -> insertText() -> attributedSubstring...() -> setMarkedText() -> ...
/// We want to return "" in attributedSubstring...()
var lastMarkedText: String?
var markedPosition = Position.null
var keyDownDone = true
var lastClickedCellPosition = Position.null
var xOffset = CGFloat(0)
var yOffset = CGFloat(0)
var cellSize = CGSize.zero
var descent = CGFloat(0)
var leading = CGFloat(0)
private let drawer: TextDrawer
private var font: NSFont {
didSet {
self.drawer.font = self.font
self.cellSize = self.drawer.cellSize
self.descent = self.drawer.descent
self.leading = self.drawer.leading
// We assume that the font is valid, eg fixed width, not too small, not too big, etc..
self.resizeNeoVimUiTo(size: self.frame.size)
}
}
override init(frame rect: NSRect = CGRect.zero) {
self.font = NSFont(name: "Menlo", size: 16)!
self.drawer = TextDrawer(font: font)
self.agent = NeoVimAgent(uuid: self.uuid)
super.init(frame: rect)
self.wantsLayer = true
self.cellSize = self.drawer.cellSize
self.descent = self.drawer.descent
self.leading = self.drawer.leading
// We cannot set bridge in init since self is not available before super.init()...
self.agent.bridge = self
self.agent.establishLocalServer()
}
// deinit would have been ideal for this, but if you quit the app, deinit does not necessarily get called...
public func cleanUp() {
self.agent.cleanUp()
}
public func debugInfo() {
Swift.print(self.grid)
}
public func setFont(font: NSFont) {
guard font.fixedPitch else {
return
}
// FIXME: check the size whether too small or too big!
self.font = font
}
override public func setFrameSize(newSize: NSSize) {
super.setFrameSize(newSize)
// initial resizing is done when grid has data
guard self.grid.hasData else {
return
}
if self.inLiveResize {
// TODO: Turn of live resizing for now.
// self.resizeNeoVimUiTo(size: newSize)
return
}
// There can be cases where the frame is resized not by live resizing, eg when the window is resized by window
// management tools. Thus, we make sure that the resize call is made when this happens.
self.resizeNeoVimUiTo(size: newSize)
}
override public func viewDidEndLiveResize() {
super.viewDidEndLiveResize()
self.resizeNeoVimUiTo(size: self.bounds.size)
}
func resizeNeoVimUiTo(size size: CGSize) {
// NSLog("\(#function): \(size)")
let discreteSize = Size(width: Int(floor(size.width / self.cellSize.width)),
height: Int(floor(size.height / self.cellSize.height)))
self.xOffset = floor((size.width - self.cellSize.width * CGFloat(discreteSize.width)) / 2)
self.yOffset = floor((size.height - self.cellSize.height * CGFloat(discreteSize.height)) / 2)
self.agent.resizeToWidth(Int32(discreteSize.width), height: Int32(discreteSize.height))
}
override public func drawRect(dirtyUnionRect: NSRect) {
guard self.grid.hasData else {
return
}
if self.inLiveResize {
NSColor.lightGrayColor().set()
dirtyUnionRect.fill()
return
}
// NSLog("\(#function): \(dirtyUnionRect)")
let context = NSGraphicsContext.currentContext()!.CGContext
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextSetTextDrawingMode(context, .Fill);
let dirtyRects = self.rectsBeingDrawn()
self.rowRunIntersecting(rects: dirtyRects).forEach { rowFrag in
// For background drawing we don't filter out the put(0, 0)s: in some cases only the put(0, 0)-cells should be
// redrawn. => FIXME: probably we have to consider this also when drawing further down, ie when the range starts
// with '0'...
self.drawBackground(positions: rowFrag.range.map { self.pointInViewFor(row: rowFrag.row, column: $0) },
background: rowFrag.attrs.background)
let positions = rowFrag.range
// filter out the put(0, 0)s (after a wide character)
.filter { self.grid.cells[rowFrag.row][$0].string.characters.count > 0 }
.map { self.pointInViewFor(row: rowFrag.row, column: $0) }
if positions.isEmpty {
return
}
let string = self.grid.cells[rowFrag.row][rowFrag.range].reduce("") { $0 + $1.string }
let glyphPositions = positions.map { CGPoint(x: $0.x, y: $0.y + self.descent + self.leading) }
self.drawer.drawString(string,
positions: UnsafeMutablePointer(glyphPositions), positionsCount: positions.count,
highlightAttrs: rowFrag.attrs,
context: context)
}
self.drawCursor(self.grid.background)
}
private func drawCursor(background: UInt32) {
// FIXME: for now do some rudimentary cursor drawing
let cursorPosition = self.grid.putPosition
// NSLog("\(#function): \(cursorPosition)")
var cursorRect = self.cellRectFor(row: cursorPosition.row, column: cursorPosition.column)
if self.grid.isNextCellEmpty(cursorPosition) {
let nextPosition = self.grid.nextCellPosition(cursorPosition)
cursorRect = cursorRect.union(self.cellRectFor(row: nextPosition.row, column:nextPosition.column))
}
ColorUtils.colorIgnoringAlpha(background).set()
NSRectFillUsingOperation(cursorRect, .CompositeDifference)
}
private func drawBackground(positions positions: [CGPoint], background: UInt32) {
ColorUtils.colorIgnoringAlpha(background).set()
let backgroundRect = CGRect(
x: positions[0].x, y: positions[0].y,
width: CGFloat(positions.count) * self.cellSize.width, height: self.cellSize.height
)
backgroundRect.fill()
}
private func rowRunIntersecting(rects rects: [CGRect]) -> [RowRun] {
return rects
.map { rect -> (Range<Int>, Range<Int>) in
// Get all Regions that intersects with the given rects. There can be overlaps between the Regions, but for the
// time being we ignore them; probably not necessary to optimize them away.
let region = self.regionFor(rect: rect)
return (region.rowRange, region.columnRange)
}
.map { self.rowRunsFor(rowRange: $0, columnRange: $1) } // All RowRuns for all Regions grouped by their row range.
.flatMap { $0 } // Flattened RowRuns for all Regions.
}
private func rowRunsFor(rowRange rowRange: Range<Int>, columnRange: Range<Int>) -> [RowRun] {
return rowRange
.map { (row) -> [RowRun] in
let rowCells = self.grid.cells[row]
let startIdx = columnRange.startIndex
var result = [ RowRun(row: row, range: startIdx...startIdx, attrs: rowCells[startIdx].attrs) ]
columnRange.forEach { idx in
if rowCells[idx].attrs == result.last!.attrs {
let last = result.popLast()!
result.append(RowRun(row: row, range: last.range.startIndex...idx, attrs: last.attrs))
} else {
result.append(RowRun(row: row, range: idx...idx, attrs: rowCells[idx].attrs))
}
}
return result // All RowRuns for a row in a Region.
} // All RowRuns for all rows in a Region grouped by row.
.flatMap { $0 } // Flattened RowRuns for a Region.
}
private func regionFor(rect rect: CGRect) -> Region {
let rowStart = max(
Int(floor((self.frame.height - (rect.origin.y + rect.size.height)) / self.cellSize.height)), 0
)
let rowEnd = min(
Int(ceil((self.frame.height - rect.origin.y) / self.cellSize.height)) - 1, self.grid.size.height - 1
)
let columnStart = max(
Int(floor(rect.origin.x / self.cellSize.width)), 0
)
let columnEnd = min(
Int(ceil((rect.origin.x + rect.size.width) / self.cellSize.width)) - 1, self.grid.size.width - 1
)
return Region(top: rowStart, bottom: rowEnd, left: columnStart, right: columnEnd)
}
func pointInViewFor(position position: Position) -> CGPoint {
return self.pointInViewFor(row: position.row, column: position.column)
}
func pointInViewFor(row row: Int, column: Int) -> CGPoint {
return CGPoint(
x: CGFloat(column) * self.cellSize.width + self.xOffset,
y: self.frame.size.height - CGFloat(row) * self.cellSize.height - self.cellSize.height - self.yOffset
)
}
func cellRectFor(row row: Int, column: Int) -> CGRect {
return CGRect(origin: self.pointInViewFor(row: row, column: column), size: self.cellSize)
}
func regionRectFor(region region: Region) -> CGRect {
let top = CGFloat(region.top)
let bottom = CGFloat(region.bottom)
let left = CGFloat(region.left)
let right = CGFloat(region.right)
let width = right - left + 1
let height = bottom - top + 1
return CGRect(
x: left * self.cellSize.width + self.xOffset,
y: (CGFloat(self.grid.size.height) - bottom) * self.cellSize.height - self.yOffset,
width: width * self.cellSize.width,
height: height * self.cellSize.height
)
}
func wrapNamedKeys(string: String) -> String {
return "<\(string)>"
}
func vimPlainString(string: String) -> String {
return string.stringByReplacingOccurrencesOfString("<", withString: self.wrapNamedKeys("lt"))
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}