feat: fade launcher list edges beneath bars
This commit is contained in:
@@ -143,6 +143,8 @@ Files are never permanently deleted by uninstall. Trash moves preserve file meta
|
||||
|
||||
The launcher keeps its Raycast-inspired 750 × 474-point, titlebar-free rounded panel, large borderless search field, compact icon rows, right-aligned application labels, and bottom-right launch pill. Its Sunset palette uses a dark brown background, soft amber and rust glows, warm cream text, and orange accents on the caret, selection, and action outline.
|
||||
|
||||
The scrolling result list fades to transparent over 16 points at its upper and lower edges, softening the cutoffs below search and above the footer. The fade stays fixed to the viewport while results scroll; the search bar and footer controls remain crisp.
|
||||
|
||||
The application icon uses the same orange `sparkles` SF Symbol on a self-contained, rounded dark-brown tile. `Scripts/icon.swift` renders every standard and Retina icon size, and `make build` packages the resulting `.icns` in the signed bundle for Finder and other system icon surfaces. Conjure remains an accessory app; adding an application icon does not add a Dock or app-switcher entry.
|
||||
|
||||
Settings shares the launcher's dark appearance, frosted HUD material, Sunset background glows, cream headings, and orange control accents. The material and tint extend behind the title bar, while controls stay below it. Native window controls and shortcut recording behavior remain available.
|
||||
|
||||
@@ -257,7 +257,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
}
|
||||
tableView.setAccessibilityLabel("Applications")
|
||||
|
||||
let scrollView = NSScrollView()
|
||||
let scrollView = LauncherScrollView()
|
||||
scrollView.documentView = tableView
|
||||
scrollView.hasVerticalScroller = true
|
||||
scrollView.autohidesScrollers = true
|
||||
@@ -909,6 +909,33 @@ final class ApplicationCell: NSTableCellView {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LauncherScrollView: NSScrollView {
|
||||
private let edgeFade = CAGradientLayer()
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
edgeFade.colors = [NSColor.clear.cgColor, NSColor.black.cgColor,
|
||||
NSColor.black.cgColor, NSColor.clear.cgColor]
|
||||
edgeFade.startPoint = CGPoint(x: 0.5, y: 0)
|
||||
edgeFade.endPoint = CGPoint(x: 0.5, y: 1)
|
||||
layer?.mask = edgeFade
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("Conjure creates its scroll view programmatically") }
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
let fade = min(0.5, 16 / max(bounds.height, 1))
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
edgeFade.frame = bounds
|
||||
edgeFade.locations = [0, NSNumber(value: Double(fade)), NSNumber(value: Double(1 - fade)), 1]
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class ApplicationTableView: NSTableView {
|
||||
var openSelection: (() -> Void)?
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import AppKit
|
||||
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(8) > 80 && alpha(8) < 180)
|
||||
#expect(alpha(20) == 255)
|
||||
#expect(alpha(height / 2) == 255)
|
||||
#expect(alpha(height - 21) == 255)
|
||||
#expect(alpha(height - 9) > 80 && alpha(height - 9) < 180)
|
||||
#expect(alpha(height - 1) < 20)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user