Skip to content

Commit

Permalink
Moved viewport from ViewportProvider to AsyncAttachmentRenderingDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdeep committed Nov 7, 2023
1 parent 68f65c6 commit 9714cb8
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ class ListFormattingProvider: EditorListFormattingProvider {
}

extension CommandsExampleViewController: AsyncAttachmentRenderingDelegate {
var viewport: CGRect? { nil }

func shouldRenderAsync(attachment: Proton.Attachment) -> Bool {
attachment is GridViewAttachment
}
Expand Down
4 changes: 0 additions & 4 deletions Proton/Proton.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@
1BD21554246951090000BCE2 /* LayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD21553246951090000BCE2 /* LayoutManager.swift */; };
1BD21556246952370000BCE2 /* BackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD21555246952370000BCE2 /* BackgroundStyle.swift */; };
1BD90ED72AF85A6300BB31FA /* EditorViewportSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD90ED62AF85A6300BB31FA /* EditorViewportSnapshotTests.swift */; };
1BD90ED92AF864B600BB31FA /* MockViewportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD90ED82AF864B600BB31FA /* MockViewportProvider.swift */; };
1BD9323E243CA506005E4136 /* CommandName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD9323D243CA506005E4136 /* CommandName.swift */; };
1BD993B123C995B000563ACB /* TextProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD993B023C995B000563ACB /* TextProcessorTests.swift */; };
1BD993B423C9966A00563ACB /* MockTextProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD993B323C9966A00563ACB /* MockTextProcessor.swift */; };
Expand Down Expand Up @@ -293,7 +292,6 @@
1BD21553246951090000BCE2 /* LayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutManager.swift; sourceTree = "<group>"; };
1BD21555246952370000BCE2 /* BackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundStyle.swift; sourceTree = "<group>"; };
1BD90ED62AF85A6300BB31FA /* EditorViewportSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorViewportSnapshotTests.swift; sourceTree = "<group>"; };
1BD90ED82AF864B600BB31FA /* MockViewportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewportProvider.swift; sourceTree = "<group>"; };
1BD9323D243CA506005E4136 /* CommandName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandName.swift; sourceTree = "<group>"; };
1BD993B023C995B000563ACB /* TextProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextProcessorTests.swift; sourceTree = "<group>"; };
1BD993B323C9966A00563ACB /* MockTextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTextProcessor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -851,7 +849,6 @@
1B8BE91E23C71E8A00353B17 /* MockEditorViewDelegate.swift */,
1BD993C323CACCE100563ACB /* MockAttachment.swift */,
1B6FB1892ABA75E7008CE69E /* MockAsyncAttachmentRenderingDelegate.swift */,
1BD90ED82AF864B600BB31FA /* MockViewportProvider.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -1129,7 +1126,6 @@
1B5E2583240F5B0C00163E74 /* NSAttributedStringExtensionTests.swift in Sources */,
1B45CDCB23BF1621001EB196 /* MockDefaultTextFormattingProvider.swift in Sources */,
1BD185D0284D839C001F4FBC /* GridViewAttachmentSnapshotTests.swift in Sources */,
1BD90ED92AF864B600BB31FA /* MockViewportProvider.swift in Sources */,
1B45CDA923BEED0E001EB196 /* AutogrowingTextViewTests.swift in Sources */,
7D0C8C6D2AD25D4A00D2F5D1 /* EditorKeyTests.swift in Sources */,
1BD185BF284C2A66001F4FBC /* GridTests.swift in Sources */,
Expand Down
28 changes: 28 additions & 0 deletions Proton/Sources/Swift/Attachment/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,36 @@ public protocol AttachmentOffsetProviding: AnyObject {
}

public protocol AsyncAttachmentRenderingDelegate: AnyObject {
/// Provides the viewport for the `Editor`. In typical cases, this would be used if the `EditorView` is made non-scrollable
/// and hosted within another scrollable container i.e. ScrollView.
/// - Note:
/// To use default value, i.e. viewport of the EditorView, leave this value as `nil`
/// - Important:
/// `EditorView` also has a `viewport` property that also depends on this property.
/// Care must be taken to not to return `editor.viewport` here. Doing so will cause a stack overflow crash.
/// An independently calculated value can safely be returned here.
var viewport: CGRect? { get }

/// Determines if particular attachment should be rendered asynchronously.
/// The check may also be used to render certain types of attachments synchronously or asynchronously.
/// - Parameter attachment: Attachment to be rendered.
/// - Returns: `true` to render asynchronously.
func shouldRenderAsync(attachment: Attachment) -> Bool

/// Notifies when an attachment is rendered asynchronously.
/// - Parameters:
/// - attachment: Attachment that is rendered.
/// - editor: Editor in which the attachment is rendered.
func didRenderAttachment(_ attachment: Attachment, in editor: EditorView)

/// Notifies when the viewport is rendered. Value of `viewport` is governed by `viewport` property in `AsyncAttachmentRenderingDelegate`
/// when not nil, else from `EditorView`
/// - Note:
/// There may be more than one invocation for the same `viewport` especially when user scroll out and back to the same `viewport`. This is
/// invoked when all the attachments in the `viewport` are rendered or the text is laid out and there are no attachments to render.
/// - Parameters:
/// - viewport: Viewport that is rendered.
/// - editor: Editor for which the `viewport` rendering is completed.
func didCompleteRenderingViewport(_ viewport: CGRect, in editor: EditorView)
}

