Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Editor Height Question #33

Open
rydamckinney opened this issue Jan 5, 2023 · 15 comments
Open

Dynamic Editor Height Question #33

rydamckinney opened this issue Jan 5, 2023 · 15 comments
Labels
feature New feature or request

Comments

@rydamckinney
Copy link

Is it currently possible to make the RichTextEditor frame dynamic with the following behavior?

  • Defined minimum and maximum height
  • When the current height is > min, and < max, no scrolling enabled
  • When the max height is reached, enable scrolling
@danielsaidi
Copy link
Owner

Hi @rydamckinney

I think this would be a bit tricky to achieve on all platforms, since the text views work a bit differently on iOS, where scrolling is part of the UITextView, compared to on macOS, where the NSTextView must be added to a scroll view.

Would be nice to have though 👍

@danielsaidi danielsaidi added the feature New feature or request label Jan 5, 2023
@darrarski
Copy link

Not sure about macOS, but this can be done easily on iOS:

struct MyTextView: UIViewRepresentable {
  typealias UIViewType = UITextView
  
  /* ... */

  func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
    // make sure we have a valid proposed width:
    guard let proposedWidth = proposal.width, proposedWidth > 0, proposedWidth < .infinity else {
      return nil
    }

    // calculate size of UITextView that fits current text:
    var sizeThatFits = uiView.sizeThatFits(CGSize(
      width: proposedWidth,
      height: .greatestFiniteMagnitude
    ))

    // fill parent horizontally:
    sizeThatFits.width = proposedWidth

    return sizeThatFits
  }
}

It's just an example of unconstrained, dynamic size. Constraining the minimum and maximum height should be straightforward.

@danielsaidi
Copy link
Owner

Thank you @darrarski! Did you get this to work @rydamckinney?

@rydamckinney
Copy link
Author

rydamckinney commented Apr 21, 2023

Hey @danielsaidi Yes I was able to get this working, thought not in the context of RichTextKit.

