diff --git a/README.md b/README.md index cfc3ed6..1bc3910 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Select an application and open **Actions ⌘K**. **Open** is the first, default- The red **Uninstall application** action morphs the existing launcher into a file-review screen with checkboxes, paths, sizes, and a selected-item count; it does not create another window or review sheet. A native 180-millisecond resize and content crossfade runs in both directions, or switches immediately with Reduce Motion. The filter uses a centered, intrinsic-height text field without built-in search/cancel icons, beside an explicit back chevron. Filtering files by name or path does not change file selections. Up/Down or Control-K/Control-J highlights files; Return toggles the highlighted eligible file, including from the filter input, and Space also toggles when the list has focus. Back or Escape restores the original launcher frame, query, and selection. Temporary review geometry is not saved as the launcher position. Leaving is blocked during confirmation or an uninstall operation. Command-Return or the uninstall button opens a native final confirmation dialog; nothing is moved merely by entering review. The confirmation control uses the launcher's frosted rounded pill with separate Command and Return key badges. The launcher Actions control likewise shows separate Command and K badges matching its Open Return badge. -Keyboard file toggles update the existing checkbox without reloading or fading the list, preserving both the highlighted row and scroll position. The launcher's footer Open and Actions labels and their shortcut badges share 12-point medium text. Launcher and uninstall footer badges use square 18×18-point containers, centering the visible glyph outlines rather than the font's line box. Adjacent key badges have a 2-point gap between their outlines. +Keyboard file toggles update the existing checkbox without reloading or fading the list, preserving both the highlighted row and scroll position. The launcher's footer Open and Actions labels use 12-point medium text. All custom shortcut hints—in the launcher footer, uninstall footer, and actions selector—use `ShortcutIndicator` from `Sources/Conjure/ShortcutIndicator.swift` (for example, `ShortcutIndicator("⌘", "K")`). It owns 12-point medium key text, square 18×18-point containers, visible-glyph centering, border styling, and 2-point gaps between adjacent keys; callers only position or hide the whole indicator. Native menus and shortcut-recording controls retain their platform styling. The actions selector is an arrowless panel inside the launcher, aligned to the bottom and right edges of the footer action pill, replacing it visually while open rather than floating above it. It shares the launcher's frosted Sunset background, 38-point selection row height, 22×22-point icon boxes with 12-point icon-to-text spacing, 14-point action and search text, rounded selection styling, and Return key badge; selection width follows the panel. Application and command rows use the same sizing constants. SF Symbol optical alignment insets are removed so command icons render inside the same bounds as application icons. The application name appears at the top and borderless action search sits eight points from the bottom, without a divider. Native animations crossfade the panel and footer pill: 160 milliseconds to open and 120 milliseconds to close, with unchanged geometry. Reduce Motion disables the animation. Rapid reopening invalidates stale dismissal completions, and closing panels immediately stop intercepting input. Escape, Command-K, an outside click, or switching away closes it and restores the pill. Closing while Conjure remains active restores launcher search focus; switching away does not steal focus back. Return in action search executes the highlighted action: Open launches the application, while Uninstall opens its review. diff --git a/Sources/Conjure/ApplicationActionsController.swift b/Sources/Conjure/ApplicationActionsController.swift index a7fc232..a698b72 100644 --- a/Sources/Conjure/ApplicationActionsController.swift +++ b/Sources/Conjure/ApplicationActionsController.swift @@ -242,7 +242,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat private struct ActionEntry { let view = LauncherRowView() let button: NSButton - let returnKey = NSTextField(labelWithString: "↵") + let returnKey = ShortcutIndicator("↵") init(title: String, symbol: String, color: NSColor) { button = NSButton(title: title, target: nil, action: nil) @@ -259,14 +259,6 @@ private struct ActionEntry { icon.contentTintColor = color icon.imageScaling = .scaleProportionallyDown icon.setAccessibilityElement(false) - returnKey.font = .systemFont(ofSize: 14) - returnKey.textColor = SunsetPalette.secondaryText - returnKey.alignment = .center - returnKey.wantsLayer = true - returnKey.layer?.cornerRadius = 5 - returnKey.layer?.borderWidth = 1 - returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor - returnKey.setAccessibilityElement(false) for child in [icon, button, returnKey] { child.translatesAutoresizingMaskIntoConstraints = false view.addSubview(child) @@ -281,9 +273,7 @@ private struct ActionEntry { button.centerYAnchor.constraint(equalTo: view.centerYAnchor), button.trailingAnchor.constraint(equalTo: returnKey.leadingAnchor, constant: -12), returnKey.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12), - returnKey.centerYAnchor.constraint(equalTo: view.centerYAnchor), - returnKey.widthAnchor.constraint(equalToConstant: 18), - returnKey.heightAnchor.constraint(equalToConstant: 18) + returnKey.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } } diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index e872d90..8d2f187 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -1,6 +1,5 @@ import AppKit import ConjureKit -import CoreText @MainActor final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate { @@ -268,12 +267,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe actionsButton.toolTip = "Actions for the selected application (Command-K)" actionsButton.isEnabled = false openButton.toolTip = "Open the selected application (Return). Use ↑ and ↓ to select." - let returnKey = shortcutBadge("↵") - let commandKey = shortcutBadge("⌘") - let kKey = shortcutBadge("K") - for badge in [returnKey, commandKey, kKey] { - badge.font = openButton.font - } + let returnKey = ShortcutIndicator("↵") + let actionKeys = ShortcutIndicator("⌘", "K") let actionPill = NSVisualEffectView() actionPill.material = .popover @@ -281,7 +276,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe actionPill.state = .active roundMaterialCorners(actionPill, radius: 18) actionPill.layer?.borderWidth = 0.5 - for view in [openButton, returnKey, actionsButton, commandKey, kKey] { + for view in [openButton, returnKey, actionsButton, actionKeys] { view.translatesAutoresizingMaskIntoConstraints = false actionPill.addSubview(view) } @@ -320,20 +315,15 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe openButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), returnKey.leadingAnchor.constraint(equalTo: openButton.trailingAnchor, constant: 8), actionsButton.leadingAnchor.constraint(equalTo: returnKey.trailingAnchor, constant: 14), - commandKey.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8), - commandKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), - kKey.leadingAnchor.constraint(equalTo: commandKey.trailingAnchor, constant: 2), - kKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), - kKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12), + actionKeys.leadingAnchor.constraint(equalTo: actionsButton.trailingAnchor, constant: 8), + actionKeys.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), + actionKeys.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12), actionsButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), - returnKey.widthAnchor.constraint(equalToConstant: 18), - returnKey.heightAnchor.constraint(equalToConstant: 18), ]) window.effectiveAppearance.performAsCurrentDrawingAppearance { let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor content.layer?.borderColor = border - returnKey.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor actionPill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor } @@ -704,41 +694,3 @@ func roundMaterialCorners(_ view: NSVisualEffectView, radius: CGFloat) { view.maskImage = mask } -@MainActor -func shortcutBadge(_ title: String) -> NSTextField { - let badge = ShortcutBadge(labelWithString: title) - badge.font = .systemFont(ofSize: 14) - badge.textColor = SunsetPalette.secondaryText - badge.alignment = .center - badge.wantsLayer = true - badge.layer?.cornerRadius = 5 - badge.layer?.borderWidth = 1 - badge.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor - badge.setAccessibilityElement(false) - badge.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - badge.widthAnchor.constraint(equalToConstant: 18), - badge.heightAnchor.constraint(equalToConstant: 18) - ]) - return badge -} - -@MainActor -private final class ShortcutBadge: NSTextField { - // NSTextField's horizontal alignment insets otherwise make an 18-point badge 22 points wide. - override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } - - override func draw(_ dirtyRect: NSRect) { - guard let context = NSGraphicsContext.current?.cgContext else { return } - let line = CTLineCreateWithAttributedString(attributedStringValue) - let ink = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - context.saveGState() - context.textMatrix = .identity - // Center visible ink, not the font's asymmetric ascender/descender line box. - context.translateBy(x: bounds.midX - ink.midX, y: bounds.midY + ink.midY) - context.scaleBy(x: 1, y: -1) - context.textPosition = .zero - CTLineDraw(line, context) - context.restoreGState() - } -} diff --git a/Sources/Conjure/ShortcutIndicator.swift b/Sources/Conjure/ShortcutIndicator.swift new file mode 100644 index 0000000..2effca6 --- /dev/null +++ b/Sources/Conjure/ShortcutIndicator.swift @@ -0,0 +1,55 @@ +import AppKit +import CoreText + +@MainActor +final class ShortcutIndicator: NSStackView { + init(_ keys: String...) { + super.init(frame: .zero) + orientation = .horizontal + alignment = .centerY + spacing = 2 + setHuggingPriority(.required, for: .horizontal) + setHuggingPriority(.required, for: .vertical) + translatesAutoresizingMaskIntoConstraints = false + setAccessibilityElement(false) + for key in keys { + let badge = ShortcutKeyBadge(labelWithString: key) + badge.font = .systemFont(ofSize: 12, weight: .medium) + badge.textColor = SunsetPalette.secondaryText + badge.alignment = .center + badge.wantsLayer = true + badge.layer?.cornerRadius = 5 + badge.layer?.borderWidth = 1 + badge.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor + badge.setAccessibilityElement(false) + badge.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + badge.widthAnchor.constraint(equalToConstant: 18), + badge.heightAnchor.constraint(equalToConstant: 18) + ]) + addArrangedSubview(badge) + } + } + + required init?(coder: NSCoder) { fatalError("Conjure creates shortcut indicators programmatically") } +} + +@MainActor +private final class ShortcutKeyBadge: NSTextField { + // NSTextField's horizontal alignment insets otherwise make an 18-point badge 22 points wide. + override var alignmentRectInsets: NSEdgeInsets { NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } + + override func draw(_ dirtyRect: NSRect) { + guard let context = NSGraphicsContext.current?.cgContext else { return } + let line = CTLineCreateWithAttributedString(attributedStringValue) + let ink = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + context.saveGState() + context.textMatrix = .identity + // Center visible ink, not the font's asymmetric ascender/descender line box. + context.translateBy(x: bounds.midX - ink.midX, y: bounds.midY + ink.midY) + context.scaleBy(x: 1, y: -1) + context.textPosition = .zero + CTLineDraw(line, context) + context.restoreGState() + } +} diff --git a/Sources/Conjure/UninstallReviewController.swift b/Sources/Conjure/UninstallReviewController.swift index 8a21451..6822a3b 100644 --- a/Sources/Conjure/UninstallReviewController.swift +++ b/Sources/Conjure/UninstallReviewController.swift @@ -118,9 +118,8 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate, pill.layer?.borderWidth = 0.5 pill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor pill.layer?.backgroundColor = SunsetPalette.accent.withAlphaComponent(0.08).cgColor - let commandKey = shortcutBadge("⌘") - let returnKey = shortcutBadge("↵") - for child in [remove, commandKey, returnKey] { + let shortcut = ShortcutIndicator("⌘", "↵") + for child in [remove, shortcut] { child.translatesAutoresizingMaskIntoConstraints = false pill.addSubview(child) } @@ -128,11 +127,9 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate, pill.heightAnchor.constraint(equalToConstant: 36), remove.leadingAnchor.constraint(equalTo: pill.leadingAnchor, constant: 14), remove.centerYAnchor.constraint(equalTo: pill.centerYAnchor), - commandKey.leadingAnchor.constraint(equalTo: remove.trailingAnchor, constant: 8), - commandKey.centerYAnchor.constraint(equalTo: pill.centerYAnchor), - returnKey.leadingAnchor.constraint(equalTo: commandKey.trailingAnchor, constant: 2), - returnKey.centerYAnchor.constraint(equalTo: pill.centerYAnchor), - returnKey.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -12) + shortcut.leadingAnchor.constraint(equalTo: remove.trailingAnchor, constant: 8), + shortcut.centerYAnchor.constraint(equalTo: pill.centerYAnchor), + shortcut.trailingAnchor.constraint(equalTo: pill.trailingAnchor, constant: -12) ]) let footer = NSStackView(views: [NSView(), pill]) footer.orientation = .horizontal diff --git a/TODO.md b/TODO.md index f0d05c4..beb5b27 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,7 @@ - [x] make the actions indicator font size smaller. -- [ ] decrease the distance between the icons and their text by 1/3. -- [ ] add unit conversion and calculator behavior like raylib has. +- [x] decrease the distance between the icons and their text by 1/3. - [ ] add more actions to the actions selector: - show in finder - reset ranking - - quit application \ No newline at end of file + - quit application +- [ ] add unit conversion and calculator behavior like raylib has. \ No newline at end of file