Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ extension TextViewController {
view = PlainControllerView.make()
}

backgroundView.frame = view.bounds
backgroundView.autoresizingMask = [.width, .height]
view.addSubview(backgroundView)

scrollView = SourceEditorScrollView()
scrollView.documentView = textView

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extension TextViewController {
/// Style the scroll view.
package func styleScrollView() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.drawsBackground = false
scrollView.contentView.postsFrameChangedNotifications = true
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = !wrapLines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class TextViewController: NSViewController {
public internal(set) var scrollView: SourceEditorScrollView!
public internal(set) var textView: TextView!
var gutterView: GutterView!
let backgroundView = SourceEditorBackgroundView()

/// Middleman between the text view to our invisible characters config, with knowledge of things like the
/// /// user's theme and indent option to help correctly draw invisible character placeholders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,7 @@ public extension SourceEditorConfiguration {
controller.textView.selectionManager.selectionBackgroundColor = theme.selection
controller.textView.selectionManager.selectedLineBackgroundColor = currentLineColor
controller.textView.selectionManager.insertionPointColor = theme.insertionPoint
controller.textView.enclosingScrollView?.backgroundColor = if useThemeBackground {
theme.background
} else {
.clear
}
controller.backgroundView.fillColor = useThemeBackground ? theme.background : nil

(controller.textView as? SourceEditorTextView)?.statementHighlightColor = theme.statementHighlight
(controller.textView as? SourceEditorTextView)?.backgroundFillColor = useThemeBackground ? theme.background : nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// SourceEditorBackgroundView.swift
// TableProEditorKit
//

import AppKit

/// The editor's background, behind everything else it draws and across its whole frame.
///
/// The text view and the gutter each paint their own frame, which leaves the content insets between them and the
/// editor's edge. Painted by the scroll view instead, as `NSScrollView.backgroundColor`, a semantic color such as
/// `textBackgroundColor` comes out as the window's background inside a window with a toolbar, so in Dark those
/// margins read lighter than the text beside them. Drawn here the way the text view draws its own, the color holds.
final class SourceEditorBackgroundView: NSView {
var fillColor: NSColor? {
didSet {
guard fillColor != oldValue else { return }
needsDisplay = true
}
}

/// Bounded by hand: from macOS 14 a view that does not clip is handed dirty rects that reach past its bounds, and
/// filling one painted the host's toolbar above the editor.
override func draw(_ dirtyRect: NSRect) {
guard let fillColor else { return }
fillColor.setFill()
bounds.intersection(dirtyRect).fill()
}

override func hitTest(_ point: NSPoint) -> NSView? {
nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ struct EditorThemeNativeColorTests {
#expect(textView.backgroundFillColor == nil)
}

/// The content insets sit between the text view and the editor's edge, outside every view that paints itself.
/// Left to the scroll view, a system background came out as the window's color there, a lighter band above and
/// beside the text in Dark.
@Test("The editor paints the theme background behind its whole frame, the margins included")
func editorPaintsTheBackgroundBehindItsMargins() throws {
let controller = Mock.loadedTextViewController()
let background = controller.backgroundView

#expect(controller.scrollView.drawsBackground == false)
#expect(background.superview === controller.view)
#expect(background.frame == controller.view.bounds)
let findView = try #require(controller.findViewController?.view)
let order = controller.view.subviews
#expect(order.firstIndex { $0 === background } ?? .max < order.firstIndex { $0 === findView } ?? .min)

#expect(background.fillColor == controller.theme.background)
var theme = controller.theme
theme.background = .systemTeal
controller.configuration.appearance.theme = theme
#expect(background.fillColor == .systemTeal)

controller.configuration.appearance.useThemeBackground = false
#expect(background.fillColor == nil)
}

@Test("A theme without a cursor color leaves the caret to the system")
func caretIsNativeUnlessOverridden() {
let controller = Mock.loadedTextViewController()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,23 @@ struct TextViewControllerClippingTests {
inner.isEmpty || outer.insetBy(dx: -0.5, dy: -0.5).contains(inner)
}

@Test("The gutter's line numbers paint nothing over the view above the editor")
func gutterPaintsNothingAboveTheEditor() throws {
/// Every column of the band, the gutter's and the text's: from macOS 14 a view that does not clip is handed dirty
/// rects reaching past its bounds, so a background filled from the dirty rect painted the whole band too.
@Test("The editor paints nothing over the view above it")
func editorPaintsNothingAboveItself() throws {
controller.gutterView.needsDisplay = true
let rep = try #require(root.bitmapImageRepForCachingDisplay(in: root.bounds))
root.cacheDisplay(in: root.bounds, to: rep)
let scale = CGFloat(rep.pixelsWide) / root.bounds.width

/// The band measured where the gutter cannot reach, rather than the color it fills with: the cached drawing
/// lands in a color space that shifts the fill.
let untouched = try #require(rep.colorAt(x: Int((root.bounds.maxX - 20) * scale), y: Int(10 * scale)))
let gutterWidth = controller.gutterView.frame.maxX
#expect(gutterWidth > 0)
#expect(controller.gutterView.frame.maxX > 0)
var inked = 0
for x in stride(from: CGFloat(1), to: gutterWidth, by: 1) {
for y in stride(from: CGFloat(1), to: Self.bandHeight - 1, by: 1) {
let color = rep.colorAt(x: Int(x * scale), y: Int(y * scale))
if !Self.isSame(color, untouched) { inked += 1 }
for x in stride(from: CGFloat(1), to: root.bounds.width - 1, by: 1) {
for y in stride(from: CGFloat(1), to: Self.bandHeight - 1, by: 2) {
if !Self.isBand(rep.colorAt(x: Int(x * scale), y: Int(y * scale))) { inked += 1 }
}
}
#expect(inked == 0, "the gutter painted \(inked) points above the editor")
#expect(inked == 0, "the editor painted \(inked) points above itself")
}

@Test("The gutter and its ribbons are asked to draw only inside the editor")
Expand All @@ -143,11 +140,9 @@ struct TextViewControllerClippingTests {
#expect(contains(editorInWindow, visibleRectInWindow(find.findPanel)))
}

private static func isSame(_ lhs: NSColor?, _ rhs: NSColor) -> Bool {
guard let lhs = lhs?.usingColorSpace(.sRGB), let rhs = rhs.usingColorSpace(.sRGB) else { return false }
let tolerance: CGFloat = 0.02
return abs(lhs.redComponent - rhs.redComponent) <= tolerance
&& abs(lhs.greenComponent - rhs.greenComponent) <= tolerance
&& abs(lhs.blueComponent - rhs.blueComponent) <= tolerance
/// The band's red, loosely: the cached drawing lands in a color space that shifts the fill.
private static func isBand(_ color: NSColor?) -> Bool {
guard let color = color?.usingColorSpace(.sRGB) else { return false }
return color.redComponent > 0.8 && color.greenComponent < 0.4 && color.blueComponent < 0.4
}
}
1 change: 1 addition & 0 deletions apple/changelog.d/3596.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
In Dark, a read-only code view no longer shows a lighter band above and beside its text.
Loading