I needed markdown live syntax highlighting for the text input so I modified a fork of HighlightedTextEditor (https://github.com/kyle-n/HighlightedTextEditor) to switch between 2 wrapped NSTextViews (fixed vs dynamic height) based on the usage.

The solution should be generalizable to RichTextKit. The dynamic height view works in a pretty hacky way by checking & changing a constant height constraint on the scroll view that holds the NSTextView anytime that the text is changed. While it works, there's some dropped frames / animation glitches that I don't have a workaround for.

The rest of the code is pretty specific to the view hierarchy it's embedded in so I'm not sharing that so as to not confuse others (as I was led astray by many use-case-specific solutions I encountered in my research).

Here's the crux of the logic:

open class DynamicHeightNSTextView: NSTextView {
...
    open override func didChangeText() {
        super.didChangeText()
        
        self.invalidateIntrinsicContentSize()
        
        if let scrollHeight = self.scrollViewHeight {
            NSAnimationContext.runAnimationGroup { context in
                context.duration = 0.25
               //scrollHeight is a constant NSLayoutConstraint for the containing scrollview's height  
                scrollHeight.constant = min(self.maxHeight, max(self.intrinsicContentSize.height, self.minHeight))
            }
        }
    }
...
}
    ```

@danielsaidi
Copy link
Owner

Thank you for sharing @rydamckinney!

I will keep this issue open for when I have time to return to this project.

@gabrielalbino
Copy link

This feature would be my dream!

@slh-ideaflow
Copy link

I have what I think is a simpler question but which has led me here -- how can I get the RichTextEditor to dynamically adjust its height based on the text content inside, so that it doesn't have to scroll for you to see all the text? I'm working in iOS (and SwiftUI).

@danielsaidi
Copy link
Owner

@slh-ideaflow I'd love to support this. I guess it'd involve calculating the editor frame and apply it as fixed height?

@slh-ideaflow
Copy link

Actually @danielsaidi , I think the @rydamckinney 's implementation above provides what I need, I just probably wasn't using it correctly before. I also found it helps to add a little padding to ensure the vertical scrolling won't be present:

sizeThatFits.height += uiView.textContainerInset.bottom

@danielsaidi
Copy link
Owner

@slh-ideaflow Thank you for sharing Sam!

@danielsaidi
Copy link
Owner

@rydamckinney Would adding that code to RichTextKit make it work right out of the box, or would it require additional changes?

@bryan1anderson
Copy link
Contributor

@danielsaidi

public struct StaticRichTextEditor: ViewRepresentable {

    /// Create a rich text editor with a rich text value and
    /// a certain rich text data format.
    ///
    /// - Parameters:
    ///   - text: The rich text to edit.
    ///   - context: The rich text context to use.
    ///   - format: The rich text data format, by default `.archivedData`.
    ///   - viewConfiguration: A platform-specific view configuration, if any.
    public init(
        text: Binding<NSAttributedString>,
        context: RichTextContext,
        format: RichTextDataFormat = .archivedData,
        viewConfiguration: @escaping ViewConfiguration = { _ in }
    ) {
        self.text = text
        self._context = ObservedObject(wrappedValue: context)
        self.format = format
        self.viewConfiguration = viewConfiguration
    }

    public typealias ViewConfiguration = (RichTextViewComponent) -> Void

    @ObservedObject
    private var context: RichTextContext

    private var text: Binding<NSAttributedString>
    private var format: RichTextDataFormat
    private var viewConfiguration: ViewConfiguration

    @Environment(\.richTextEditorConfig)
    private var config

    @Environment(\.richTextEditorStyle)
    private var style

    #if iOS || os(tvOS) || os(visionOS)
    public let textView = RichTextView()
    #endif

    #if macOS
    public let textView = RichTextView()

    
    #endif

    public func makeCoordinator() -> RichTextCoordinator {
        RichTextCoordinator(
            text: text,
            textView: textView,
            richTextContext: context
        )
    }

    #if iOS || os(tvOS) || os(visionOS)
    public func makeUIView(context: Context) -> some UIView {
        textView.setup(with: text.wrappedValue, format: format)
        textView.configuration = config
        textView.theme = style
        viewConfiguration(textView)
        return textView
    }

    public func updateUIView(_ view: UIViewType, context: Context) {}
    
    @available(iOS 16.0, *)
    public func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? {
        guard let proposedWidth = proposal.width, proposedWidth > 0, proposedWidth < .infinity else {
          return nil
        }
        if let textView = uiView as? RichTextView {
            let inset = textView.textContainerInset
            
            // calculate size of UITextView that fits current text:
            var sizeThatFits = uiView.sizeThatFits(CGSize(
                width: proposedWidth,
                height: UIView.layoutFittingCompressedSize.height
            ))
            
            print(proposal, inset, sizeThatFits, textView.contentInset)
            
            
            sizeThatFits.width = proposedWidth
            
            return sizeThatFits
        }
        return nil
    }

    #else

    public func makeNSView(context: Context) -> some NSView {
        textView.setup(with: text.wrappedValue, format: format)
        textView.configuration = config
        textView.theme = style
        viewConfiguration(textView)
        return textView
    }

    public func updateNSView(_ view: NSViewType, context: Context) {}
    
    @available(macOS 13.0, *)
    public func sizeThatFits(_ proposal: ProposedViewSize, nsView: NSViewType, context: Context) -> CGSize? {
        guard let proposedWidth = proposal.width, proposedWidth > 0, proposedWidth < .infinity else {
          return nil
        }
        
        if let textView = nsView as? NSTextView {
            let inset = textView.textContainerInset

            let height = max(proposal.height ?? 100, textView.contentSize.height) + inset.height + inset.height
            
            return CGSize(width: proposedWidth, height: height)
        }
        return nil

    }
    #endif
}

I'm sure you could figure out a better way to integrate this into the RichTextEditor... all that business with textView as a documentView of the NSScrollView... so I opted for this. It kind of makes sense to use as a different type because it's not really configurable. Like you can't just enable scrolling and then disable it, at least not the way I see it for NSTextView.

Anyways, this works. I'll add some demos

Screen.Recording.2025-01-09.at.6.56.41.PM.mov
Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-01-09.at.18.55.57.mp4

@bryan1anderson
Copy link
Contributor

#225

@danielsaidi
Copy link
Owner

This looks amazing! 🤩

It would be great if this could be integrated with the already existing one, but I can imagine that it's tricky. I'll give it some time, or else merge it as a standalone view but perhaps not expose it directly in the SDK.

@bryan1anderson
Copy link
Contributor

It works great! SwiftUI scrollview even scrolls to the right place when you keep typing, which is very good.
Yeah I really struggled with that. I did begin by implementing it into the existing view.. with that said you do have a rich text viewer so I think there is a fair argument to be made that you already are in the business of creating standalone views. I think one of the things I struggled with was that all of this seems to need to be accomplished once on init of the view, so making it configurable like isScrolling.. it all has to be decided when you create the view.. I think... if that makes sense?

But I've seen your previous comments about how complicated it is with appkit non scrolling by default, and iOS scrolling by default... maybe the API being two separate views is actually a good thing because it communicates up front the complexity that you have mentioned. With all of that said, maybe the "Scrollable and NonScrollable" views are private, and the existing RichEditorView ends up being a wrapper with a parameter for embedInVerticalScrollView.. or an enum ContentContainerView.scroll, .resizingStatic?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants