diff --git a/Color Picker.xcodeproj/project.pbxproj b/Color Picker.xcodeproj/project.pbxproj index fc3f2b9..40480aa 100644 --- a/Color Picker.xcodeproj/project.pbxproj +++ b/Color Picker.xcodeproj/project.pbxproj @@ -626,7 +626,7 @@ repositoryURL = "https://github.com/sindresorhus/Regex"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 0.1.1; + minimumVersion = 1.0.0; }; }; E394DAAC263E95D900F5B042 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = { @@ -634,7 +634,7 @@ repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 4.1.0; + minimumVersion = 4.2.0; }; }; E394DAB0263E965500F5B042 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { @@ -642,7 +642,7 @@ repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.1; + minimumVersion = 1.3.0; }; }; E3E14060259A0D97004FC89F /* XCRemoteSwiftPackageReference "Defaults" */ = { @@ -650,7 +650,7 @@ repositoryURL = "https://github.com/sindresorhus/Defaults"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.0.0; + minimumVersion = 6.1.0; }; }; E3E9F9AB2642B75100AE6450 /* XCRemoteSwiftPackageReference "appcenter-sdk-apple" */ = { diff --git a/Color Picker/ColorPickerView.swift b/Color Picker/ColorPickerView.swift index 04ee36a..ca41fb2 100644 --- a/Color Picker/ColorPickerView.swift +++ b/Color Picker/ColorPickerView.swift @@ -26,41 +26,64 @@ private struct RecentlyPickedColorsButton: View { .labelStyle(.titleAndIcon) } } + // Without, it becomes disabled. (macOS 12.0.1) + .buttonStyle(.automatic) } label: { Image(systemName: "clock.fill") + .controlSize(.large) +// .padding(8) // Has no effect. (macOS 12.0.1) + .contentShape(.rectangle) } - // TODO: Use `.menuIndicator(.hidden)` when targeting macOS 12. - .menuStyle(.borderedButton) + .menuIndicatorHidden() + .padding(8) .fixedSize() + .opacity(0.6) // Try to match the other buttons. .disabled(recentlyPickedColors.isEmpty) .help(recentlyPickedColors.isEmpty ? "No recently picked colors" : "Recently picked colors") } } private struct BarView: View { + @Environment(\.colorScheme) private var colorScheme @EnvironmentObject private var appState: AppState @StateObject private var pasteboardObserver = NSPasteboard.SimpleObservable(.general).stop() var body: some View { - HStack { + HStack(spacing: 12) { Button { appState.pickColor() } label: { Image(systemName: "eyedropper") + .font(.system(size: 14).bold()) + .padding(8) } + .contentShape(.rectangle) .help("Pick color") .keyboardShortcut("p") + .padding(.leading, 4) Button { appState.pasteColor() } label: { Image(systemName: "paintbrush.fill") + .padding(8) } + .contentShape(.rectangle) .help("Paste color in the format Hex, HSL, RGB, or LCH") .keyboardShortcut("V") .disabled(NSColor.fromPasteboardGraceful(.general) == nil) RecentlyPickedColorsButton() + moreButton Spacer() } + // Cannot do this as the `Menu` buttons don't respect it. (macOS 12.0.1) +// .font(.title3) + .background2 { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(Color.black.opacity(colorScheme == .dark ? 0.17 : 0.05)) + } + .padding(.vertical, 4) + .buttonStyle(.borderless) + .menuStyle(.borderlessButton) .onAppearOnScreen { pasteboardObserver.start() } @@ -68,6 +91,25 @@ private struct BarView: View { pasteboardObserver.stop() } } + + private var moreButton: some View { + Menu { + Button("Copy as HSB") { + appState.colorPanel.color.hsbColorString.copyToPasteboard() + } + // Without, it becomes disabled. (macOS 12.0.1) + .buttonStyle(.automatic) + } label: { + Label("More", systemImage: "ellipsis.circle.fill") + .labelStyle(.iconOnly) +// .padding(8) // Has no effect. (macOS 12.0.1) + } + .padding(8) + .contentShape(.rectangle) + .fixedSize() + .opacity(0.6) // Try to match the other buttons. + .menuIndicatorHidden() + } } struct ColorPickerView: View { @@ -89,6 +131,7 @@ struct ColorPickerView: View { private var hexColorView: some View { HStack { + // TODO: When I use `TextField`, add the copy button using `.safeAreaInset()`. NativeTextField( text: $hexColor, placeholder: "Hex", @@ -113,8 +156,9 @@ struct ColorPickerView: View { hexColor.copyToPasteboard() } label: { Image(systemName: "doc.on.doc.fill") - .controlSize(.small) } + .buttonStyle(.borderless) + .contentShape(.rectangle) .keyboardShortcut("H") } } @@ -145,8 +189,9 @@ struct ColorPickerView: View { hslColor.copyToPasteboard() } label: { Image(systemName: "doc.on.doc.fill") - .controlSize(.small) } + .buttonStyle(.borderless) + .contentShape(.rectangle) .keyboardShortcut("S") } } @@ -177,8 +222,9 @@ struct ColorPickerView: View { rgbColor.copyToPasteboard() } label: { Image(systemName: "doc.on.doc.fill") - .controlSize(.small) } + .buttonStyle(.borderless) + .contentShape(.rectangle) .keyboardShortcut("R") } } @@ -209,8 +255,9 @@ struct ColorPickerView: View { lchColor.copyToPasteboard() } label: { Image(systemName: "doc.on.doc.fill") - .controlSize(.small) } + .buttonStyle(.borderless) + .contentShape(.rectangle) .keyboardShortcut("L") } } @@ -295,6 +342,6 @@ struct ColorPickerView: View { struct ColorPickerView_Previews: PreviewProvider { static var previews: some View { - ColorPickerView(colorPanel: NSColorPanel.shared) + ColorPickerView(colorPanel: .shared) } } diff --git a/Color Picker/Utilities.swift b/Color Picker/Utilities.swift index 5e3046c..eec2ff6 100644 --- a/Color Picker/Utilities.swift +++ b/Color Picker/Utilities.swift @@ -32,6 +32,9 @@ extension NSColor { var lchColorString: String { usingColorSpace(.sRGB)!.format(.cssLCH) } + + var hsbColorString: String { + format(.hsb) } var stringRepresentation: String { @@ -462,6 +465,38 @@ extension NSColor { extension NSColor { typealias HSB = (hue: Double, saturation: Double, brightness: Double, alpha: Double) + /** + This preserves the original color space as long as it is RGB, otherwise, it is normalized to extended sRGB. + */ + var hsbRaw: HSB { + var color = self + + if colorSpace.colorSpaceModel != .rgb { + guard let color_ = usingColorSpace(.extendedSRGB) else { + assertionFailure("Unsupported color space") + return HSB(0, 0, 0, 0) + } + + color = color_ + } + + // swiftlint:disable no_cgfloat + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + // swiftlint:enable no_cgfloat + + color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + + return HSB( + hue: hue.double, + saturation: saturation.double, + brightness: brightness.double, + alpha: alpha.double + ) + } + var hsb: HSB { #if canImport(AppKit) guard let color = usingColorSpace(.extendedSRGB) else { @@ -819,6 +854,7 @@ extension NSColor { case cssLCH case cssHSLLegacy case cssRGBLegacy + case hsb } /** @@ -868,6 +904,12 @@ extension NSColor { let green = Int((rgb.green * 0xFF).rounded()) let blue = Int((rgb.blue * 0xFF).rounded()) return String(format: "rgb(%d, %d, %d)", red, green, blue) + case .hsb: + let hsb = hsbRaw // We use the current color space. + let hue = Int((hsb.hue * 360).rounded()) + let saturation = Int((hsb.saturation * 100).rounded()) + let brightness = Int((hsb.brightness * 100).rounded()) + return String(format: "%d %d%% %d%%", hue, saturation, brightness) } } } @@ -2643,3 +2685,64 @@ extension NSImage { } } #endif + + +// TODO: Remove when targeting macOS 12. +extension View { + func overlay2( + alignment: Alignment = .center, + @ViewBuilder content: () -> Overlay + ) -> some View { + overlay(ZStack(content: content), alignment: alignment) + } + + func background2( + alignment: Alignment = .center, + @ViewBuilder content: () -> V + ) -> some View { + background(ZStack(content: content), alignment: alignment) + } +} + + +extension Shape where Self == Rectangle { + static var rectangle: Self { .init() } +} + +extension Shape where Self == Circle { + static var circle: Self { .init() } +} + +extension Shape where Self == Capsule { + static var capsule: Self { .init() } +} + +extension Shape where Self == Ellipse { + static var ellipse: Self { .init() } +} + +extension Shape where Self == ContainerRelativeShape { + static var containerRelative: Self { .init() } +} + +extension Shape where Self == RoundedRectangle { + static func roundedRectangle(cornerRadius: Double, style: RoundedCornerStyle = .circular) -> Self { + .init(cornerRadius: cornerRadius, style: style) + } + + static func roundedRectangle(cornerSize: CGSize, style: RoundedCornerStyle = .circular) -> Self { + .init(cornerSize: cornerSize, style: style) + } +} + + +extension View { + @ViewBuilder + func menuIndicatorHidden() -> some View { + if #available(macOS 12, *) { + menuIndicator(.hidden) + } else { + self + } + } +}