Skip to content

Commit

Permalink
Fix Page Up/Down Keys (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter authored Jun 16, 2024
1 parent a8cfe19 commit 9e6d94a
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 224 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 Expand Up @@ -299,8 +299,8 @@ public class TextLayoutManager: NSObject {

var height: CGFloat = 0
var width: CGFloat = 0
var relativeMinY = max(layoutData.minY - position.yPos, 0)
var relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)
let relativeMinY = max(layoutData.minY - position.yPos, 0)
let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)

for lineFragmentPosition in line.typesetter.lineFragments.linesStartingAt(
relativeMinY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ package extension TextSelectionManager {
)
case .word:
return extendSelectionWord(string: string, from: offset, delta: delta)
case .line, .container:
case .line:
return extendSelectionLine(string: string, from: offset, delta: delta)
case .visualLine:
return extendSelectionVisualLine(string: string, from: offset, delta: delta)
Expand All @@ -46,6 +46,8 @@ package extension TextSelectionManager {
} else {
return NSRange(location: 0, length: offset)
}
case .page: // Not a valid destination horizontally.
return NSRange(location: offset, length: 0)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ package extension TextSelectionManager {
return extendSelectionVerticalCharacter(from: offset, up: up, suggestedXPos: suggestedXPos)
case .word, .line, .visualLine:
return extendSelectionVerticalLine(from: offset, up: up)
case .container:
return extendSelectionContainer(from: offset, delta: up ? 1 : -1)
case .page:
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 @@ -115,22 +115,36 @@ package extension TextSelectionManager {
}
}

/// Extends a selection one "container" long.
/// Extends a selection one "page" long.
/// - Parameters:
/// - 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 extendSelectionContainer(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)
}
return endOffset > offset
? NSRange(location: offset, length: endOffset - offset)
: NSRange(location: endOffset, length: offset - endOffset)

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)
}

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 @@ -52,8 +52,7 @@ public class TextSelectionManager: NSObject {
case word
case line
case visualLine
/// Eg: Bottom of screen
case container
case page
case document
}

Expand Down Expand Up @@ -323,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
}
}
18 changes: 18 additions & 0 deletions Sources/CodeEditTextView/TextView/TextView+Move.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,22 @@ extension TextView {
selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true)
updateAfterMove()
}

override public func pageUp(_ sender: Any?) {
enclosingScrollView?.pageUp(sender)
}

override public func pageUpAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true)
updateAfterMove()
}

override public func pageDown(_ sender: Any?) {
enclosingScrollView?.pageDown(sender)
}

override public func pageDownAndModifySelection(_ sender: Any?) {
selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true)
updateAfterMove()
}
}
Loading

0 comments on commit 9e6d94a

Please sign in to comment.