diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 2512cf17..6f2256d7 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -178,6 +178,7 @@ D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; }; D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; + D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; @@ -490,6 +491,7 @@ D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = ""; }; D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = ""; }; + D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Helpers.swift"; sourceTree = ""; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = ""; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = ""; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = ""; }; @@ -1073,6 +1075,7 @@ D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */, D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */, + D690797224A4EF9700023A34 /* UIBezierPath+Helpers.swift */, ); path = Extensions; sourceTree = ""; @@ -1870,6 +1873,7 @@ D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */, + D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */, D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */, D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */, ); diff --git a/Tusker/Extensions/UIBezierPath+Helpers.swift b/Tusker/Extensions/UIBezierPath+Helpers.swift new file mode 100644 index 00000000..cf702312 --- /dev/null +++ b/Tusker/Extensions/UIBezierPath+Helpers.swift @@ -0,0 +1,61 @@ +// +// UIBezierPath+Helpers.swift +// Tusker +// +// Created by Shadowfacts on 6/25/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +// TODO: write unit tests for this +extension UIBezierPath { + + /// Create a new UIBezierPath that wraps around the given array of rectangles. + /// This is not a convex hull aglorithm. What this does is it takes a set of rectangles + /// and draws a line around the outer borders of the combined shape. + convenience init(wrappingAround rects: [CGRect]) { + precondition(rects.count > 0) + let rects = rects.sorted { $0.minY < $1.minY } + + self.init() + + // start at the top left corner + self.move(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY)) + + // walk down the left side + var prevLeft = rects.first!.minX + for rect in rects where !rect.minX.isEqual(to: prevLeft) { + self.addLine(to: CGPoint(x: prevLeft, y: rect.minY)) + self.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + prevLeft = rect.minX + } + + // ensure at the bottom left if not already + let bottomLeft = CGPoint(x: rects.last!.minX, y: rects.last!.maxY) + if !self.currentPoint.equalTo(bottomLeft) { + self.addLine(to: bottomLeft) + } + + // across the bottom of the last rect + self.addLine(to: CGPoint(x: rects.last!.maxX, y: rects.last!.maxY)) + + // walk up the right side + var prevRight = rects.last!.maxX + for rect in rects.reversed() where !rect.maxX.isEqual(to: prevRight) { + self.addLine(to: CGPoint(x: prevRight, y: rect.maxY)) + self.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + prevRight = rect.maxX + } + + // ensure at the top right if not already + let topRight = CGPoint(x: rects.first!.maxX, y: rects.first!.minY) + if !self.currentPoint.equalTo(topRight) { + self.addLine(to: topRight) + } + + // across the top of the first rect + self.addLine(to: CGPoint(x: rects.first!.minX, y: rects.first!.minY)) + } + +} diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 706e6d61..4cae3e86 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -327,7 +327,13 @@ extension ContentTextView: UIContextMenuInteractionDelegate { // The preview parameters describe how the preview view is shown inside the prev. let parameters = UIPreviewParameters(textLineRects: rectsInCoordinateSpaceOfEnclosingRect as [NSValue]) - // todo: parameters.visiblePath around text + + // Mask the snapshot layer to only show the text of the link, and nothing else. + // By default, the system-applied mask is too wide and other content may seep in. + let path = UIBezierPath(wrappingAround: rectsInCoordinateSpaceOfEnclosingRect) + let maskLayer = CAShapeLayer() + maskLayer.path = path.cgPath + snapshot.layer.mask = maskLayer // The center point of the the minimum enclosing rect in our coordinate space is the point where the // center of the preview should be, since that's also in this view's coordinate space. @@ -336,7 +342,12 @@ extension ContentTextView: UIContextMenuInteractionDelegate { // The preview target describes how the preview is positioned. let target = UIPreviewTarget(container: self, center: rectsCenter) - return UITargetedPreview(view: snapshot, parameters: parameters, target: target) + // Create a dummy containerview for the snapshot view, since using a view with a CALayer mask and UIPreviewParameters(textLineRects:) + // causes the mask to be ignored. See FB7832297 + let snapshotContainer = UIView(frame: snapshot.bounds) + snapshotContainer.addSubview(snapshot) + + return UITargetedPreview(view: snapshotContainer, parameters: parameters, target: target) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {