From 27c203b336d7b0a93bb31094cdf94605624b1b54 Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Fri, 16 Oct 2026 09:26:04 +0200 Subject: [PATCH] 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. --- README.md | 2 + Sources/Conjure/EmojiPickerController.swift | 4 +- .../Conjure/LauncherWindowController.swift | 5 ++- TODO.md | 2 +- Tests/ConjureKitTests/LauncherFadeTests.swift | 45 +++++++++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c1ae7a4..d83389b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Sources/Conjure/EmojiPickerController.swift b/Sources/Conjure/EmojiPickerController.swift index 591e632..7efe47d 100644 --- a/Sources/Conjure/EmojiPickerController.swift +++ b/Sources/Conjure/EmojiPickerController.swift @@ -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 { diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index c6cf37d..ee118a4 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -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 diff --git a/TODO.md b/TODO.md index 8924ee4..f58f0ba 100644 --- a/TODO.md +++ b/TODO.md @@ -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. " Application", or " 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 diff --git a/Tests/ConjureKitTests/LauncherFadeTests.swift b/Tests/ConjureKitTests/LauncherFadeTests.swift index dc9f213..2ad057f 100644 --- a/Tests/ConjureKitTests/LauncherFadeTests.swift +++ b/Tests/ConjureKitTests/LauncherFadeTests.swift @@ -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..