From 3332e2dcc9c45770917d8abaf729c2d520fb6d45 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 10:27:07 +1000 Subject: [PATCH 01/18] Initialize MarkupTextView with TextKit 2 --- .../Common/MarkupTextViewRepresentable.swift | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index a1d21d28..7e00d77e 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -126,7 +126,7 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { } // MARK: Coordinator - class Coordinator: NSObject, UITextViewDelegate, NSTextStorageDelegate { + class Coordinator: NSObject, UITextViewDelegate, NSTextStorageDelegate, NSTextContentStorageDelegate, NSTextContentManagerDelegate, NSTextLayoutManagerDelegate { /// Is event happening during updateUIView? /// Used to avoid setting properties in events during view updates, as /// that would cause feedback cycles where an update triggers an event, @@ -267,11 +267,22 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { NSRange, UITextItemInteraction ) -> Bool - + // MARK: makeUIView func makeUIView(context: Context) -> MarkupTextView { Self.logger.debug("makeUIView") - let view = MarkupTextView() + + let textLayoutManager = NSTextLayoutManager() + let textContentStorage = NSTextContentStorage() + let textContainer = NSTextContainer() + textContentStorage.delegate = context.coordinator + textLayoutManager.delegate = context.coordinator + + textContentStorage.addTextLayoutManager(textLayoutManager) + + textLayoutManager.textContainer = textContainer + + let view = MarkupTextView(frame: self.frame, textContainer: textContainer) // Coordinator is both an UITextViewDelegate // and an NSTextStorageDelegate. From d2c8099962e3dd179e00de079e437bc6638608b0 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 10:27:25 +1000 Subject: [PATCH 02/18] Apply conditional formatting as a test --- .../Common/MarkupTextViewRepresentable.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 7e00d77e..cb1a17c1 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -243,6 +243,28 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) ) } + + // MARK: - NSTextContentStorageDelegate + + func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { + // In this method, we'll inject some attributes for display, without modifying the text storage directly. + var paragraphWithDisplayAttributes: NSTextParagraph? = nil + + let originalText = textContentStorage.textStorage!.attributedSubstring(from: range) + let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText) + + // Apply basic syntax highlighting + let boldRe = /\*(.+)\*/ + if let match = originalText.string.firstMatch(of: boldRe) { + let text = match.1 + let range = NSMakeRange(originalText.string.distance(from: originalText.string.startIndex, to: text.startIndex) - 1, text.count + 2) + + textWithDisplayAttributes.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 24), range: range) + paragraphWithDisplayAttributes = NSTextParagraph(attributedString: textWithDisplayAttributes) + } + + return paragraphWithDisplayAttributes + } } static var logger = Logger( From cd4337aa16baeebdc080c8c32789cc0e2d1de059 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 10:51:28 +1000 Subject: [PATCH 03/18] Port TranscludeBlockLayoutFragment Lift and shift seemed to "just work" here, but there are plenty of rough edges --- .../Common/MarkupTextViewRepresentable.swift | 22 +++ .../Text/TranscludeBlockLayoutFragment.swift | 155 ++++++++++++++++++ .../Subconscious.xcodeproj/project.pbxproj | 12 ++ 3 files changed, 189 insertions(+) create mode 100644 xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index cb1a17c1..1dbf6bed 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -244,6 +244,28 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) } + // MARK: - NSTextLayoutManagerDelegate + + func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, + textLayoutFragmentFor location: NSTextLocation, + in textElement: NSTextElement) -> NSTextLayoutFragment { + // TODO: might be better to hold a ref to textContentStorage somewhere + // textContentStorage is a concrete implementation of textContentManager + let textContentStorage = textLayoutManager.textContentManager as! NSTextContentStorage + let content = textContentStorage.attributedString(for: textElement) + + // Where we decide which layout/rendering implementation to use per TextLayoutFragment + + if content?.string.contains("/slashlink") ?? false { + let layoutFragment = TranscludeBlockLayoutFragment(textElement: textElement, range: textElement.elementRange) + layoutFragment.text = content?.string + return layoutFragment + + } else { + return NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) + } + } + // MARK: - NSTextContentStorageDelegate func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { diff --git a/xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift b/xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift new file mode 100644 index 00000000..217f3044 --- /dev/null +++ b/xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift @@ -0,0 +1,155 @@ +// +// TranscludeBlockLayoutFragment.swift +// Subconscious (iOS) +// +// Created by Ben Follington on 28/2/2023. +// + +/* + Copyright © 2022 Apple Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import SwiftUI + +#if os(iOS) +import UIKit +#else +import Cocoa +#endif +import CoreGraphics + +struct EmbeddedTranscludePreview: View { + var label: String = "Subconscious" + + var body: some View { + VStack(alignment: .leading) { + Text("title") + Text(label) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + .padding(4) + .frame(maxWidth: .infinity, maxHeight: 128) // TODO: share constant for all instances of 128 + .background(.white) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(.purple, lineWidth: 2) + ) + .padding(2) + } +} + +extension UIView { + func asImage() -> UIImage { + let renderer = UIGraphicsImageRenderer(bounds: bounds) + let img = renderer.image { rendererContext in + rendererContext.cgContext.saveGState() + + // SwiftUI views render upside down by default, flip them + let flipVertical: CGAffineTransform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: bounds.height ) + rendererContext.cgContext.concatenate(flipVertical) + + layer.render(in: rendererContext.cgContext) + + rendererContext.cgContext.restoreGState() + } + + return img + } +} + +class TranscludeBlockLayoutFragment: NSTextLayoutFragment { + // Max height constraint + let SLASHLINK_PREVIEW_HEIGHT = 128.0 + var text: String? + + override var leadingPadding: CGFloat { return 10 } + override var trailingPadding: CGFloat { return 10 } + override var topMargin: CGFloat { return 0 } + override var bottomMargin: CGFloat { return 0 } + + private var img: UIImage? + + private func render() { + let v = EmbeddedTranscludePreview(label: text ?? "Testing") + // Host our SwiftUI view within a UIKit view + let hosted = UIHostingController(rootView: v) + let view = hosted.view! + view.translatesAutoresizingMaskIntoConstraints = false + + // We have to mount the view before it will actually do layout calculations + UIApplication.shared.windows.first!.rootViewController?.view.addSubview(hosted.view) + + // Ideally here is where we would dynamically adjust the height of the rendered card + // However there doesn't seem to be a way to get the "preferred" size of the child content, it always tries to expand to fill the space provided + // This might be due to the underlying UIKit constraint system + let size = hosted.sizeThatFits(in: CGSize(width: containerWidth(), height: SLASHLINK_PREVIEW_HEIGHT)) + hosted.view.frame = CGRect(origin: CGPoint(x: 0, y: SLASHLINK_PREVIEW_HEIGHT), size: CGSize(width: containerWidth(), height: size.height)) + hosted.view.bounds = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: containerWidth(), height: size.height )) + hosted.view.backgroundColor = .clear + + let img = view.asImage() + hosted.view.removeFromSuperview() + + // Cache for reuse in layout calculations + self.img = img + + // Reflow the document layout to include the subview dimensions + invalidateLayout() + } + + private func containerWidth() -> CGFloat { + return self.textLayoutManager!.textContainer!.size.width - leadingPadding - trailingPadding + } + + // Determines how this flows around text + override var layoutFragmentFrame: CGRect { + let parent = super.layoutFragmentFrame + let r = CGRect(origin: parent.origin, size: CGSize(width: parent.width, height: parent.height + (self.img?.size.height ?? 0))) + return r + } + + // Determines the full drawing bounds, should be larger than layoutFragmentFrame + override var renderingSurfaceBounds: CGRect { + let w = containerWidth() + + let size = super.renderingSurfaceBounds.union(CGRect(x: 0, y: 0, width: w, height: SLASHLINK_PREVIEW_HEIGHT)) + return size + } + + private func withTranslation(x: CGFloat, y: CGFloat, ctx: CGContext, perform: (CGContext) -> Void) { + ctx.translateBy(x: x, y: y) + perform(ctx) + ctx.translateBy(x: -x, y: -y) + } + + override func draw(at renderingOrigin: CGPoint, in ctx: CGContext) { + render() + ctx.saveGState() + + if let img = self.img { + let height = img.size.height + withTranslation(x: leadingPadding, y: super.layoutFragmentFrame.height, ctx: ctx) { ctx in + ctx.draw(img.cgImage!, in: CGRect(x: 0, y: 0, width: img.size.width, height: height)) + // DEBUG: Render border around the cached view image +// ctx.stroke(CGRect(origin: renderingSurfaceBounds.origin, size: img.size)) + } + } + + // DEBUG: render border around entire draw surface +// withTranslation(x: leadingPadding, y: 0, ctx: ctx) { ctx in +// ctx.stroke(CGRect(origin: renderingSurfaceBounds.origin, size: renderingSurfaceBounds.size)) +// } + + ctx.restoreGState() + + // Draw the text on top. + super.draw(at: renderingOrigin, in: ctx) + } +} diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 295a36de..43c09de1 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ B54B922828E669D6003ACA1F /* MementoGeist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54B922728E669D6003ACA1F /* MementoGeist.swift */; }; B575834528ED8D9100F6EE88 /* combo.json in Resources */ = {isa = PBXBuildFile; fileRef = B575834428ED8D9100F6EE88 /* combo.json */; }; + B584814929AD82270033F434 /* TranscludeBlockLayoutFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B584814829AD82270033F434 /* TranscludeBlockLayoutFragment.swift */; }; B58FE48A28DED93F00E000CC /* ComboGeist.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58FE48928DED93F00E000CC /* ComboGeist.swift */; }; B58FE48C28DED9B600E000CC /* StoryCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58FE48B28DED9B600E000CC /* StoryCombo.swift */; }; B58FE48E28DEDAEA00E000CC /* StoryComboView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B58FE48D28DEDAEA00E000CC /* StoryComboView.swift */; }; @@ -341,6 +342,7 @@ /* Begin PBXFileReference section */ B54B922728E669D6003ACA1F /* MementoGeist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MementoGeist.swift; sourceTree = ""; }; B575834428ED8D9100F6EE88 /* combo.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = combo.json; sourceTree = ""; }; + B584814829AD82270033F434 /* TranscludeBlockLayoutFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscludeBlockLayoutFragment.swift; sourceTree = ""; }; B58FE48928DED93F00E000CC /* ComboGeist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComboGeist.swift; sourceTree = ""; }; B58FE48B28DED9B600E000CC /* StoryCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryCombo.swift; sourceTree = ""; }; B58FE48D28DEDAEA00E000CC /* StoryComboView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryComboView.swift; sourceTree = ""; }; @@ -591,6 +593,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B584814729AD82120033F434 /* Text */ = { + isa = PBXGroup; + children = ( + B584814829AD82270033F434 /* TranscludeBlockLayoutFragment.swift */, + ); + path = Text; + sourceTree = ""; + }; B80057E927DC355E002C0129 /* SubconsciousTests */ = { isa = PBXGroup; children = ( @@ -974,6 +984,7 @@ B8CBAFA029930C5D0079107E /* Forms */, B84AD8E2281073CE006B3153 /* InlineFormattingBarView.swift */, B8AE34CA276C195E00777FF0 /* LinkSuggestionLabelView.swift */, + B584814729AD82120033F434 /* Text */, B813749928BFCCCA00CCC6FB /* MarkupTextViewRepresentable.swift */, B856521F2975BA9000B7FCA0 /* MetaTableView.swift */, B8DEBF182798B6A8007CB528 /* NavigationToolbar.swift */, @@ -1399,6 +1410,7 @@ B8CC434927A0CA8D0079D2F9 /* AnimationUtilities.swift in Sources */, B89966C728B6EE2300DF1F8C /* Notebook.swift in Sources */, B88B1CE7298EEC240062CB7F /* GatewayURLSettingsView.swift in Sources */, + B584814929AD82270033F434 /* TranscludeBlockLayoutFragment.swift in Sources */, B8B4251228FDE7780081B8D5 /* Mapping.swift in Sources */, B8AE34AD276A9CDB00777FF0 /* PrimaryButtonStyle.swift in Sources */, B8A41D4E2811E81E0096D2E7 /* WikilinkBarView.swift in Sources */, From 4c7bbc0c4285f019ec6c3d93e68f0def69f0c90b Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:16:49 +1000 Subject: [PATCH 04/18] Gate rendering of transclude embeds We don't need these yet, but we will soon --- .../Components/Common/MarkupTextViewRepresentable.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 1dbf6bed..3df1890f 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -133,6 +133,7 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { /// which triggers an update, which triggers an event, etc. var isUIViewUpdating: Bool var representable: MarkupTextViewRepresentable + var renderTranscludeBlocks: Bool = false init( representable: MarkupTextViewRepresentable @@ -249,14 +250,14 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, textLayoutFragmentFor location: NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment { + // TODO: might be better to hold a ref to textContentStorage somewhere // textContentStorage is a concrete implementation of textContentManager let textContentStorage = textLayoutManager.textContentManager as! NSTextContentStorage let content = textContentStorage.attributedString(for: textElement) // Where we decide which layout/rendering implementation to use per TextLayoutFragment - - if content?.string.contains("/slashlink") ?? false { + if renderTranscludeBlocks && content?.string.contains("/slashlink") ?? false { let layoutFragment = TranscludeBlockLayoutFragment(textElement: textElement, range: textElement.elementRange) layoutFragment.text = content?.string return layoutFragment From e2d06a7f15e0fb43f5c00cb4e9dde90d1d4b9cda Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:19:13 +1000 Subject: [PATCH 05/18] Render markup per-layout-fragment --- .../Common/MarkupTextViewRepresentable.swift | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 3df1890f..b759cecc 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -168,9 +168,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { in: textStorage.string ) ) - // Render markup on TextStorage (which is an NSMutableString) - // using closure set on view (representable) - self.representable.renderAttributesOf(textStorage) } /// Handle link taps @@ -271,22 +268,11 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { // In this method, we'll inject some attributes for display, without modifying the text storage directly. - var paragraphWithDisplayAttributes: NSTextParagraph? = nil - let originalText = textContentStorage.textStorage!.attributedSubstring(from: range) let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText) - // Apply basic syntax highlighting - let boldRe = /\*(.+)\*/ - if let match = originalText.string.firstMatch(of: boldRe) { - let text = match.1 - let range = NSMakeRange(originalText.string.distance(from: originalText.string.startIndex, to: text.startIndex) - 1, text.count + 2) - - textWithDisplayAttributes.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 24), range: range) - paragraphWithDisplayAttributes = NSTextParagraph(attributedString: textWithDisplayAttributes) - } - - return paragraphWithDisplayAttributes + self.representable.renderAttributesOf(textWithDisplayAttributes) + return NSTextParagraph(attributedString: textWithDisplayAttributes) } } From 1ae20aa319333f65860e40877452873c8c5cc9b9 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:19:52 +1000 Subject: [PATCH 06/18] Port tap-detection logic Not needed until we actually use the transclude block embeds, but I see no harm in including it --- .../Common/MarkupTextViewRepresentable.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index b759cecc..1fc1e447 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -123,6 +123,23 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) ) } + + // This allows us to intercept touches on embedded transclude blocks + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + let tapPoint = touch.location(in: self) + + let textContentStorage = self.textLayoutManager?.textContentManager as! NSTextContentStorage + + if let layoutFragment = textLayoutManager!.textLayoutFragment(for: tapPoint) { + let content = textContentStorage.attributedString(for: layoutFragment.textElement!) + + // TODO: check for whether this tap should navigate to a link + MarkupTextViewRepresentable.logger.debug("Tapped: \(String(describing: content?.string))") + // Calling super preserves default behaviour + super.touchesBegan(touches, with: event) + } + } } // MARK: Coordinator From c03bd136cba5af59f47f54946d9cb1234646acf9 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 28 Feb 2023 11:25:09 +1000 Subject: [PATCH 07/18] Remove NSTextStorageDelegate TextContentStorageDelegate and TextContentManagerDelegate take its place. Given the comment attached to this method I expected something to break, but unicode and emojis seem to be behaving just fine. --- .../Common/MarkupTextViewRepresentable.swift | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 1fc1e447..a5326308 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -143,7 +143,7 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { } // MARK: Coordinator - class Coordinator: NSObject, UITextViewDelegate, NSTextStorageDelegate, NSTextContentStorageDelegate, NSTextContentManagerDelegate, NSTextLayoutManagerDelegate { + class Coordinator: NSObject, UITextViewDelegate, NSTextContentStorageDelegate, NSTextContentManagerDelegate, NSTextLayoutManagerDelegate { /// Is event happening during updateUIView? /// Used to avoid setting properties in events during view updates, as /// that would cause feedback cycles where an update triggers an event, @@ -159,34 +159,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { self.representable = representable } - /// NSTextStorageDelegate method - /// Handle markup rendering, just before processEditing is fired. - /// It is important that we render markup in `willProcessEditing` - /// because it happens BEFORE font substitution. Rendering before font - /// substitution gives the OS a chance to replace fonts for things like - /// Emoji or Unicode characters when your font does not support them. - /// See: - /// https://github.com/gordonbrander/subconscious/wiki/nstextstorage-font-substitution-and-missing-text - /// - /// 2022-03-17 Gordon Brander - func textStorage( - _ textStorage: NSTextStorage, - willProcessEditing: NSTextStorage.EditActions, - range: NSRange, - changeInLength: Int - ) { - MarkupTextViewRepresentable.logger.debug( - "textStorage: render markup attributes" - ) - textStorage.setAttributes( - [:], - range: NSRange( - textStorage.string.startIndex..., - in: textStorage.string - ) - ) - } - /// Handle link taps func textView( _ textView: UITextView, @@ -315,7 +287,7 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { NSRange, UITextItemInteraction ) -> Bool - + // MARK: makeUIView func makeUIView(context: Context) -> MarkupTextView { Self.logger.debug("makeUIView") @@ -336,8 +308,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { // and an NSTextStorageDelegate. // Set delegate on textview (coordinator) view.delegate = context.coordinator - // Set delegate on textstorage (coordinator) - view.textStorage.delegate = context.coordinator // Set inner padding view.textContainerInset = self.textContainerInset From 51226fe5c3d206257f8fc437de3eab01cc205fab Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:13:42 +1000 Subject: [PATCH 08/18] Guard & log --- .../Common/MarkupTextViewRepresentable.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index a5326308..ef0455d4 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -129,10 +129,19 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { guard let touch = touches.first else { return } let tapPoint = touch.location(in: self) - let textContentStorage = self.textLayoutManager?.textContentManager as! NSTextContentStorage + guard let textLayoutManager = self.textLayoutManager else { + MarkupTextViewRepresentable.logger.warning("Could not access textLayoutManager") + return + } + + guard let textContentStorage = textLayoutManager.textContentManager as? NSTextContentStorage else { + MarkupTextViewRepresentable.logger.warning("Could not access textContentStorage") + return + } - if let layoutFragment = textLayoutManager!.textLayoutFragment(for: tapPoint) { - let content = textContentStorage.attributedString(for: layoutFragment.textElement!) + // Did tap a text element? + if let textElement = textLayoutManager.textLayoutFragment(for: tapPoint)?.textElement { + let content = textContentStorage.attributedString(for: textElement) // TODO: check for whether this tap should navigate to a link MarkupTextViewRepresentable.logger.debug("Tapped: \(String(describing: content?.string))") From 80b0af189ad6afcbc328e4a301654a2b351048a0 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:14:12 +1000 Subject: [PATCH 09/18] Fix style for multi-line args --- .../Components/Common/MarkupTextViewRepresentable.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index ef0455d4..b4e905a0 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -242,9 +242,12 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { // MARK: - NSTextLayoutManagerDelegate - func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, - textLayoutFragmentFor location: NSTextLocation, - in textElement: NSTextElement) -> NSTextLayoutFragment { + func textLayoutManager( + _ textLayoutManager: NSTextLayoutManager, + textLayoutFragmentFor location: NSTextLocation, + in textElement: NSTextElement + ) -> NSTextLayoutFragment { + let baseLayoutFragment = NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) // TODO: might be better to hold a ref to textContentStorage somewhere // textContentStorage is a concrete implementation of textContentManager From 8b2e31180db320c8e0829275de4d797103d2774e Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:14:29 +1000 Subject: [PATCH 10/18] Guard & log --- .../Components/Common/MarkupTextViewRepresentable.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index b4e905a0..51d277a2 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -249,10 +249,10 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) -> NSTextLayoutFragment { let baseLayoutFragment = NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) - // TODO: might be better to hold a ref to textContentStorage somewhere - // textContentStorage is a concrete implementation of textContentManager - let textContentStorage = textLayoutManager.textContentManager as! NSTextContentStorage - let content = textContentStorage.attributedString(for: textElement) + guard let textContentStorage = textLayoutManager.textContentManager as? NSTextContentStorage else { + MarkupTextViewRepresentable.logger.warning("Could not access textContentStorage") + return baseLayoutFragment + } // Where we decide which layout/rendering implementation to use per TextLayoutFragment if renderTranscludeBlocks && content?.string.contains("/slashlink") ?? false { From 6ff4b8b3093c577d08ea1de5551993106ca8e3eb Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:14:40 +1000 Subject: [PATCH 11/18] Detect transclude blocks using Subtext.slugs --- .../Common/MarkupTextViewRepresentable.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 51d277a2..375c9c75 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -254,15 +254,19 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { return baseLayoutFragment } - // Where we decide which layout/rendering implementation to use per TextLayoutFragment - if renderTranscludeBlocks && content?.string.contains("/slashlink") ?? false { - let layoutFragment = TranscludeBlockLayoutFragment(textElement: textElement, range: textElement.elementRange) - layoutFragment.text = content?.string - return layoutFragment + if let text = textContentStorage.attributedString(for: textElement)?.string { + let sub = Subtext(markup: text) - } else { - return NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) + // Only render transcludes for a single slug in a single block + if renderTranscludeBlocks && sub.slugs.count == 1 && sub.blocks.count == 1 { + let layoutFragment = TranscludeBlockLayoutFragment(textElement: textElement, range: textElement.elementRange) + layoutFragment.text = text + + return layoutFragment + } } + + return baseLayoutFragment } // MARK: - NSTextContentStorageDelegate From 78e02744ffe9b672f351e70d05206cddb6691272 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:17:40 +1000 Subject: [PATCH 12/18] Guard & log --- .../Common/MarkupTextViewRepresentable.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 375c9c75..e13c97f1 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -272,11 +272,17 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { // MARK: - NSTextContentStorageDelegate func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { - // In this method, we'll inject some attributes for display, without modifying the text storage directly. - let originalText = textContentStorage.textStorage!.attributedSubstring(from: range) - let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText) + guard let originalText = textContentStorage.textStorage?.attributedSubstring(from: range) else { + MarkupTextViewRepresentable.logger.warning("textContentStorage: could not access attributedSubstring") + return nil + } + let textWithDisplayAttributes = NSMutableAttributedString(attributedString: originalText) + MarkupTextViewRepresentable.logger.debug( + "textContentStorage: render markup attributes" + ) self.representable.renderAttributesOf(textWithDisplayAttributes) + return NSTextParagraph(attributedString: textWithDisplayAttributes) } } From 4d51aa32de5322dd3e40a0069245fe7138050d84 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:21:04 +1000 Subject: [PATCH 13/18] Move TranscludeBlockLayoutFragment --- .../TranscludeBlockLayoutFragment.swift | 7 +++++-- xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) rename xcode/Subconscious/Shared/Components/Common/{Text => Transclude}/TranscludeBlockLayoutFragment.swift (97%) diff --git a/xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift similarity index 97% rename from xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift rename to xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift index 217f3044..19976132 100644 --- a/xcode/Subconscious/Shared/Components/Common/Text/TranscludeBlockLayoutFragment.swift +++ b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift @@ -80,11 +80,14 @@ class TranscludeBlockLayoutFragment: NSTextLayoutFragment { let v = EmbeddedTranscludePreview(label: text ?? "Testing") // Host our SwiftUI view within a UIKit view let hosted = UIHostingController(rootView: v) - let view = hosted.view! + guard let view = hosted.view else { + return + } + view.translatesAutoresizingMaskIntoConstraints = false // We have to mount the view before it will actually do layout calculations - UIApplication.shared.windows.first!.rootViewController?.view.addSubview(hosted.view) + UIApplication.shared.windows.first?.rootViewController?.view.addSubview(hosted.view) // Ideally here is where we would dynamically adjust the height of the rendered card // However there doesn't seem to be a way to get the "preferred" size of the child content, it always tries to expand to fill the space provided diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 43c09de1..8837d503 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -593,12 +593,12 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - B584814729AD82120033F434 /* Text */ = { + B5D8D2FD29AED1AE0011D820 /* Transclude */ = { isa = PBXGroup; children = ( B584814829AD82270033F434 /* TranscludeBlockLayoutFragment.swift */, ); - path = Text; + path = Transclude; sourceTree = ""; }; B80057E927DC355E002C0129 /* SubconsciousTests */ = { @@ -970,6 +970,7 @@ B8EC568426F41A2C00AC64E5 /* Common */ = { isa = PBXGroup; children = ( + B5D8D2FD29AED1AE0011D820 /* Transclude */, B8CBAFA5299449810079107E /* Audience */, B8AC648E278F7E7B0099E96B /* BackLabelStyle.swift */, B8545F0C2970577600BC4EA1 /* BacklinkReacts.swift */, @@ -984,7 +985,6 @@ B8CBAFA029930C5D0079107E /* Forms */, B84AD8E2281073CE006B3153 /* InlineFormattingBarView.swift */, B8AE34CA276C195E00777FF0 /* LinkSuggestionLabelView.swift */, - B584814729AD82120033F434 /* Text */, B813749928BFCCCA00CCC6FB /* MarkupTextViewRepresentable.swift */, B856521F2975BA9000B7FCA0 /* MetaTableView.swift */, B8DEBF182798B6A8007CB528 /* NavigationToolbar.swift */, From fa386568abb90041a5de4539b852897996796ee6 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:31:01 +1000 Subject: [PATCH 14/18] Remove padding on transclude blocks --- .../Common/Transclude/TranscludeBlockLayoutFragment.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift index 19976132..f2cfc4f5 100644 --- a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift +++ b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift @@ -69,8 +69,8 @@ class TranscludeBlockLayoutFragment: NSTextLayoutFragment { let SLASHLINK_PREVIEW_HEIGHT = 128.0 var text: String? - override var leadingPadding: CGFloat { return 10 } - override var trailingPadding: CGFloat { return 10 } + override var leadingPadding: CGFloat { return 0 } + override var trailingPadding: CGFloat { return 0 } override var topMargin: CGFloat { return 0 } override var bottomMargin: CGFloat { return 0 } From 83f241c454b51388e7c2bba9c002d326688fbd9b Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Wed, 1 Mar 2023 10:31:29 +1000 Subject: [PATCH 15/18] Guard & log when rendering SwiftUI view as image --- .../TranscludeBlockLayoutFragment.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift index f2cfc4f5..9375f129 100644 --- a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift +++ b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift @@ -15,6 +15,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import os import SwiftUI #if os(iOS) @@ -65,6 +66,11 @@ extension UIView { } class TranscludeBlockLayoutFragment: NSTextLayoutFragment { + static var logger = Logger( + subsystem: Config.default.rdns, + category: "editor" + ) + // Max height constraint let SLASHLINK_PREVIEW_HEIGHT = 128.0 var text: String? @@ -87,7 +93,17 @@ class TranscludeBlockLayoutFragment: NSTextLayoutFragment { view.translatesAutoresizingMaskIntoConstraints = false // We have to mount the view before it will actually do layout calculations - UIApplication.shared.windows.first?.rootViewController?.view.addSubview(hosted.view) + guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + TranscludeBlockLayoutFragment.logger.warning("Could not find UIWindowScene") + return + } + + guard let rootView = scene.windows.first?.rootViewController?.view else { + TranscludeBlockLayoutFragment.logger.warning("Could not find rootViewController") + return + } + + rootView.addSubview(hosted.view) // Ideally here is where we would dynamically adjust the height of the rendered card // However there doesn't seem to be a way to get the "preferred" size of the child content, it always tries to expand to fill the space provided From 653197f3365ddda52802f9871b964bce57fd8bff Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 2 Mar 2023 11:19:38 +1000 Subject: [PATCH 16/18] Remove transclude-block stubs We will re-introduce these when we actually need them, this first batch of work is purely moving TK1 -> TK2 --- .../Common/MarkupTextViewRepresentable.swift | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index e13c97f1..4bcc1caf 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -123,32 +123,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) ) } - - // This allows us to intercept touches on embedded transclude blocks - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - guard let touch = touches.first else { return } - let tapPoint = touch.location(in: self) - - guard let textLayoutManager = self.textLayoutManager else { - MarkupTextViewRepresentable.logger.warning("Could not access textLayoutManager") - return - } - - guard let textContentStorage = textLayoutManager.textContentManager as? NSTextContentStorage else { - MarkupTextViewRepresentable.logger.warning("Could not access textContentStorage") - return - } - - // Did tap a text element? - if let textElement = textLayoutManager.textLayoutFragment(for: tapPoint)?.textElement { - let content = textContentStorage.attributedString(for: textElement) - - // TODO: check for whether this tap should navigate to a link - MarkupTextViewRepresentable.logger.debug("Tapped: \(String(describing: content?.string))") - // Calling super preserves default behaviour - super.touchesBegan(touches, with: event) - } - } } // MARK: Coordinator @@ -159,7 +133,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { /// which triggers an update, which triggers an event, etc. var isUIViewUpdating: Bool var representable: MarkupTextViewRepresentable - var renderTranscludeBlocks: Bool = false init( representable: MarkupTextViewRepresentable @@ -240,35 +213,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { ) } - // MARK: - NSTextLayoutManagerDelegate - - func textLayoutManager( - _ textLayoutManager: NSTextLayoutManager, - textLayoutFragmentFor location: NSTextLocation, - in textElement: NSTextElement - ) -> NSTextLayoutFragment { - let baseLayoutFragment = NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) - - guard let textContentStorage = textLayoutManager.textContentManager as? NSTextContentStorage else { - MarkupTextViewRepresentable.logger.warning("Could not access textContentStorage") - return baseLayoutFragment - } - - if let text = textContentStorage.attributedString(for: textElement)?.string { - let sub = Subtext(markup: text) - - // Only render transcludes for a single slug in a single block - if renderTranscludeBlocks && sub.slugs.count == 1 && sub.blocks.count == 1 { - let layoutFragment = TranscludeBlockLayoutFragment(textElement: textElement, range: textElement.elementRange) - layoutFragment.text = text - - return layoutFragment - } - } - - return baseLayoutFragment - } - // MARK: - NSTextContentStorageDelegate func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { From f9a8d3c5fac2c30dcbd03b5872ba04c7fac96386 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 2 Mar 2023 11:19:44 +1000 Subject: [PATCH 17/18] Update comments --- .../Components/Common/MarkupTextViewRepresentable.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift index 4bcc1caf..51022903 100644 --- a/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift +++ b/xcode/Subconscious/Shared/Components/Common/MarkupTextViewRepresentable.swift @@ -258,6 +258,7 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> MarkupTextView { Self.logger.debug("makeUIView") + // Coordinator acts as all the relevant delegates let textLayoutManager = NSTextLayoutManager() let textContentStorage = NSTextContentStorage() let textContainer = NSTextContainer() @@ -269,10 +270,6 @@ struct MarkupTextViewRepresentable: UIViewRepresentable { textLayoutManager.textContainer = textContainer let view = MarkupTextView(frame: self.frame, textContainer: textContainer) - - // Coordinator is both an UITextViewDelegate - // and an NSTextStorageDelegate. - // Set delegate on textview (coordinator) view.delegate = context.coordinator // Set inner padding From 3096b8750b53ab9fcfa931e81fb02531fb173476 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Thu, 2 Mar 2023 11:22:45 +1000 Subject: [PATCH 18/18] Remove TranscludeBlockLayoutFragment We'll add it back later --- .../TranscludeBlockLayoutFragment.swift | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift diff --git a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift b/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift deleted file mode 100644 index 9375f129..00000000 --- a/xcode/Subconscious/Shared/Components/Common/Transclude/TranscludeBlockLayoutFragment.swift +++ /dev/null @@ -1,174 +0,0 @@ -// -// TranscludeBlockLayoutFragment.swift -// Subconscious (iOS) -// -// Created by Ben Follington on 28/2/2023. -// - -/* - Copyright © 2022 Apple Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -import os -import SwiftUI - -#if os(iOS) -import UIKit -#else -import Cocoa -#endif -import CoreGraphics - -struct EmbeddedTranscludePreview: View { - var label: String = "Subconscious" - - var body: some View { - VStack(alignment: .leading) { - Text("title") - Text(label) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - .padding(4) - .frame(maxWidth: .infinity, maxHeight: 128) // TODO: share constant for all instances of 128 - .background(.white) - .overlay( - RoundedRectangle(cornerRadius: 4) - .stroke(.purple, lineWidth: 2) - ) - .padding(2) - } -} - -extension UIView { - func asImage() -> UIImage { - let renderer = UIGraphicsImageRenderer(bounds: bounds) - let img = renderer.image { rendererContext in - rendererContext.cgContext.saveGState() - - // SwiftUI views render upside down by default, flip them - let flipVertical: CGAffineTransform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: bounds.height ) - rendererContext.cgContext.concatenate(flipVertical) - - layer.render(in: rendererContext.cgContext) - - rendererContext.cgContext.restoreGState() - } - - return img - } -} - -class TranscludeBlockLayoutFragment: NSTextLayoutFragment { - static var logger = Logger( - subsystem: Config.default.rdns, - category: "editor" - ) - - // Max height constraint - let SLASHLINK_PREVIEW_HEIGHT = 128.0 - var text: String? - - override var leadingPadding: CGFloat { return 0 } - override var trailingPadding: CGFloat { return 0 } - override var topMargin: CGFloat { return 0 } - override var bottomMargin: CGFloat { return 0 } - - private var img: UIImage? - - private func render() { - let v = EmbeddedTranscludePreview(label: text ?? "Testing") - // Host our SwiftUI view within a UIKit view - let hosted = UIHostingController(rootView: v) - guard let view = hosted.view else { - return - } - - view.translatesAutoresizingMaskIntoConstraints = false - - // We have to mount the view before it will actually do layout calculations - guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { - TranscludeBlockLayoutFragment.logger.warning("Could not find UIWindowScene") - return - } - - guard let rootView = scene.windows.first?.rootViewController?.view else { - TranscludeBlockLayoutFragment.logger.warning("Could not find rootViewController") - return - } - - rootView.addSubview(hosted.view) - - // Ideally here is where we would dynamically adjust the height of the rendered card - // However there doesn't seem to be a way to get the "preferred" size of the child content, it always tries to expand to fill the space provided - // This might be due to the underlying UIKit constraint system - let size = hosted.sizeThatFits(in: CGSize(width: containerWidth(), height: SLASHLINK_PREVIEW_HEIGHT)) - hosted.view.frame = CGRect(origin: CGPoint(x: 0, y: SLASHLINK_PREVIEW_HEIGHT), size: CGSize(width: containerWidth(), height: size.height)) - hosted.view.bounds = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: containerWidth(), height: size.height )) - hosted.view.backgroundColor = .clear - - let img = view.asImage() - hosted.view.removeFromSuperview() - - // Cache for reuse in layout calculations - self.img = img - - // Reflow the document layout to include the subview dimensions - invalidateLayout() - } - - private func containerWidth() -> CGFloat { - return self.textLayoutManager!.textContainer!.size.width - leadingPadding - trailingPadding - } - - // Determines how this flows around text - override var layoutFragmentFrame: CGRect { - let parent = super.layoutFragmentFrame - let r = CGRect(origin: parent.origin, size: CGSize(width: parent.width, height: parent.height + (self.img?.size.height ?? 0))) - return r - } - - // Determines the full drawing bounds, should be larger than layoutFragmentFrame - override var renderingSurfaceBounds: CGRect { - let w = containerWidth() - - let size = super.renderingSurfaceBounds.union(CGRect(x: 0, y: 0, width: w, height: SLASHLINK_PREVIEW_HEIGHT)) - return size - } - - private func withTranslation(x: CGFloat, y: CGFloat, ctx: CGContext, perform: (CGContext) -> Void) { - ctx.translateBy(x: x, y: y) - perform(ctx) - ctx.translateBy(x: -x, y: -y) - } - - override func draw(at renderingOrigin: CGPoint, in ctx: CGContext) { - render() - ctx.saveGState() - - if let img = self.img { - let height = img.size.height - withTranslation(x: leadingPadding, y: super.layoutFragmentFrame.height, ctx: ctx) { ctx in - ctx.draw(img.cgImage!, in: CGRect(x: 0, y: 0, width: img.size.width, height: height)) - // DEBUG: Render border around the cached view image -// ctx.stroke(CGRect(origin: renderingSurfaceBounds.origin, size: img.size)) - } - } - - // DEBUG: render border around entire draw surface -// withTranslation(x: leadingPadding, y: 0, ctx: ctx) { ctx in -// ctx.stroke(CGRect(origin: renderingSurfaceBounds.origin, size: renderingSurfaceBounds.size)) -// } - - ctx.restoreGState() - - // Draw the text on top. - super.draw(at: renderingOrigin, in: ctx) - } -}