Skip to content

Commit

Permalink
Add support for copying HSB color
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Nov 12, 2021
1 parent 21599b9 commit 193eeb7
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 12 deletions.
8 changes: 4 additions & 4 deletions Color Picker.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -626,31 +626,31 @@
repositoryURL = "https://github.com/sindresorhus/Regex";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.1.1;
minimumVersion = 1.0.0;
};
};
E394DAAC263E95D900F5B042 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.1.0;
minimumVersion = 4.2.0;
};
};
E394DAB0263E965500F5B042 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.1;
minimumVersion = 1.3.0;
};
};
E3E14060259A0D97004FC89F /* XCRemoteSwiftPackageReference "Defaults" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/Defaults";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.0.0;
minimumVersion = 6.1.0;
};
};
E3E9F9AB2642B75100AE6450 /* XCRemoteSwiftPackageReference "appcenter-sdk-apple" */ = {
Expand Down
63 changes: 55 additions & 8 deletions Color Picker/ColorPickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,48 +26,90 @@ 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()
}
.onDisappearFromScreen {
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 {
Expand All @@ -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",
Expand All @@ -113,8 +156,9 @@ struct ColorPickerView: View {
hexColor.copyToPasteboard()
} label: {
Image(systemName: "doc.on.doc.fill")
.controlSize(.small)
}
.buttonStyle(.borderless)
.contentShape(.rectangle)
.keyboardShortcut("H")
}
}
Expand Down Expand Up @@ -145,8 +189,9 @@ struct ColorPickerView: View {
hslColor.copyToPasteboard()
} label: {
Image(systemName: "doc.on.doc.fill")
.controlSize(.small)
}
.buttonStyle(.borderless)
.contentShape(.rectangle)
.keyboardShortcut("S")
}
}
Expand Down Expand Up @@ -177,8 +222,9 @@ struct ColorPickerView: View {
rgbColor.copyToPasteboard()
} label: {
Image(systemName: "doc.on.doc.fill")
.controlSize(.small)
}
.buttonStyle(.borderless)
.contentShape(.rectangle)
.keyboardShortcut("R")
}
}
Expand Down Expand Up @@ -209,8 +255,9 @@ struct ColorPickerView: View {
lchColor.copyToPasteboard()
} label: {
Image(systemName: "doc.on.doc.fill")
.controlSize(.small)
}
.buttonStyle(.borderless)
.contentShape(.rectangle)
.keyboardShortcut("L")
}
}
Expand Down Expand Up @@ -295,6 +342,6 @@ struct ColorPickerView: View {

struct ColorPickerView_Previews: PreviewProvider {
static var previews: some View {
ColorPickerView(colorPanel: NSColorPanel.shared)
ColorPickerView(colorPanel: .shared)
}
}
103 changes: 103 additions & 0 deletions Color Picker/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ extension NSColor {
var lchColorString: String {
usingColorSpace(.sRGB)!.format(.cssLCH)
}

var hsbColorString: String {
format(.hsb)
}

var stringRepresentation: String {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -819,6 +854,7 @@ extension NSColor {
case cssLCH
case cssHSLLegacy
case cssRGBLegacy
case hsb
}

/**
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -2643,3 +2685,64 @@ extension NSImage {
}
}
#endif


// TODO: Remove when targeting macOS 12.
extension View {
func overlay2<Overlay: View>(
alignment: Alignment = .center,
@ViewBuilder content: () -> Overlay
) -> some View {
overlay(ZStack(content: content), alignment: alignment)
}

func background2<V: View>(
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
}
}
}

0 comments on commit 193eeb7

Please sign in to comment.