/** * 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 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, Range) 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, columnRange: Range) -> [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") } }