Expand Down
29 changes: 15 additions & 14 deletions Proton/Sources/Swift/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,19 @@ open class EditorView: UIView {
let richTextView: RichTextView
let context: RichTextViewContext
var needsAsyncTextResolution = false

private let attachmentRenderingScheduler = AsyncTaskScheduler()
// Used for tracking rendered viewport in async behaviour specifically to ensure calling
// `didCompleteRenderingViewport` only once for each viewport value.
private var renderedViewport: CGRect? {
didSet {
guard let renderedViewport,
renderedViewport != oldValue else { return }

asyncAttachmentRenderingDelegate?.didCompleteRenderingViewport(renderedViewport, in: self)
}
}


// Holds `attributedText` until Editor move to a window
// Setting attributed text without Editor being fully ready
Expand All @@ -141,8 +153,6 @@ open class EditorView: UIView {
/// Context for the current Editor
public let editorViewContext: EditorViewContext

public weak var viewportProvider: ViewportProvider?

/// Enables asynchronous rendering of attachments.
/// - Note:
/// Since attachments must me rendered on main thread, the rendering only continues when there is no user interaction. By default, rendering starts
Expand Down Expand Up @@ -461,6 +471,7 @@ open class EditorView: UIView {
return
}
attachmentRenderingScheduler.cancel()
renderedViewport = nil
// Clear text before setting new value to avoid issues with formatting/layout when
// editor is hosted in a scrollable container and content is set multiple times.
richTextView.attributedText = NSAttributedString()
Expand Down Expand Up @@ -544,7 +555,7 @@ open class EditorView: UIView {
/// A `ViewportProvider` may be needed in cases where `EditorView` is hosted inside another `UIScrollView` and the
/// viewport needs to be calculated based on the viewport of container `UIScrollView`.
public var viewport: CGRect {
return viewportProvider?.viewport ?? richTextView.viewport
return asyncAttachmentRenderingDelegate?.viewport ?? richTextView.viewport
}

/// Returns the visible text range. In case of non-scrollable `EditorView`, entire range is `visibleRange`.
Expand Down Expand Up @@ -1455,16 +1466,6 @@ extension EditorView {
}
}

/// Provides the viewport for the `Editor`. In typical cases, this would be used if the `EditorView` is made non-scrollable
/// and hosted within another scrollable container i.e. ScrollView.
/// - Important:
/// `EditorView` also has a `viewport` property that depends on `ViewportProvider`
/// Care must be taken to not to return `editor.viewport` here. Doing so will cause a stack overflow crash.
/// An independently calculated value can safely be returned here.
public protocol ViewportProvider: AnyObject {
var viewport: CGRect { get }
}

extension EditorView: AsyncTaskSchedulerDelegate {
func getIDsToPrioritize() -> [String] {
guard let visibleRange else { return [] }
Expand All @@ -1475,7 +1476,7 @@ extension EditorView: AsyncTaskSchedulerDelegate {
guard attachmentIDs.isEmpty else {
return attachmentIDs
}
asyncAttachmentRenderingDelegate?.didCompleteRenderingViewport(viewport, in: self)
self.renderedViewport = viewport
// No attachments to render. Viewport rendering complete. Get attachments below viewport
let nextRange = NSRange(location: visibleRange.endLocation, length: max(0, contentLength - visibleRange.endLocation - 1))
let nextAttachmentIDs = attachmentsInRange(nextRange)
Expand Down
50 changes: 25 additions & 25 deletions Proton/Tests/Editor/EditorViewportSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ class EditorViewportSnapshotTests: SnapshotTestCase {

let viewController = EditorTestViewController()
let editor = viewController.editor
let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate()
let viewportProvider = MockViewportProvider(viewport: CGRect(
let viewport = CGRect(
origin: CGPoint(x: 0, y: 300),
size: CGSize(width: 260, height: 300)
))
)

let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate(viewport: viewport)

editor.asyncAttachmentRenderingDelegate = asyncRenderingDelegate
editor.viewportProvider = viewportProvider

let viewportBorderView = UIView(frame: viewportProvider.viewport)
let viewportBorderView = UIView(frame: viewport)
viewportBorderView.layer.borderColor = UIColor.red.cgColor
viewportBorderView.layer.borderWidth = 2
viewportBorderView.backgroundColor = .clear
Expand All @@ -59,9 +59,9 @@ class EditorViewportSnapshotTests: SnapshotTestCase {

editor.attributedText = NSAttributedString(string: text)
var renderingNotified = false
asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewPort, _ in
asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewport, _ in
renderingNotified = true
XCTAssertEqual(viewPort, viewportProvider.viewport)
XCTAssertEqual(viewport, asyncRenderingDelegate.viewport)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
Expand All @@ -79,16 +79,16 @@ class EditorViewportSnapshotTests: SnapshotTestCase {
let ex = functionExpectation()
let viewController = EditorTestViewController()
let editor = viewController.editor
let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate()
let viewportProvider = MockViewportProvider(viewport: CGRect(

let viewport = CGRect(
origin: CGPoint(x: 0, y: 300),
size: CGSize(width: 260, height: 300)
))
)
let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate(viewport: viewport)

editor.asyncAttachmentRenderingDelegate = asyncRenderingDelegate
editor.viewportProvider = viewportProvider

let viewportBorderView = UIView(frame: viewportProvider.viewport)
let viewportBorderView = UIView(frame: viewport)
viewportBorderView.layer.borderColor = UIColor.red.cgColor
viewportBorderView.layer.borderWidth = 2
viewportBorderView.backgroundColor = .clear
Expand All @@ -103,8 +103,8 @@ class EditorViewportSnapshotTests: SnapshotTestCase {
}
editor.attributedText = text

asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewPort, editor in
XCTAssertEqual(viewPort, viewportProvider.viewport)
asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewport, _ in
XCTAssertEqual(viewport, asyncRenderingDelegate.viewport)
assertSnapshot(matching: viewController.view, as: .image, record: self.recordMode)
ex.fulfill()
}
Expand All @@ -120,16 +120,16 @@ class EditorViewportSnapshotTests: SnapshotTestCase {
ex.expectedFulfillmentCount = 2
let viewController = EditorTestViewController()
let editor = viewController.editor
let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate()
let viewportProvider = MockViewportProvider(viewport: CGRect(

let viewport = CGRect(
origin: CGPoint(x: 0, y: 150),
size: CGSize(width: 260, height: 200)
))
)
let asyncRenderingDelegate = MockAsyncAttachmentRenderingDelegate(viewport: viewport)

editor.asyncAttachmentRenderingDelegate = asyncRenderingDelegate
editor.viewportProvider = viewportProvider

let viewportBorderView = UIView(frame: viewportProvider.viewport)
let viewportBorderView = UIView(frame: viewport)
viewportBorderView.layer.borderColor = UIColor.red.cgColor
viewportBorderView.layer.borderWidth = 2
viewportBorderView.backgroundColor = .clear
Expand All @@ -143,17 +143,17 @@ class EditorViewportSnapshotTests: SnapshotTestCase {
text.append(NSAttributedString(string: "Text after panel"))
}
editor.attributedText = text
var expectedViewport = viewportProvider.viewport
var expectedViewport = asyncRenderingDelegate.viewport

asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewPort, editor in
XCTAssertEqual(viewPort, expectedViewport)
asyncRenderingDelegate.onDidCompleteRenderingViewport = { viewport, _ in
XCTAssertEqual(viewport, expectedViewport)
assertSnapshot(matching: viewController.view, as: .image, record: self.recordMode)
viewportProvider.viewport = CGRect(
asyncRenderingDelegate.viewport = CGRect(
origin: CGPoint(x: 0, y: 600),
size: CGSize(width: 260, height: 200)
)
viewportBorderView.frame = viewportProvider.viewport
expectedViewport = viewportProvider.viewport
viewportBorderView.frame = asyncRenderingDelegate.viewport ?? viewportBorderView.frame
expectedViewport = asyncRenderingDelegate.viewport
ex.fulfill()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import UIKit
import Proton

class MockAsyncAttachmentRenderingDelegate: AsyncAttachmentRenderingDelegate {
var viewport: CGRect?
var onShouldRenderAsync: (Attachment) -> Bool = { _ in return true }
var onDidRenderAttachment: ((Attachment, EditorView) -> Void)?
var onDidCompleteRenderingViewport: ((CGRect, EditorView) -> Void)?

init(viewport: CGRect? = nil) {
self.viewport = viewport
}

func shouldRenderAsync(attachment: Attachment) -> Bool {
onShouldRenderAsync(attachment)
Expand Down
32 changes: 0 additions & 32 deletions Proton/Tests/Editor/Mocks/MockViewportProvider.swift

This file was deleted.

0 comments on commit 9714cb8

Please sign in to comment.