Skip to content

Commit 9e6d94a

Browse files
Fix Page Up/Down Keys (#38)
1 parent a8cfe19 commit 9e6d94a

11 files changed

+341
-224
lines changed

Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ public class TextLayoutManager: NSObject {
189189
// MARK: - Layout
190190

191191
/// Lays out all visible lines
192-
func layoutLines() { // swiftlint:disable:this function_body_length
192+
func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length
193193
guard layoutView?.superview != nil,
194-
let visibleRect = delegate?.visibleRect,
194+
let visibleRect = rect ?? delegate?.visibleRect,
195195
!isInTransaction,
196196
let textStorage else {
197197
return
@@ -299,8 +299,8 @@ public class TextLayoutManager: NSObject {
299299

300300
var height: CGFloat = 0
301301
var width: CGFloat = 0
302-
var relativeMinY = max(layoutData.minY - position.yPos, 0)
303-
var relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)
302+
let relativeMinY = max(layoutData.minY - position.yPos, 0)
303+
let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY)
304304

305305
for lineFragmentPosition in line.typesetter.lineFragments.linesStartingAt(
306306
relativeMinY,

Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Horizontal.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ package extension TextSelectionManager {
3636
)
3737
case .word:
3838
return extendSelectionWord(string: string, from: offset, delta: delta)
39-
case .line, .container:
39+
case .line:
4040
return extendSelectionLine(string: string, from: offset, delta: delta)
4141
case .visualLine:
4242
return extendSelectionVisualLine(string: string, from: offset, delta: delta)
@@ -46,6 +46,8 @@ package extension TextSelectionManager {
4646
} else {
4747
return NSRange(location: 0, length: offset)
4848
}
49+
case .page: // Not a valid destination horizontally.
50+
return NSRange(location: offset, length: 0)
4951
}
5052
}
5153

Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Vertical.swift

+28-14
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ package extension TextSelectionManager {
3636
return extendSelectionVerticalCharacter(from: offset, up: up, suggestedXPos: suggestedXPos)
3737
case .word, .line, .visualLine:
3838
return extendSelectionVerticalLine(from: offset, up: up)
39-
case .container:
40-
return extendSelectionContainer(from: offset, delta: up ? 1 : -1)
39+
case .page:
40+
return extendSelectionPage(from: offset, delta: up ? 1 : -1, suggestedXPos: suggestedXPos)
4141
case .document:
4242
if up {
4343
return NSRange(location: 0, length: offset)
@@ -61,7 +61,7 @@ package extension TextSelectionManager {
6161
guard let point = layoutManager?.rectForOffset(offset)?.origin,
6262
let newOffset = layoutManager?.textOffsetAtPoint(
6363
CGPoint(
64-
x: suggestedXPos == nil ? point.x : suggestedXPos!,
64+
x: suggestedXPos ?? point.x,
6565
y: point.y - (layoutManager?.estimateLineHeight() ?? 2.0)/2 * (up ? 1 : -3)
6666
)
6767
) else {
@@ -115,22 +115,36 @@ package extension TextSelectionManager {
115115
}
116116
}
117117

118-
/// Extends a selection one "container" long.
118+
/// Extends a selection one "page" long.
119119
/// - Parameters:
120120
/// - offset: The location to start extending the selection from.
121121
/// - delta: The direction the selection should be extended. `1` for forwards, `-1` for backwards.
122122
/// - Returns: The range of the extended selection.
123-
private func extendSelectionContainer(from offset: Int, delta: Int) -> NSRange {
124-
guard let textView, let endOffset = layoutManager?.textOffsetAtPoint(
125-
CGPoint(
126-
x: delta > 0 ? textView.frame.maxX : textView.frame.minX,
127-
y: delta > 0 ? textView.frame.maxY : textView.frame.minY
128-
)
129-
) else {
123+
private func extendSelectionPage(from offset: Int, delta: Int, suggestedXPos: CGFloat?) -> NSRange {
124+
guard let textView = textView,
125+
let layoutManager,
126+
let currentYPos = layoutManager.rectForOffset(offset)?.origin.y else {
130127
return NSRange(location: offset, length: 0)
131128
}
132-
return endOffset > offset
133-
? NSRange(location: offset, length: endOffset - offset)
134-
: NSRange(location: endOffset, length: offset - endOffset)
129+
130+
let pageHeight = textView.visibleRect.height
131+
132+
// Grab the line where the next selection should be. Then use the suggestedXPos to find where in the line the
133+
// selection should be extended to.
134+
layoutManager.layoutLines(
135+
in: NSRect(x: 0, y: currentYPos, width: layoutManager.maxLineWidth, height: pageHeight)
136+
)
137+
guard let nextPageOffset = layoutManager.textOffsetAtPoint(CGPoint(
138+
x: suggestedXPos ?? 0,
139+
y: min(textView.frame.height, max(0, currentYPos + (delta > 0 ? -pageHeight : pageHeight)))
140+
)) else {
141+
return NSRange(location: offset, length: 0)
142+
}
143+
144+
if delta > 0 {
145+
return NSRange(location: nextPageOffset, length: offset - nextPageOffset)
146+
} else {
147+
return NSRange(location: offset, length: nextPageOffset - offset)
148+
}
135149
}
136150
}

Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift

+7-6
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ public class TextSelectionManager: NSObject {
5252
case word
5353
case line
5454
case visualLine
55-
/// Eg: Bottom of screen
56-
case container
55+
case page
5756
case document
5857
}
5958

@@ -323,10 +322,12 @@ public class TextSelectionManager: NSObject {
323322

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

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

331332
context.fill(fillRects)
332333
context.restoreGState()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// TextView+FirstResponder.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 6/15/24.
6+
//
7+
8+
import AppKit
9+
10+
extension TextView {
11+
open override func becomeFirstResponder() -> Bool {
12+
isFirstResponder = true
13+
selectionManager.cursorTimer.resetTimer()
14+
needsDisplay = true
15+
return super.becomeFirstResponder()
16+
}
17+
18+
open override func resignFirstResponder() -> Bool {
19+
isFirstResponder = false
20+
selectionManager.removeCursors()
21+
needsDisplay = true
22+
return super.resignFirstResponder()
23+
}
24+
25+
open override var canBecomeKeyView: Bool {
26+
super.canBecomeKeyView && acceptsFirstResponder && !isHiddenOrHasHiddenAncestor
27+
}
28+
29+
/// Sent to the window's first responder when `NSWindow.makeKey()` occurs.
30+
@objc private func becomeKeyWindow() {
31+
_ = becomeFirstResponder()
32+
}
33+
34+
/// Sent to the window's first responder when `NSWindow.resignKey()` occurs.
35+
@objc private func resignKeyWindow() {
36+
_ = resignFirstResponder()
37+
}
38+
39+
open override var needsPanelToBecomeKey: Bool {
40+
isSelectable || isEditable
41+
}
42+
43+
open override var acceptsFirstResponder: Bool {
44+
isSelectable
45+
}
46+
47+
open override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
48+
return true
49+
}
50+
51+
open override func resetCursorRects() {
52+
super.resetCursorRects()
53+
if isSelectable {
54+
addCursorRect(visibleRect, cursor: .iBeam)
55+
}
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// TextView+KeyDown.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 6/15/24.
6+
//
7+
8+
import AppKit
9+
import Carbon.HIToolbox
10+
11+
extension TextView {
12+
override public func keyDown(with event: NSEvent) {
13+
guard isEditable else {
14+
super.keyDown(with: event)
15+
return
16+
}
17+
18+
NSCursor.setHiddenUntilMouseMoves(true)
19+
20+
if !(inputContext?.handleEvent(event) ?? false) {
21+
interpretKeyEvents([event])
22+
} else {
23+
// Not handled, ignore so we don't double trigger events.
24+
return
25+
}
26+
}
27+
28+
override public func performKeyEquivalent(with event: NSEvent) -> Bool {
29+
guard isEditable else {
30+
return super.performKeyEquivalent(with: event)
31+
}
32+
33+
switch Int(event.keyCode) {
34+
case kVK_PageUp:
35+
if !event.modifierFlags.contains(.shift) {
36+
self.pageUp(event)
37+
return true
38+
}
39+
case kVK_PageDown:
40+
if !event.modifierFlags.contains(.shift) {
41+
self.pageDown(event)
42+
return true
43+
}
44+
default:
45+
return false
46+
}
47+
48+
return false
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// TextView+Layout.swift
3+
// CodeEditTextView
4+
//
5+
// Created by Khan Winter on 6/15/24.
6+
//
7+
8+
import Foundation
9+
10+
extension TextView {
11+
open override class var isCompatibleWithResponsiveScrolling: Bool {
12+
true
13+
}
14+
15+
open override func prepareContent(in rect: NSRect) {
16+
needsLayout = true
17+
super.prepareContent(in: rect)
18+
}
19+
20+
override public func draw(_ dirtyRect: NSRect) {
21+
super.draw(dirtyRect)
22+
if isSelectable {
23+
selectionManager.drawSelections(in: dirtyRect)
24+
}
25+
}
26+
27+
override open var isFlipped: Bool {
28+
true
29+
}
30+
31+
override public var visibleRect: NSRect {
32+
if let scrollView {
33+
var rect = scrollView.documentVisibleRect
34+
rect.origin.y += scrollView.contentInsets.top
35+
return rect.pixelAligned
36+
} else {
37+
return super.visibleRect
38+
}
39+
}
40+
41+
public var visibleTextRange: NSRange? {
42+
let minY = max(visibleRect.minY, 0)
43+
let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight())
44+
guard let minYLine = layoutManager.textLineForPosition(minY),
45+
let maxYLine = layoutManager.textLineForPosition(maxY) else {
46+
return nil
47+
}
48+
return NSRange(
49+
location: minYLine.range.location,
50+
length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length
51+
)
52+
}
53+
54+
public func updatedViewport(_ newRect: CGRect) {
55+
if !updateFrameIfNeeded() {
56+
layoutManager.layoutLines()
57+
}
58+
inputContext?.invalidateCharacterCoordinates()
59+
}
60+
61+
@discardableResult
62+
public func updateFrameIfNeeded() -> Bool {
63+
var availableSize = scrollView?.contentSize ?? .zero
64+
availableSize.height -= (scrollView?.contentInsets.top ?? 0) + (scrollView?.contentInsets.bottom ?? 0)
65+
let newHeight = max(layoutManager.estimatedHeight(), availableSize.height)
66+
let newWidth = layoutManager.estimatedWidth()
67+
68+
var didUpdate = false
69+
70+
if newHeight >= availableSize.height && frame.size.height != newHeight {
71+
frame.size.height = newHeight
72+
// No need to update layout after height adjustment
73+
}
74+
75+
if wrapLines && frame.size.width != availableSize.width {
76+
frame.size.width = availableSize.width
77+
didUpdate = true
78+
} else if !wrapLines && frame.size.width != max(newWidth, availableSize.width) {
79+
frame.size.width = max(newWidth, availableSize.width)
80+
didUpdate = true
81+
}
82+
83+
if didUpdate {
84+
needsLayout = true
85+
needsDisplay = true
86+
layoutManager.layoutLines()
87+
}
88+
89+
if isSelectable {
90+
selectionManager?.updateSelectionViews()
91+
}
92+
93+
return didUpdate
94+
}
95+
}

Sources/CodeEditTextView/TextView/TextView+Move.swift

+18
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,22 @@ extension TextView {
158158
selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true)
159159
updateAfterMove()
160160
}
161+
162+
override public func pageUp(_ sender: Any?) {
163+
enclosingScrollView?.pageUp(sender)
164+
}
165+
166+
override public func pageUpAndModifySelection(_ sender: Any?) {
167+
selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true)
168+
updateAfterMove()
169+
}
170+
171+
override public func pageDown(_ sender: Any?) {
172+
enclosingScrollView?.pageDown(sender)
173+
}
174+
175+
override public func pageDownAndModifySelection(_ sender: Any?) {
176+
selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true)
177+
updateAfterMove()
178+
}
161179
}

0 commit comments

Comments
 (0)