From 0ba61153d4e2bbca4a3c8a4ddce71500446be6ab Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Thu, 15 Oct 2026 15:35:20 +0200 Subject: [PATCH] fix: center emoji artwork in launcher icon slot --- README.md | 2 +- .../Conjure/LauncherWindowController.swift | 11 ++++++-- Tests/ConjureKitTests/EmojiIconTests.swift | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 Tests/ConjureKitTests/EmojiIconTests.swift diff --git a/README.md b/README.md index 71e2ad6..ff4205f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Currency and crypto conversions require internet access and use [currency-api](h ## Emoji picker -Search for **Conjure Emoji** and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji. +Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji. Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right. Return or clicking an emoji copies it and dismisses Conjure, restoring focus to the previous app. Paste with Command-V; no Accessibility permission is needed. Escape or the Back chevron returns to the launcher and restores your search. diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index 7eaee25..dfcd229 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -1,5 +1,6 @@ import AppKit import ConjureKit +import CoreText @MainActor final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate { @@ -40,9 +41,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe private var pendingPresentation: (() -> Void)? private static let emojiIcon: NSImage = { let text = NSAttributedString(string: "🤔", attributes: [.font: NSFont.systemFont(ofSize: 28)]) - let size = text.size() - return NSImage(size: size, flipped: false) { _ in - text.draw(at: .zero) + let line = CTLineCreateWithAttributedString(text) + return NSImage(size: NSSize(width: 32, height: 32), flipped: false) { rect in + guard let context = NSGraphicsContext.current?.cgContext else { return false } + context.textPosition = .zero + let bounds = CTLineGetImageBounds(line, context) + context.textPosition = CGPoint(x: rect.midX - bounds.midX, y: rect.midY - bounds.midY) + CTLineDraw(line, context) return true } }() diff --git a/Tests/ConjureKitTests/EmojiIconTests.swift b/Tests/ConjureKitTests/EmojiIconTests.swift new file mode 100644 index 0000000..aa616e1 --- /dev/null +++ b/Tests/ConjureKitTests/EmojiIconTests.swift @@ -0,0 +1,28 @@ +import AppKit +import Testing +@testable import Conjure + +@Test @MainActor func emojiCommandArtworkHasBalancedVisiblePadding() throws { + _ = NSApplication.shared + let launcher = LauncherWindowController() + func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } + let views = descendants(try #require(launcher.window?.contentView)) + let search = try #require(views.compactMap { $0 as? NSSearchField }.first) + search.stringValue = "emoji" + launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) + let table = try #require(views.compactMap { $0 as? NSTableView }.first) + let cell = try #require(launcher.tableView(table, viewFor: nil, row: 0) as? ApplicationCell) + let image = try #require(cell.imageView?.image) + let data = try #require(image.tiffRepresentation) + let bitmap = try #require(NSBitmapImageRep(data: data)) + var left = bitmap.pixelsWide, right = -1, top = bitmap.pixelsHigh, bottom = -1 + for y in 0.. 0.5 { + left = min(left, x); right = max(right, x) + top = min(top, y); bottom = max(bottom, y) + } + } + #expect(right > left && bottom > top) + #expect(abs(left - (bitmap.pixelsWide - right - 1)) <= 2) + #expect(abs(top - (bitmap.pixelsHigh - bottom - 1)) <= 2) +}