27c203b336
The 32-point viewport fade covered launcher rows with only 16 points of inset and emoji with only 12. Match their top spacing to the shared fade height while keeping the viewport mask fixed during scrolling.
71 lines
3.7 KiB
Swift
71 lines
3.7 KiB
Swift
import AppKit
|
|
import ConjureKit
|
|
import Testing
|
|
@testable import Conjure
|
|
|
|
@Test @MainActor
|
|
func launcherFadesStayAtViewportEdgesAfterScrollingAndResizing() throws {
|
|
_ = NSApplication.shared
|
|
let scroll = LauncherScrollView(frame: NSRect(x: 0, y: 0, width: 200, height: 120))
|
|
scroll.documentView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 600))
|
|
for height in [120, 240] {
|
|
scroll.frame.size.height = CGFloat(height)
|
|
scroll.contentView.scroll(to: NSPoint(x: 0, y: 80))
|
|
scroll.layoutSubtreeIfNeeded()
|
|
let mask = try #require(scroll.layer?.mask)
|
|
let context = try #require(CGContext(data: nil, width: 200, height: height,
|
|
bitsPerComponent: 8, bytesPerRow: 800, space: CGColorSpaceCreateDeviceRGB(),
|
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue))
|
|
mask.render(in: context)
|
|
let pixels = try #require(context.data?.assumingMemoryBound(to: UInt8.self))
|
|
func alpha(_ y: Int) -> UInt8 { pixels[y * 800 + 100 * 4 + 3] }
|
|
#expect(alpha(0) < 20)
|
|
#expect(alpha(height / 2) == 255)
|
|
#expect(alpha(height - 1) < 20)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor
|
|
func launcherAndEmojiContentStartBelowTheTopFade() throws {
|
|
_ = NSApplication.shared
|
|
let suite = "ConjureFade.\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defer { defaults.removePersistentDomain(forName: suite) }
|
|
func topOpacity(_ rect: NSRect, in scroll: LauncherScrollView) throws -> UInt8 {
|
|
let width = Int(scroll.bounds.width), height = Int(scroll.bounds.height)
|
|
let y = Int(rect.minY - scroll.contentView.bounds.minY)
|
|
try #require((0..<height).contains(y))
|
|
let context = try #require(CGContext(data: nil, width: width, height: height,
|
|
bitsPerComponent: 8, bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(),
|
|
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue))
|
|
let mask = try #require(scroll.layer?.mask)
|
|
mask.render(in: context)
|
|
let pixels = try #require(context.data?.assumingMemoryBound(to: UInt8.self))
|
|
return pixels[y * width * 4 + width / 2 * 4 + 3]
|
|
}
|
|
let launcher = LauncherWindowController(
|
|
history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) })
|
|
)
|
|
let content = try #require(launcher.window?.contentView)
|
|
let search = try #require(content.subviews.compactMap { $0 as? NSStackView }
|
|
.flatMap(\.arrangedSubviews).compactMap { $0 as? NSSearchField }.first)
|
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
|
content.layoutSubtreeIfNeeded()
|
|
let tableScroll = try #require(content.subviews.compactMap { $0 as? LauncherScrollView }.first)
|
|
let table = try #require(tableScroll.documentView as? NSTableView)
|
|
let firstRow = tableScroll.contentView.convert(table.rect(ofRow: 0), from: table)
|
|
#expect(try topOpacity(firstRow, in: tableScroll) == 255)
|
|
|
|
let picker = EmojiPickerController(defaults: defaults)
|
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 400),
|
|
styleMask: [.borderless], backing: .buffered, defer: false)
|
|
window.contentView = picker.view
|
|
picker.view.layoutSubtreeIfNeeded()
|
|
let emojiScroll = try #require(picker.view.subviews.compactMap { $0 as? LauncherScrollView }.first)
|
|
let grid = try #require(emojiScroll.documentView as? NSCollectionView)
|
|
let path = IndexPath(item: 0, section: 0)
|
|
let attributes = try #require(grid.collectionViewLayout?.layoutAttributesForItem(at: path))
|
|
let firstEmoji = emojiScroll.contentView.convert(attributes.frame, from: grid)
|
|
#expect(try topOpacity(firstEmoji, in: emojiScroll) == 255)
|
|
}
|