diff --git a/README.md b/README.md index 0eb778f..31c7baa 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ The launcher opens without a window animation; Settings is constructed only when Right-click the launcher background for Settings and Quit. Actions include favorites, resetting usage ranking, revealing applications in Finder, quitting them, and uninstalling them. +The Actions surface crops the launcher’s full-size background at its own position, keeping the glows continuous without showing the results underneath. + ### Global shortcuts and launch at login In Settings, choose a **Global shortcut destination**: **Applications**, **Conjure Emoji**, or **Conjure Translate**. Choose **Record Shortcut…** and press a combination containing Command, Option, or Control. For example, select **Conjure Emoji** and record Command-Shift-Space to open the emoji picker directly. **Clear** disables only the selected destination's shortcut. No shortcuts are assigned by default; existing launcher bindings are preserved. diff --git a/Sources/Conjure/ApplicationActionsController.swift b/Sources/Conjure/ApplicationActionsController.swift index 0bde349..69ec1fc 100644 --- a/Sources/Conjure/ApplicationActionsController.swift +++ b/Sources/Conjure/ApplicationActionsController.swift @@ -4,6 +4,7 @@ import ConjureKit @MainActor final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate { private let search = ActionsSearchField() + private let background = LauncherBackground(frame: .zero) private let entries = [ ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text), ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text), @@ -49,11 +50,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat view.layer?.shadowOffset = NSSize(width: 0, height: -4) let content = NSVisualEffectView() content.material = .hudWindow - content.blendingMode = .withinWindow + content.blendingMode = .behindWindow content.state = .active roundMaterialCorners(content, radius: 18) content.layer?.borderWidth = 0.5 - let background = LauncherBackground(frame: .zero) background.setAccessibilityElement(false) content.addSubview(background) applicationLabel.textColor = SunsetPalette.secondaryText @@ -108,10 +108,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat content.trailingAnchor.constraint(equalTo: view.trailingAnchor), content.topAnchor.constraint(equalTo: view.topAnchor), content.bottomAnchor.constraint(equalTo: view.bottomAnchor), - background.leadingAnchor.constraint(equalTo: content.leadingAnchor), - background.trailingAnchor.constraint(equalTo: content.trailingAnchor), - background.topAnchor.constraint(equalTo: content.topAnchor), - background.bottomAnchor.constraint(equalTo: content.bottomAnchor), applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16), applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16), applicationLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14), @@ -164,6 +160,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat view.alphaValue = 0 view.translatesAutoresizingMaskIntoConstraints = false host.addSubview(view) + // Crop the window-sized background; filtering must not rescale its glows. + NSLayoutConstraint.activate([ + background.leadingAnchor.constraint(equalTo: host.leadingAnchor), + background.trailingAnchor.constraint(equalTo: host.trailingAnchor), + background.topAnchor.constraint(equalTo: host.topAnchor), + background.bottomAnchor.constraint(equalTo: host.bottomAnchor) + ]) NSLayoutConstraint.activate([ view.trailingAnchor.constraint(equalTo: pill.trailingAnchor), view.bottomAnchor.constraint(equalTo: pill.bottomAnchor) diff --git a/TODO.md b/TODO.md index 524ea20..b530959 100644 --- a/TODO.md +++ b/TODO.md @@ -2,5 +2,12 @@ - the window should scroll with the current selection - [x] make escape close the window that was opened, be it the application launcher, the emoji picker, or something else. - clicking the "back" chevron should still take you back to the application launcher view though (as it does today). +- [ ] remove window open/fade in/transition animation when opening conjure from keybindings + - this is already the case for the application launcher window. + - evidently not the case for the emoji picker (though it should be ubiquitous) - [ ] add a notes app inspired by raycast notes + - support multiple different notes + - support switching between notes + - highlight markdown syntax + - allow images - [ ] improve on the conjure translation command \ No newline at end of file diff --git a/Tests/ConjureKitTests/ActionsBackgroundTests.swift b/Tests/ConjureKitTests/ActionsBackgroundTests.swift new file mode 100644 index 0000000..038a7a1 --- /dev/null +++ b/Tests/ConjureKitTests/ActionsBackgroundTests.swift @@ -0,0 +1,31 @@ +import AppKit +import ConjureKit +import Testing +@testable import Conjure + +@Test @MainActor func actionsBackgroundKeepsWindowCoordinates() throws { + _ = NSApplication.shared + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 480), + styleMask: [.borderless], backing: .buffered, defer: false) + let host = try #require(window.contentView) + let anchor = NSView(frame: NSRect(x: 700, y: 20, width: 30, height: 30)) + host.addSubview(anchor) + let actions = ApplicationActionsController() + defer { actions.close(animated: false) } + func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } + for width in [750.0, 900.0] { + window.setContentSize(NSSize(width: width, height: 480)) + actions.showEmoji(named: "Smile", relativeTo: anchor) + let background = try #require(descendants(host).compactMap { $0 as? LauncherBackground }.first) + let search = try #require(descendants(actions.view).compactMap { $0 as? NSSearchField }.first) + for query in ["", "Copy Emoji", "no matching action"] { + search.stringValue = query + actions.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) + host.layoutSubtreeIfNeeded() + let rect = background.convert(background.bounds, to: host) + #expect(rect == host.bounds) + #expect(background.bounds.size != actions.view.bounds.size) + } + actions.close(animated: false) + } +}