| ... | @@ -63,7 +63,7 @@ private final class CanvasUndoManager: UndoManager { | ... | @@ -63,7 +63,7 @@ private final class CanvasUndoManager: UndoManager { |
| 63 | /// behind `UITextInput` so the system keyboard, marked text, autocorrection, dictation, text | 63 | /// behind `UITextInput` so the system keyboard, marked text, autocorrection, dictation, text |
| 64 | /// interaction, loupe and edit menu work on it. | 64 | /// interaction, loupe and edit menu work on it. |
| 65 | final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextInteractionDelegate, | 65 | final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextInteractionDelegate, |
| 66 | UITextSelectionDisplayInteractionDelegate, UIGestureRecognizerDelegate, UIEditMenuInteractionDelegate, | 66 | UIGestureRecognizerDelegate, UIEditMenuInteractionDelegate, |
| 67 | UIPencilInteractionDelegate, UIScribbleInteractionDelegate | 67 | UIPencilInteractionDelegate, UIScribbleInteractionDelegate |
| 68 | { | 68 | { |
| 69 | /// The zoom a page opens at once the reader has pinched one. | 69 | /// The zoom a page opens at once the reader has pinched one. |
| ... | @@ -99,18 +99,11 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -99,18 +99,11 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 99 | }() | 99 | }() |
| 100 | /// Moves or widens the focused outline from its grips. | 100 | /// Moves or widens the focused outline from its grips. |
| 101 | private lazy var grip = UIPanGestureRecognizer(target: self, action: #selector(gripped)) | 101 | private lazy var grip = UIPanGestureRecognizer(target: self, action: #selector(gripped)) |
| 102 | /// Where the touch the grip and handle drags look at came down. | 102 | /// Where the outline grip drag started. |
| 103 | private var touchStart = CGPoint.zero | 103 | private var touchStart = CGPoint.zero |
| 104 | /// Drags a selection handle, which the text interaction leaves to the view drawing it. | | |
| 105 | private lazy var handleDrag = UIPanGestureRecognizer(target: self, action: #selector(handleDragged)) | | |
| 106 | /// The selection end a handle drag keeps, and the loupe following the other. | | |
| 107 | private var handleAnchor = 0 | | |
| 108 | private var loupe: UITextLoupeSession? | | |
| 109 | private lazy var editMenu = UIEditMenuInteraction(delegate: self) | 104 | private lazy var editMenu = UIEditMenuInteraction(delegate: self) |
| | 105 | /// Owns text gestures and their caret, highlight, handles and loupe together. |
| 110 | private let interaction = UITextInteraction(for: .editable) | 106 | private let interaction = UITextInteraction(for: .editable) |
| 111 | /// Draws the caret, selection highlight and handles, which the text interaction's | | |
| 112 | /// gestures move; the canvas paints neither. | | |
| 113 | private lazy var display = UITextSelectionDisplayInteraction(textInput: self, delegate: self) | | |
| 114 | private let history = CanvasUndoManager() | 107 | private let history = CanvasUndoManager() |
| 115 | private let spaceHint = Hint() | 108 | private let spaceHint = Hint() |
| 116 | /// The Pencil's strokes, and a finger's while `fingerInks`. | 109 | /// The Pencil's strokes, and a finger's while `fingerInks`. |
| ... | @@ -179,13 +172,13 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -179,13 +172,13 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 179 | layer.isOpaque = true | 172 | layer.isOpaque = true |
| 180 | // Frames reach the screen with the transaction that moves UIKit's caret and handles. | 173 | // Frames reach the screen with the transaction that moves UIKit's caret and handles. |
| 181 | layer.presentsWithTransaction = true | 174 | layer.presentsWithTransaction = true |
| 182 | for recognizer in [tap, doubleTap, grip, handleDrag, ink] { | 175 | for recognizer in [tap, doubleTap, grip, ink] { |
| 183 | recognizer.delegate = self | 176 | recognizer.delegate = self |
| 184 | addGestureRecognizer(recognizer) | 177 | addGestureRecognizer(recognizer) |
| 185 | } | 178 | } |
| 186 | // The Pencil draws; fingers, a trackpad and a mouse scroll and select. | 179 | // The Pencil draws; fingers, a trackpad and a mouse scroll and select. |
| 187 | let hands = [UITouch.TouchType.direct, .indirectPointer].map { NSNumber(value: $0.rawValue) } | 180 | let hands = [UITouch.TouchType.direct, .indirectPointer].map { NSNumber(value: $0.rawValue) } |
| 188 | let recognizers: [UIGestureRecognizer?] = [tap, doubleTap, grip, handleDrag, panGestureRecognizer, pinchGestureRecognizer] | 181 | let recognizers: [UIGestureRecognizer?] = [tap, doubleTap, grip, panGestureRecognizer, pinchGestureRecognizer] |
| 189 | for recognizer in recognizers { | 182 | for recognizer in recognizers { |
| 190 | recognizer?.allowedTouchTypes = hands | 183 | recognizer?.allowedTouchTypes = hands |
| 191 | } | 184 | } |
| ... | @@ -197,12 +190,12 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -197,12 +190,12 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 197 | picker.onClose = { [weak self] in self?.showPicker(false) } | 190 | picker.onClose = { [weak self] in self?.showPicker(false) } |
| 198 | tap.require(toFail: doubleTap) | 191 | tap.require(toFail: doubleTap) |
| 199 | panGestureRecognizer.require(toFail: grip) | 192 | panGestureRecognizer.require(toFail: grip) |
| 200 | panGestureRecognizer.require(toFail: handleDrag) | | |
| 201 | interaction.textInput = self | 193 | interaction.textInput = self |
| 202 | interaction.delegate = self | 194 | interaction.delegate = self |
| 203 | addInteraction(interaction) | 195 | addInteraction(interaction) |
| 204 | addInteraction(display) | 196 | for recognizer in interaction.gesturesForFailureRequirements { |
| 205 | display.isActivated = false | 197 | panGestureRecognizer.require(toFail: recognizer) |
| | 198 | } |
| 206 | addInteraction(editMenu) | 199 | addInteraction(editMenu) |
| 207 | history.canvas = self | 200 | history.canvas = self |
| 208 | NotificationCenter.default.addObserver( | 201 | NotificationCenter.default.addObserver( |
| ... | @@ -444,7 +437,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -444,7 +437,7 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 444 | } | 437 | } |
| 445 | transform() | 438 | transform() |
| 446 | // Scrolling carries the caret and handles along; zooming moves them within the page. | 439 | // Scrolling carries the caret and handles along; zooming moves them within the page. |
| 447 | display.setNeedsSelectionUpdate() | 440 | inputDelegate?.selectionDidChange(self) |
| 448 | } | 441 | } |
| 449 | | 442 | |
| 450 | func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { | 443 | func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { |
| ... | @@ -507,8 +500,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -507,8 +500,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 507 | | 500 | |
| 508 | func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { | 501 | func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { |
| 509 | if recognizer === ink { return !readOnly && (touch.type == .pencil || fingerInks && touch.type == .direct) } | 502 | if recognizer === ink { return !readOnly && (touch.type == .pencil || fingerInks && touch.type == .direct) } |
| 510 | if recognizer === grip || recognizer === handleDrag { touchStart = touch.location(in: self) } | 503 | if recognizer === grip { touchStart = touch.location(in: self) } |
| 511 | return !fingerInks || ![tap, doubleTap, grip, handleDrag].contains(recognizer) | 504 | return !fingerInks || ![tap, doubleTap, grip].contains(recognizer) |
| 512 | } | 505 | } |
| 513 | | 506 | |
| 514 | override func gestureRecognizerShouldBegin(_ recognizer: UIGestureRecognizer) -> Bool { | 507 | override func gestureRecognizerShouldBegin(_ recognizer: UIGestureRecognizer) -> Bool { |
| ... | @@ -518,7 +511,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -518,7 +511,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 518 | && !onHandle(recognizer.location(in: self)) | 511 | && !onHandle(recognizer.location(in: self)) |
| 519 | case doubleTap: target(recognizer.location(in: self)) == .page | 512 | case doubleTap: target(recognizer.location(in: self)) == .page |
| 520 | case grip: !readOnly && target(touchStart) == .grip | 513 | case grip: !readOnly && target(touchStart) == .grip |
| 521 | case handleDrag: onHandle(touchStart) | | |
| 522 | default: super.gestureRecognizerShouldBegin(recognizer) | 514 | default: super.gestureRecognizerShouldBegin(recognizer) |
| 523 | } | 515 | } |
| 524 | } | 516 | } |
| ... | @@ -582,61 +574,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -582,61 +574,23 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 582 | } | 574 | } |
| 583 | } | 575 | } |
| 584 | | 576 | |
| 585 | @objc private func handleDragged(_ recognizer: UIPanGestureRecognizer) { | | |
| 586 | guard let range = selectedTextRange else { return } | | |
| 587 | // The finger sits below the knob; aim at the text line above it. | | |
| 588 | let point = recognizer.location(in: self) | | |
| 589 | let aim = CGPoint(x: point.x, y: point.y - 16) | | |
| 590 | switch recognizer.state { | | |
| 591 | case .began: | | |
| 592 | let start = caretRect(for: range.start) | | |
| 593 | let end = caretRect(for: range.end) | | |
| 594 | let nearStart = hypot(start.midX - touchStart.x, start.midY - touchStart.y) | | |
| 595 | < hypot(end.midX - touchStart.x, end.midY - touchStart.y) | | |
| 596 | handleAnchor = nearStart ? range.upper : range.lower | | |
| 597 | let handle = display.handleViews.min { | | |
| 598 | hypot($0.center.x - touchStart.x, $0.center.y - touchStart.y) | | |
| 599 | < hypot($1.center.x - touchStart.x, $1.center.y - touchStart.y) | | |
| 600 | } | | |
| 601 | loupe = UITextLoupeSession.begin(at: aim, fromSelectionWidgetView: handle, in: self) | | |
| 602 | editMenu.dismissMenu() | | |
| 603 | fallthrough | | |
| 604 | case .changed: | | |
| 605 | guard let position = closestPosition(to: aim) else { return } | | |
| 606 | // A handle stops one character short of the other. | | |
| 607 | let step = position.offset < handleAnchor || (position.offset == handleAnchor && handleAnchor > 0) ? -1 : 1 | | |
| 608 | let end = position.offset == handleAnchor ? handleAnchor + step : position.offset | | |
| 609 | let moved = Range(handleAnchor, min(max(end, 0), endOfDocument.offset)) | | |
| 610 | edit(external: true) { choose(moved) } | | |
| 611 | let caret = caretRect(for: position) | | |
| 612 | loupe?.move(to: CGPoint(x: caret.midX, y: caret.midY), withCaretRect: caret, trackingCaret: false) | | |
| 613 | default: | | |
| 614 | loupe?.invalidate() | | |
| 615 | loupe = nil | | |
| 616 | let end = caretRect(for: range.end) | | |
| 617 | editMenu.presentEditMenu(with: UIEditMenuConfiguration( | | |
| 618 | identifier: nil, sourcePoint: CGPoint(x: end.midX, y: caretRect(for: range.start).minY))) | | |
| 619 | } | | |
| 620 | } | | |
| 621 | | | |
| 622 | @objc private func gripped(_ recognizer: UIPanGestureRecognizer) { | 577 | @objc private func gripped(_ recognizer: UIPanGestureRecognizer) { |
| 623 | guard let handle else { return } | 578 | guard let handle else { return } |
| 624 | let point = visible(recognizer.location(in: self)) | 579 | let point = visible(recognizer.location(in: self)) |
| 625 | switch recognizer.state { | 580 | switch recognizer.state { |
| 626 | case .began: | 581 | case .began: |
| | 582 | inputDelegate?.selectionWillChange(self) |
| 627 | fingerTool(at: touchStart) | 583 | fingerTool(at: touchStart) |
| 628 | let start = visible(touchStart) | 584 | let start = visible(touchStart) |
| 629 | _ = sb_view_press(handle, Float(start.x), Float(start.y)) | 585 | _ = sb_view_press(handle, Float(start.x), Float(start.y)) |
| 630 | _ = sb_view_drag(handle, Float(point.x), Float(point.y)) | 586 | _ = sb_view_drag(handle, Float(point.x), Float(point.y)) |
| 631 | // The caret and handles stay behind until the outline lands. | 587 | inputDelegate?.selectionDidChange(self) |
| 632 | display.isActivated = false | | |
| 633 | editMenu.dismissMenu() | 588 | editMenu.dismissMenu() |
| 634 | dirty = true | 589 | dirty = true |
| 635 | case .changed: | 590 | case .changed: |
| 636 | _ = sb_view_drag(handle, Float(point.x), Float(point.y)) | 591 | _ = sb_view_drag(handle, Float(point.x), Float(point.y)) |
| 637 | dirty = true | 592 | dirty = true |
| 638 | default: | 593 | default: |
| 639 | display.isActivated = isFirstResponder | | |
| 640 | edit(external: true) { sb_view_release(handle) } | 594 | edit(external: true) { sb_view_release(handle) } |
| 641 | spaceHint.hide() | 595 | spaceHint.hide() |
| 642 | } | 596 | } |
| ... | @@ -825,7 +779,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -825,7 +779,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 825 | sync() | 779 | sync() |
| 826 | revealCaret() | 780 | revealCaret() |
| 827 | } | 781 | } |
| 828 | display.setNeedsSelectionUpdate() | | |
| 829 | changedSelection() | 782 | changedSelection() |
| 830 | return changed | 783 | return changed |
| 831 | } | 784 | } |
| ... | @@ -858,7 +811,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -858,7 +811,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 858 | } | 811 | } |
| 859 | guard super.becomeFirstResponder() else { return false } | 812 | guard super.becomeFirstResponder() else { return false } |
| 860 | if let handle { sb_view_focus(handle, true) } | 813 | if let handle { sb_view_focus(handle, true) } |
| 861 | display.isActivated = true | | |
| 862 | dirty = true | 814 | dirty = true |
| 863 | changedSelection() | 815 | changedSelection() |
| 864 | return true | 816 | return true |
| ... | @@ -867,7 +819,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -867,7 +819,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 867 | override func resignFirstResponder() -> Bool { | 819 | override func resignFirstResponder() -> Bool { |
| 868 | guard super.resignFirstResponder() else { return false } | 820 | guard super.resignFirstResponder() else { return false } |
| 869 | if let handle { sb_view_focus(handle, false) } | 821 | if let handle { sb_view_focus(handle, false) } |
| 870 | display.isActivated = false | | |
| 871 | dirty = true | 822 | dirty = true |
| 872 | onChange?() | 823 | onChange?() |
| 873 | return true | 824 | return true |
| ... | @@ -1305,7 +1256,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -1305,7 +1256,8 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 1305 | | 1256 | |
| 1306 | var selectedTextRange: UITextRange? { | 1257 | var selectedTextRange: UITextRange? { |
| 1307 | get { | 1258 | get { |
| 1308 | guard let handle else { return nil } | 1259 | // Outline previews move before their text geometry is committed. |
| | 1260 | guard let handle, grip.state != .began, grip.state != .changed else { return nil } |
| 1309 | var range: [UInt32] = [0, 0] | 1261 | var range: [UInt32] = [0, 0] |
| 1310 | sb_selection(handle, &range) | 1262 | sb_selection(handle, &range) |
| 1311 | return Range(Int(range[0]), Int(range[1])) | 1263 | return Range(Int(range[0]), Int(range[1])) |
| ... | @@ -1313,7 +1265,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI | ... | @@ -1313,7 +1265,6 @@ final class CanvasView: UIScrollView, UIScrollViewDelegate, UITextInput, UITextI |
| 1313 | set { | 1265 | set { |
| 1314 | guard let newValue else { return } | 1266 | guard let newValue else { return } |
| 1315 | _ = choose(newValue) | 1267 | _ = choose(newValue) |
| 1316 | display.setNeedsSelectionUpdate() | | |
| 1317 | dirty = true | 1268 | dirty = true |
| 1318 | changedSelection() | 1269 | changedSelection() |
| 1319 | } | 1270 | } |