fix: keep resting content below the top fade
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.
This commit is contained in:
@@ -29,6 +29,8 @@ Open Conjure, type an application name, and press Return to launch it or bring i
|
||||
|
||||
Favorited launcher entries show a small star before their **Application** or **Command** label.
|
||||
|
||||
Launcher results and the emoji grid start below the top fade; scrolling moves content naturally through it.
|
||||
|
||||
Conjure searches the system and user Applications folders. Hover over a result to see its path; duplicate names show their locations. Drag the launcher background to reposition it; its position is remembered.
|
||||
|
||||
Conjure runs without a Dock icon. Dismissing the launcher leaves it running; reopen it from Finder or use your configured global shortcut.
|
||||
|
||||
@@ -39,7 +39,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
layout.itemSize = NSSize(width: 64, height: 56)
|
||||
layout.minimumInteritemSpacing = 6
|
||||
layout.minimumLineSpacing = 6
|
||||
layout.sectionInset = NSEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
|
||||
layout.sectionInset = NSEdgeInsets(top: LauncherScrollView.fadeHeight, left: 16, bottom: 12, right: 16)
|
||||
collection.collectionViewLayout = layout
|
||||
collection.backgroundColors = [.clear]
|
||||
collection.dataSource = self
|
||||
@@ -260,7 +260,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, insetForSectionAt section: Int) -> NSEdgeInsets {
|
||||
NSEdgeInsets(top: section == 1 ? 18 : 12, left: 16, bottom: splitFavorites && section == 0 ? 0 : 12, right: 16)
|
||||
NSEdgeInsets(top: section == 1 ? 18 : LauncherScrollView.fadeHeight, left: 16, bottom: splitFavorites && section == 0 ? 0 : 12, right: 16)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
|
||||
@@ -310,7 +310,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
scrollView.contentView.drawsBackground = false
|
||||
scrollView.horizontalScrollElasticity = .none
|
||||
scrollView.automaticallyAdjustsContentInsets = false
|
||||
scrollView.contentInsets = NSEdgeInsets(top: 16, left: 0, bottom: 16, right: 0)
|
||||
scrollView.contentInsets = NSEdgeInsets(top: LauncherScrollView.fadeHeight, left: 0, bottom: 16, right: 0)
|
||||
|
||||
statusLabel.font = .systemFont(ofSize: 12)
|
||||
statusLabel.textColor = SunsetPalette.secondaryText
|
||||
@@ -1079,6 +1079,7 @@ final class ApplicationCell: NSTableCellView {
|
||||
|
||||
@MainActor
|
||||
final class LauncherScrollView: NSScrollView {
|
||||
static let fadeHeight: CGFloat = 32
|
||||
private let edgeFade = CAGradientLayer()
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
@@ -1095,7 +1096,7 @@ final class LauncherScrollView: NSScrollView {
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
let fade = min(0.5, 32 / max(bounds.height, 1))
|
||||
let fade = min(0.5, Self.fadeHeight / max(bounds.height, 1))
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
edgeFade.frame = bounds
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- [x] add an emoji picker inspired raycast's emoji picker
|
||||
- [x] make the star/favorite symbol a bit smaller when prefixed launcher list entries (e.g. "<star> Application", or "<star> Command")
|
||||
- [ ] currently the top fade (from the top section/overlay, typically some search input) overshadows the top of main content of the window.
|
||||
- [x] currently the top fade (from the top section/overlay, typically some search input) overshadows the top of main content of the window.
|
||||
- spacing should be added such that the fade doesn't overlay the content.
|
||||
- of course, if the user scrolls some scrollable content, the content would (and should) naturally be overshadowed.
|
||||
- [ ] add a notes app inspired by raycast notes
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import ConjureKit
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@@ -23,3 +24,47 @@ func launcherFadesStayAtViewportEdgesAfterScrollingAndResizing() throws {
|
||||
#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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user