Skip to content

Commit

Permalink
Page Up/Down Works
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter committed Jun 15, 2024
1 parent 3fe8670 commit b51f8e2
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 248 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ public class TextLayoutManager: NSObject {
// MARK: - Layout

/// Lays out all visible lines
func layoutLines() { // swiftlint:disable:this function_body_length
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
guard layoutView?.superview != nil,
let visibleRect = delegate?.visibleRect,
let visibleRect = rect ?? delegate?.visibleRect,
!isInTransaction,
let textStorage else {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ package extension TextSelectionManager {
case .word, .line, .visualLine:
return extendSelectionVerticalLine(from: offset, up: up)
case .page:
return extendSelectionPage(from: offset, delta: up ? 1 : -1)
return extendSelectionPage(from: offset, delta: up ? 1 : -1, suggestedXPos: suggestedXPos)
case .document:
if up {
return NSRange(location: 0, length: offset)
Expand All @@ -61,7 +61,7 @@ package extension TextSelectionManager {
guard let point = layoutManager?.rectForOffset(offset)?.origin,
let newOffset = layoutManager?.textOffsetAtPoint(
CGPoint(
x: suggestedXPos == nil ? point.x : suggestedXPos!,
x: suggestedXPos ?? point.x,
y: point.y - (layoutManager?.estimateLineHeight() ?? 2.0)/2 * (up ? 1 : -3)
)
) else {
Expand Down Expand Up @@ -120,17 +120,29 @@ package extension TextSelectionManager {
/// - offset: The location to start extending the selection from.
/// - delta: The direction the selection should be extended. `1` for forwards, `-1` for backwards.
/// - Returns: The range of the extended selection.
private func extendSelectionPage(from offset: Int, delta: Int) -> NSRange {
guard let textView, let endOffset = layoutManager?.textOffsetAtPoint(
CGPoint(
x: delta > 0 ? textView.frame.maxX : textView.frame.minX,
y: delta > 0 ? textView.frame.maxY : textView.frame.minY
)
) else {
private func extendSelectionPage(from offset: Int, delta: Int, suggestedXPos: CGFloat?) -> NSRange {
guard let textView = textView,
let layoutManager,
let currentYPos = layoutManager.rectForOffset(offset)?.origin.y else {
return NSRange(location: offset, length: 0)
}

let pageHeight = textView.visibleRect.height

// Grab the line where the next selection should be. Then use the suggestedXPos to find where in the line the
// selection should be extended to.
layoutManager.layoutLines(in: NSRect(x: 0, y: currentYPos, width: layoutManager.maxLineWidth, height: pageHeight))
guard let nextPageOffset = layoutManager.textOffsetAtPoint(CGPoint(
x: suggestedXPos ?? 0,
y: min(textView.frame.height, max(0, currentYPos + (delta > 0 ? -pageHeight : pageHeight)))
)) else {
return NSRange(location: offset, length: 0)
}
return endOffset > offset
? NSRange(location: offset, length: endOffset - offset)
: NSRange(location: endOffset, length: offset - endOffset)

if delta > 0 {
return NSRange(location: nextPageOffset, length: offset - nextPageOffset)
} else {
return NSRange(location: offset, length: nextPageOffset - offset)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,12 @@ public class TextSelectionManager: NSObject {

let fillRects = getFillRects(in: rect, for: textSelection)

let min = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin ?? .zero
let max = fillRects.max(by: { $0.origin.y < $1.origin.y }) ?? .zero
let size = CGSize(width: max.maxX - min.x, height: max.maxY - min.y)
textSelection.boundingRect = CGRect(origin: min, size: size)
let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0
let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0
let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero
let origin = CGPoint(x: minX, y: minY)
let size = CGSize(width: max.maxX - minX, height: max.maxY - minY)
textSelection.boundingRect = CGRect(origin: origin, size: size)

context.fill(fillRects)
context.restoreGState()
Expand Down
57 changes: 57 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// TextView+FirstResponder.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import AppKit

extension TextView {
open override func becomeFirstResponder() -> Bool {
isFirstResponder = true
selectionManager.cursorTimer.resetTimer()
needsDisplay = true
return super.becomeFirstResponder()
}

open override func resignFirstResponder() -> Bool {
isFirstResponder = false
selectionManager.removeCursors()
needsDisplay = true
return super.resignFirstResponder()
}

open override var canBecomeKeyView: Bool {
super.canBecomeKeyView && acceptsFirstResponder && !isHiddenOrHasHiddenAncestor
}

/// Sent to the window's first responder when `NSWindow.makeKey()` occurs.
@objc private func becomeKeyWindow() {
_ = becomeFirstResponder()
}

/// Sent to the window's first responder when `NSWindow.resignKey()` occurs.
@objc private func resignKeyWindow() {
_ = resignFirstResponder()
}

open override var needsPanelToBecomeKey: Bool {
isSelectable || isEditable
}

open override var acceptsFirstResponder: Bool {
isSelectable
}

open override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return true
}

open override func resetCursorRects() {
super.resetCursorRects()
if isSelectable {
addCursorRect(visibleRect, cursor: .iBeam)
}
}
}
50 changes: 50 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+KeyDown.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// TextView+KeyDown.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import AppKit
import Carbon.HIToolbox

extension TextView {
override public func keyDown(with event: NSEvent) {
guard isEditable else {
super.keyDown(with: event)
return
}

NSCursor.setHiddenUntilMouseMoves(true)

if !(inputContext?.handleEvent(event) ?? false) {
interpretKeyEvents([event])
} else {
// Not handled, ignore so we don't double trigger events.
return
}
}

override public func performKeyEquivalent(with event: NSEvent) -> Bool {
guard isEditable else {
return super.performKeyEquivalent(with: event)
}

switch Int(event.keyCode) {
case kVK_PageUp:
if !event.modifierFlags.contains(.shift) {
self.pageUp(event)
return true
}
case kVK_PageDown:
if !event.modifierFlags.contains(.shift) {
self.pageDown(event)
return true
}
default:
return false
}

return false
}
}
95 changes: 95 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Layout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// TextView+Layout.swift
// CodeEditTextView
//
// Created by Khan Winter on 6/15/24.
//

import Foundation

extension TextView {
open override class var isCompatibleWithResponsiveScrolling: Bool {
true
}

open override func prepareContent(in rect: NSRect) {
needsLayout = true
super.prepareContent(in: rect)
}

override public func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
if isSelectable {
selectionManager.drawSelections(in: dirtyRect)
}
}

override open var isFlipped: Bool {
true
}

override public var visibleRect: NSRect {
if let scrollView {
var rect = scrollView.documentVisibleRect
rect.origin.y += scrollView.contentInsets.top
return rect.pixelAligned
} else {
return super.visibleRect
}
}

public var visibleTextRange: NSRange? {
let minY = max(visibleRect.minY, 0)
let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight())
guard let minYLine = layoutManager.textLineForPosition(minY),
let maxYLine = layoutManager.textLineForPosition(maxY) else {
return nil
}
return NSRange(
location: minYLine.range.location,
length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length
)
}

public func updatedViewport(_ newRect: CGRect) {
if !updateFrameIfNeeded() {
layoutManager.layoutLines()
}
inputContext?.invalidateCharacterCoordinates()
}

@discardableResult
public func updateFrameIfNeeded() -> Bool {
var availableSize = scrollView?.contentSize ?? .zero
availableSize.height -= (scrollView?.contentInsets.top ?? 0) + (scrollView?.contentInsets.bottom ?? 0)
let newHeight = max(layoutManager.estimatedHeight(), availableSize.height)
let newWidth = layoutManager.estimatedWidth()

var didUpdate = false

if newHeight >= availableSize.height && frame.size.height != newHeight {
frame.size.height = newHeight
// No need to update layout after height adjustment
}

if wrapLines && frame.size.width != availableSize.width {
frame.size.width = availableSize.width
didUpdate = true
} else if !wrapLines && frame.size.width != max(newWidth, availableSize.width) {
frame.size.width = max(newWidth, availableSize.width)
didUpdate = true
}

if didUpdate {
needsLayout = true
needsDisplay = true
layoutManager.layoutLines()
}

if isSelectable {
selectionManager?.updateSelectionViews()
}

return didUpdate
}
}
Loading

0 comments on commit b51f8e2

Please sign in to comment.