Search code examples
swiftuinsattributedstringappkitnstextview

textContainerInset affecting NSAttributedString's background color alignment


I'm working on a SwiftUI "live markdown editor" that formats and hides syntax in realtime as the user writes.

I'm using NSTextView and NSViewRepresentable, and conditionally applying attributes using NSAttributedString.

I have "highlighting" functionality, by changing the string's background color. However, when the text is indented and wrapping, the background color sporadically bleeds into the indentation (see screenshot). Which line it bleeds into changes as I resize the window - it's not tied to any particular character from what I can tell.

By comparison, MacOS's textedit correctly leaves the indented whitespace white (as shown in the attached screenshot - I copied my text as "rich text" and pasted into TextEdit.

I've narrowed it down to my textContainerInset code I'm using to center the text container in a wider view:

// Observer for frame changes
            NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: scrollView, queue: OperationQueue.main) { [weak textView] _ in
                guard let textView = textView else { return }
                print("Text View Frame: \(textView.frame)")
                   print("Text Container Size: \(textView.textContainer?.containerSize ?? CGSize.zero)")
                if scrollView.frame.width < 750 {
                    textView.textContainer?.containerSize = CGSize(width: scrollView.frame.width, height: CGFloat.greatestFiniteMagnitude)
                    textView.textContainer?.widthTracksTextView = true
                    //textView.textContainerInset = NSSize(width: (scrollView.frame.width - textView.frame.width) / 2, height: 0)
                } else {
                    textView.textContainer?.containerSize = CGSize(width: 750, height: CGFloat.greatestFiniteMagnitude)
                    textView.textContainer?.widthTracksTextView = false
                  //  textView.textContainerInset = NSSize(width: (scrollView.frame.width - 750) / 2, height: 0)
                }
            }
            textView.adjustFontSizeCallback = adjustFontSize
            return scrollView

If I comment out the insets above, the issue goes away. Except my container is no longer centered, which I need.

What am I missing here?

Screenshot Comparison


Solution

  • Found a fix (admittedly a bit of a hack, I believe): The issue was the textContainerInset I was using to center my textContainer when the window got larger than 750px. I noticed that if I removed the insets (and let the container be full width), the text background colors never "bled" into the indented whitespace.

    My workaround is to use lineFragmentPadding to create the "margins" instead of the inset. It definitely feels more like a hack, as Apple explicitly says not to use this to create margins :

    Line fragment padding is not designed to express text margins. Instead, you should use insets on your text view, adjust the paragraph margin attributes, or change the position of the text view within its superview.

    But I haven't found another way to do what I need to. I welcome further insight, but for now, I'm running with the following:

     // Observer for frame changes
            NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: scrollView, queue: OperationQueue.main) { [weak textView] _ in
                guard let textView = textView else { return }
    
                if scrollView.frame.width <= 750 {
                    textView.textContainer?.containerSize = CGSize(width: scrollView.frame.width, height: CGFloat.greatestFiniteMagnitude)
                    textView.textContainer?.widthTracksTextView = true
                    textView.textContainer?.lineFragmentPadding = 20
                } else {
                    let padding = (scrollView.frame.width - 750) / 2
                    textView.textContainer?.containerSize = CGSize(width: 750 + (2 * padding), height: CGFloat.greatestFiniteMagnitude) // Compensate for the padding
                    textView.textContainer?.widthTracksTextView = false
                    textView.textContainer?.lineFragmentPadding = padding + 20
                }
            }