19 |
20 | ColorWell is designed to mimic the appearance and behavior of the new color well design in macOS 13 Ventura, for those who want to use the new design on older operating systems. While the goal is for ColorWell to look and behave in a similar way to Apple's design, it is not an exact clone. There are a number of subtle design differences ranging from the way system colors are handled to the size of the drop shadow. However, in practice, there are very few notable differences:
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ## Install
31 |
32 | Add the following dependency to your `Package.swift` file:
33 |
34 | ```swift
35 | .package(url: "https://github.com/jordanbaird/ColorWell", from: "0.2.2")
36 | ```
37 |
38 | ## Usage
39 |
40 | [Read the full documentation here](https://swiftpackageindex.com/jordanbaird/ColorWell/documentation)
41 |
42 | ### SwiftUI
43 |
44 | Create a `ColorWellView` and add it to your view hierarchy. There are a wide range of initializers, as well as several modifiers to choose from, allowing you to set the color well's color, label, and action.
45 |
46 | ```swift
47 | import SwiftUI
48 | import ColorWell
49 |
50 | struct ContentView: View {
51 | @Binding var fontColor: Color
52 |
53 | var body: some View {
54 | VStack {
55 | ColorWellView("Font Color", color: fontColor, action: updateFontColor)
56 | .colorWellStyle(.expanded)
57 |
58 | MyCustomTextEditor(fontColor: $fontColor)
59 | }
60 | }
61 |
62 | private func updateFontColor(_ color: Color) {
63 | fontColor = color
64 | }
65 | }
66 | ```
67 |
68 | ### Cocoa
69 |
70 | Create a `ColorWell` using one of the available initializers. Observe color changes using the `onColorChange(perform:)` method.
71 |
72 | ```swift
73 | import Cocoa
74 | import ColorWell
75 |
76 | class ContentViewController: NSViewController {
77 | let colorWell: ColorWell
78 | let textEditor: MyCustomNSTextEditor
79 |
80 | init(fontColor: NSColor) {
81 | self.colorWell = ColorWell(color: fontColor)
82 | self.textEditor = MyCustomNSTextEditor(fontColor: fontColor)
83 |
84 | super.init(nibName: "ContentView", bundle: Bundle(for: Self.self))
85 |
86 | // Set the style
87 | colorWell.style = .expanded
88 |
89 | // Add a change handler
90 | colorWell.onColorChange { newColor in
91 | self.textEditor.fontColor = newColor
92 | }
93 | }
94 |
95 | override func viewDidLoad() {
96 | super.viewDidLoad()
97 |
98 | view.addSubview(colorWell)
99 | view.addSubview(textEditor)
100 |
101 | // Layout the views, perform setup work, etc.
102 | }
103 | }
104 | ```
105 |
106 | ## License
107 |
108 | ColorWell is available under the [MIT license](LICENSE).
109 |
110 | [ci-badge]: https://img.shields.io/github/actions/workflow/status/jordanbaird/ColorWell/test.yml?branch=main&style=flat-square
111 | [release-badge]: https://img.shields.io/github/v/release/jordanbaird/ColorWell?style=flat-square
112 | [versions-badge]: https://img.shields.io/badge/dynamic/json?color=F05138&label=Swift&query=%24.message&url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fjordanbaird%2FColorWell%2Fbadge%3Ftype%3Dswift-versions&style=flat-square
113 | [docs-badge]: https://img.shields.io/static/v1?label=%20&message=documentation&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEsSURBVHgB7dntDYIwEAbgV+MAuoEj6AaO4AiO4AayAbqBbuAGjoIbwAbnHT8MMTH9uEJrvCch/FB7vEh7EABjjBMRnXhrKY1GxsNUuFhN45gmBKU783lCDKtBiYeoUoeYI79KE6KEACI6RCkBRFSIkgKI4BClBRBBIUoMILxDlBpASIgjtBL3gR2FaV1jzjyKvg98xqDEw615t3Z87eFbc/IAPkJqljwHvFiA3CxAbhaAdI+cNZTUfWD4edQBOMacog9cEE/z25514twsQG4/H2ABJZ5vG97tEefKc/QJhRR9oIH7AeWbjodchdYcSnEJLRGvg5L6EmJb3g6Ic4eSNbLcLEBuf9HIZKnrl0rtvX8E5zLr8w+o79kVbkiBT/yZxn3Z90lqVTDGOL0AoGWIIaQgyakAAAAASUVORK5CYII=&color=informational&labelColor=gray&style=flat-square
114 | [license-badge]: https://img.shields.io/github/license/jordanbaird/ColorWell?style=flat-square
115 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Cocoa/Collections/ColorWell-swift.class.Deprecated.md:
--------------------------------------------------------------------------------
1 | # Deprecated Symbols
2 |
3 | Review unsupported symbols and their replacements.
4 |
5 | ## Topics
6 |
7 | ### Initializers
8 |
9 | - ``ColorWell/ColorWell/init(ciColor:)``
10 |
11 | ### Instance Properties
12 |
13 | - ``ColorWell/ColorWell/colorPanel``
14 | - ``ColorWell/ColorWell/showsAlpha``
15 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Cocoa/Extensions/ColorWell-swift.class.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ColorWell``
2 |
3 | ## Topics
4 |
5 | ### Designated Initializers
6 |
7 | - ``init(frame:color:style:)``
8 | - ``init(coder:)``
9 |
10 | ### Convenience Initializers
11 |
12 | - ``init(frame:)``
13 | - ``init(color:)``
14 | - ``init(style:)``
15 | - ``init()``
16 | - ``init(frame:color:)``
17 | - ``init(cgColor:)``
18 | - ``init(coreImageColor:)``
19 | - ``init(_:)``
20 |
21 | ### Instance Properties
22 |
23 | - ``allowsMultipleSelection``
24 | - ``color``
25 | - ``isActive``
26 | - ``isEnabled``
27 | - ``style-swift.property``
28 | - ``swatchColors``
29 |
30 | ### Instance Methods
31 |
32 | - ``activate(exclusive:)``
33 | - ``deactivate()``
34 | - ``onColorChange(perform:)``
35 |
36 | ### Supporting Types
37 |
38 | - ``Style-swift.enum``
39 |
40 | ### Deprecated
41 |
42 | -
43 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Cocoa/Extensions/ColorWell.Style/ColorWell.Style.init(rawValue:).md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ColorWell/Style-swift.enum/init(rawValue:)``
2 |
3 | Creates a new color well style with the specified raw value.
4 |
5 | If the specified raw value does not correspond to any of the existing color well styles, this initializer returns `nil`.
6 |
7 | ```swift
8 | print(Style(rawValue: 0))
9 | // Prints "Optional(Style.expanded)"
10 |
11 | print(Style(rawValue: 1))
12 | // Prints "Optional(Style.swatches)"
13 |
14 | print(Style(rawValue: 2))
15 | // Prints "Optional(Style.colorPanel)"
16 |
17 | print(Style(rawValue: 3))
18 | // Prints "nil"
19 | ```
20 |
21 | - Parameter rawValue: The raw value to use for the new instance.
22 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Cocoa/Extensions/ColorWell.Style/ColorWell.Style.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ColorWell/Style-swift.enum``
2 |
3 | ## Topics
4 |
5 | ### Getting the available styles
6 |
7 | - ``expanded``
8 | - ``swatches``
9 | - ``colorPanel``
10 |
11 | ### Creating a style from a raw value
12 |
13 | - ``init(rawValue:)``
14 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/ColorWell.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell``
2 |
3 | A versatile alternative to `NSColorWell` for Cocoa and `ColorPicker` for SwiftUI.
4 |
5 | ## Overview
6 |
7 | ColorWell is designed to mimic the appearance and behavior of the new color well design in macOS 13 Ventura, for those who want to use the new design on older operating systems.
8 |
9 | | Light mode | Dark mode |
10 | | --------------- | -------------- |
11 | | ![][light-mode] | ![][dark-mode] |
12 |
13 | ## SwiftUI
14 |
15 | Create a ``ColorWellView`` and add it to your view hierarchy. There are a wide range of initializers, as well as several modifiers to choose from, allowing you to set the color well's color, label, and action.
16 |
17 | ```swift
18 | import SwiftUI
19 | import ColorWell
20 |
21 | struct ContentView: View {
22 | @Binding var fontColor: Color
23 |
24 | var body: some View {
25 | VStack {
26 | ColorWellView("Font Color", color: fontColor, action: updateFontColor)
27 | .colorWellStyle(.expanded)
28 |
29 | MyCustomTextEditor(fontColor: $fontColor)
30 | }
31 | }
32 |
33 | private func updateFontColor(_ color: Color) {
34 | fontColor = color
35 | }
36 | }
37 | ```
38 |
39 | ## Cocoa
40 |
41 | Create a ``ColorWell/ColorWell`` using one of the available initializers. Observe color changes using the ``ColorWell/ColorWell/onColorChange(perform:)`` method.
42 |
43 | ```swift
44 | import Cocoa
45 | import ColorWell
46 |
47 | class ContentViewController: NSViewController {
48 | let colorWell: ColorWell
49 | let textEditor: MyCustomNSTextEditor
50 |
51 | init(fontColor: NSColor) {
52 | self.colorWell = ColorWell(color: fontColor)
53 | self.textEditor = MyCustomNSTextEditor(fontColor: fontColor)
54 |
55 | super.init(nibName: "ContentView", bundle: Bundle(for: Self.self))
56 |
57 | // Set the style
58 | colorWell.style = .expanded
59 |
60 | // Add a change handler
61 | colorWell.onColorChange { newColor in
62 | self.textEditor.fontColor = newColor
63 | }
64 | }
65 |
66 | override func viewDidLoad() {
67 | super.viewDidLoad()
68 |
69 | view.addSubview(colorWell)
70 | view.addSubview(textEditor)
71 |
72 | // Layout the views, perform setup work, etc.
73 | }
74 | }
75 | ```
76 |
77 | [light-mode]: color-well-with-popover-light.png
78 | [dark-mode]: color-well-with-popover-dark.png
79 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/color-well-with-popover-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/color-well-with-popover-dark.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/color-well-with-popover-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/color-well-with-popover-light.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/design-comparison-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/design-comparison-dark.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/design-comparison-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/design-comparison-light.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/grid-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/grid-view.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/swatches-style@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/swatches-style@2x.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/Resources/swatches-style~dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jordanbaird/ColorWell/5063ae4afcf6a2c417e7631eec032bca844fa936/Sources/ColorWell/Documentation.docc/Resources/swatches-style~dark@2x.png
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Collections/ColorWellView.Deprecated.md:
--------------------------------------------------------------------------------
1 | # Deprecated Symbols
2 |
3 | Review unsupported symbols and their replacements.
4 |
5 | ## Topics
6 |
7 | ### Initializers
8 |
9 | - ``ColorWell/ColorWellView/init(color:)``
10 | - ``ColorWell/ColorWellView/init(cgColor:)``
11 | - ``ColorWell/ColorWellView/init(color:showsAlpha:)``
12 | - ``ColorWell/ColorWellView/init(cgColor:showsAlpha:)``
13 | - ``ColorWell/ColorWellView/init(color:action:)``
14 | - ``ColorWell/ColorWellView/init(cgColor:action:)``
15 | - ``ColorWell/ColorWellView/init(color:showsAlpha:action:)``
16 | - ``ColorWell/ColorWellView/init(cgColor:showsAlpha:action:)``
17 | - ``ColorWell/ColorWellView/init(color:label:)``
18 | - ``ColorWell/ColorWellView/init(cgColor:label:)``
19 | - ``ColorWell/ColorWellView/init(showsAlpha:color:label:)``
20 | - ``ColorWell/ColorWellView/init(showsAlpha:cgColor:label:)``
21 | - ``ColorWell/ColorWellView/init(label:action:)``
22 | - ``ColorWell/ColorWellView/init(showsAlpha:label:action:)``
23 | - ``ColorWell/ColorWellView/init(color:label:action:)``
24 | - ``ColorWell/ColorWellView/init(cgColor:label:action:)``
25 | - ``ColorWell/ColorWellView/init(showsAlpha:color:label:action:)``
26 | - ``ColorWell/ColorWellView/init(showsAlpha:cgColor:label:action:)``
27 | - ``ColorWell/ColorWellView/init(_:color:)-9vor5``
28 | - ``ColorWell/ColorWellView/init(_:color:)-x43r``
29 | - ``ColorWell/ColorWellView/init(_:cgColor:)-4ptzb``
30 | - ``ColorWell/ColorWellView/init(_:cgColor:)-6sdq1``
31 | - ``ColorWell/ColorWellView/init(_:color:showsAlpha:)-9n2ku``
32 | - ``ColorWell/ColorWellView/init(_:color:showsAlpha:)-8w0wq``
33 | - ``ColorWell/ColorWellView/init(_:cgColor:showsAlpha:)-669gg``
34 | - ``ColorWell/ColorWellView/init(_:cgColor:showsAlpha:)-7u0cq``
35 | - ``ColorWell/ColorWellView/init(_:action:)-k9g0``
36 | - ``ColorWell/ColorWellView/init(_:action:)-9c6rx``
37 | - ``ColorWell/ColorWellView/init(_:showsAlpha:action:)-8jhlo``
38 | - ``ColorWell/ColorWellView/init(_:showsAlpha:action:)-2v1oy``
39 | - ``ColorWell/ColorWellView/init(_:color:action:)-8ghst``
40 | - ``ColorWell/ColorWellView/init(_:color:action:)-3s0o1``
41 | - ``ColorWell/ColorWellView/init(_:cgColor:action:)-62zw9``
42 | - ``ColorWell/ColorWellView/init(_:cgColor:action:)-3tub``
43 | - ``ColorWell/ColorWellView/init(_:color:showsAlpha:action:)-68zal``
44 | - ``ColorWell/ColorWellView/init(_:color:showsAlpha:action:)-60wmk``
45 | - ``ColorWell/ColorWellView/init(_:cgColor:showsAlpha:action:)-8ra6i``
46 | - ``ColorWell/ColorWellView/init(_:cgColor:showsAlpha:action:)-5v4z6``
47 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellStyle/ColorWellStyle.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ColorWellStyle``
2 |
3 | ## Topics
4 |
5 | ### Types of styles
6 |
7 | - ``ExpandedColorWellStyle``
8 | - ``SwatchesColorWellStyle``
9 | - ``StandardColorWellStyle``
10 |
11 | ### Creating a color well style
12 |
13 | - ``ColorWellStyle/expanded``
14 | - ``ColorWellStyle/swatches``
15 | - ``ColorWellStyle/standard``
16 |
17 | ### Deprecated styles
18 |
19 | - ``PanelColorWellStyle``
20 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellStyle/ExpandedColorWellStyle.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ExpandedColorWellStyle``
2 |
3 | ## Topics
4 |
5 | ### Getting the style
6 |
7 | - ``ColorWellStyle/expanded``
8 |
9 | ### Creating an expanded color well style
10 |
11 | - ``init()``
12 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellStyle/PanelColorWellStyle.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/PanelColorWellStyle``
2 |
3 | ## Topics
4 |
5 | ### Getting the style
6 |
7 | - ``ColorWellStyle/colorPanel``
8 |
9 | ### Creating a panel color well style
10 |
11 | - ``init()``
12 |
13 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellStyle/StandardColorWellStyle.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/StandardColorWellStyle``
2 |
3 | ## Topics
4 |
5 | ### Getting the style
6 |
7 | - ``ColorWellStyle/standard``
8 |
9 | ### Creating a standard color well style
10 |
11 | - ``init()``
12 |
13 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellStyle/SwatchesColorWellStyle.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/SwatchesColorWellStyle``
2 |
3 | ## Topics
4 |
5 | ### Getting the style
6 |
7 | - ``ColorWellStyle/swatches``
8 |
9 | ### Creating a swatches color well style
10 |
11 | - ``init()``
12 |
13 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Documentation.docc/SwiftUI/Extensions/ColorWellView.md:
--------------------------------------------------------------------------------
1 | # ``ColorWell/ColorWellView``
2 |
3 | You create a color well view by providing an initial color value and an action to perform when the color changes. The action can be a method, a closure-typed property, or a literal closure. You can provide a label for the color well in the form of a string, a `LocalizedStringKey`, or a custom view.
4 |
5 | ```swift
6 | ColorWellView("Font Color", color: fontColor) { newColor in
7 | fontColor = newColor
8 | }
9 | ```
10 |
11 | By default, color wells support colors with opacity; to disable opacity support, set the `supportsOpacity` parameter to `false`.
12 |
13 | ### Styling color wells
14 |
15 | You can customize a color well's appearance using one of the standard color well styles, like ``ColorWellStyle/swatches``, and apply the style with the ``colorWellStyle(_:)`` modifier:
16 |
17 | ```swift
18 | HStack {
19 | ColorWellView("Foreground", color: .blue)
20 | ColorWellView("Background", color: .blue)
21 | }
22 | .colorWellStyle(.swatches)
23 | ```
24 |
25 | If you apply the style to a container view, as in the example above, all the color wells in the container use the style:
26 |
27 | 
28 |
29 | ## Topics
30 |
31 | ### Creating a color well with an initial color
32 |
33 | - ``init(color:supportsOpacity:)``
34 | - ``init(cgColor:supportsOpacity:)``
35 |
36 | ### Creating a color well with a color and action
37 |
38 | - ``init(color:supportsOpacity:action:)``
39 | - ``init(cgColor:supportsOpacity:action:)``
40 |
41 | ### Creating a color well with a color and label
42 |
43 | - ``init(color:supportsOpacity:label:)``
44 | - ``init(cgColor:supportsOpacity:label:)``
45 | - ``init(_:color:supportsOpacity:)-4e9vl``
46 | - ``init(_:cgColor:supportsOpacity:)-1zr5r``
47 | - ``init(_:color:supportsOpacity:)-2js4x``
48 | - ``init(_:cgColor:supportsOpacity:)-91mdm``
49 |
50 | ### Creating a color well with a label and action
51 |
52 | - ``init(supportsOpacity:label:action:)``
53 | - ``init(_:supportsOpacity:action:)-4ijj0``
54 | - ``init(_:supportsOpacity:action:)-1dho9``
55 |
56 | ### Creating a color well with a color, label, and action
57 |
58 | - ``init(color:supportsOpacity:label:action:)``
59 | - ``init(cgColor:supportsOpacity:label:action:)``
60 | - ``init(_:color:supportsOpacity:action:)-7turx``
61 | - ``init(_:cgColor:supportsOpacity:action:)-78agl``
62 | - ``init(_:color:supportsOpacity:action:)-6lguj``
63 | - ``init(_:cgColor:supportsOpacity:action:)-3f573``
64 |
65 | ### Modifying color wells
66 |
67 | - ``colorWellStyle(_:)``
68 | - ``swatchColors(_:)``
69 | - ``onColorChange(perform:)``
70 |
71 | ### Getting a color well's content view
72 |
73 | - ``body``
74 |
75 | ### Supporting Types
76 |
77 | - ``ColorWellStyle``
78 |
79 | ### Deprecated
80 |
81 | -
82 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/ActionButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActionButton.swift
3 | // ColorWell
4 | //
5 |
6 | import Cocoa
7 |
8 | /// A button that takes a closure for its action.
9 | class ActionButton: NSButton {
10 | private let _action: () -> Void
11 |
12 | init(title: String, action: @escaping () -> Void) {
13 | self._action = action
14 | super.init(frame: .zero)
15 | self.title = title
16 | self.target = self
17 | self.action = #selector(performAction)
18 | }
19 |
20 | @available(*, unavailable)
21 | required init?(coder: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | @objc private func performAction() {
26 | _action()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/Cache.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cache.swift
3 | // ColorWell
4 | //
5 |
6 | // MARK: - CacheContext
7 |
8 | /// A private context used to store the value of a cache.
9 | private class CacheContext {
10 | /// The value stored by the context.
11 | var cachedValue: Value
12 |
13 | /// The identifier associated with the context.
14 | var id: ID
15 |
16 | /// The constructor function associated with the context.
17 | var constructor: Constructor
18 |
19 | /// Creates a context with the given value, identifier, and constructor.
20 | init(cachedValue: Value, id: ID, constructor: Constructor) {
21 | self.cachedValue = cachedValue
22 | self.id = id
23 | self.constructor = constructor
24 | }
25 | }
26 |
27 | // MARK: - Cache
28 |
29 | /// A type that caches a value alongside an equatable identifier
30 | /// that can be used to determine whether the value has changed.
31 | struct Cache {
32 | /// The cache's context.
33 | private let context: Context
34 |
35 | /// The value stored by the cache.
36 | var cachedValue: Value {
37 | context.cachedValue
38 | }
39 |
40 | /// Creates a cache with the given value and identifier.
41 | init(_ cachedValue: Value, id: ID) {
42 | context = Context(cachedValue, id: id)
43 | }
44 |
45 | /// Compares the cache's stored identifier with the specified
46 | /// identifier, and, if the two values are different, updates
47 | /// the cached value using the cache's constructor.
48 | func recache(id: ID) {
49 | guard context.id != id else {
50 | return
51 | }
52 | context.id = id
53 | context.cachedValue = context.constructor(id)
54 | }
55 |
56 | /// Updates the constructor that is stored with this cache.
57 | func updateConstructor(_ constructor: @escaping (ID) -> Value) {
58 | context.constructor = constructor
59 | }
60 | }
61 |
62 | // MARK: Cache.Context
63 | extension Cache {
64 | /// The context for the `Cache` type.
65 | private class Context: CacheContext Value> {
66 | /// Creates a context with the given value and identifier.
67 | init(_ cachedValue: Value, id: ID) {
68 | super.init(
69 | cachedValue: cachedValue,
70 | id: id,
71 | constructor: { _ in cachedValue }
72 | )
73 | }
74 | }
75 | }
76 |
77 | // MARK: - OptionalCache
78 |
79 | /// A type that caches an optional value, and is able to be
80 | /// recached based on whether its value is `nil`.
81 | struct OptionalCache {
82 | /// The cache's context.
83 | private let context: Context
84 |
85 | /// The value stored by the cache.
86 | var cachedValue: Wrapped? {
87 | context.cachedValue
88 | }
89 |
90 | /// Creates a cache with the given value.
91 | init(_ cachedValue: Wrapped? = nil) {
92 | context = Context(cachedValue)
93 | }
94 |
95 | /// Updates the the cached value using the cache's constructor.
96 | func recache() {
97 | if cachedValue == nil {
98 | context.cachedValue = context.constructor()
99 | }
100 | }
101 |
102 | /// Sets the cached value to `nil`.
103 | func clear() {
104 | context.cachedValue = nil
105 | }
106 |
107 | /// Updates the constructor that is stored with this cache.
108 | func updateConstructor(_ constructor: @escaping () -> Wrapped?) {
109 | context.constructor = constructor
110 | }
111 | }
112 |
113 | // MARK: OptionalCache.Context
114 | extension OptionalCache {
115 | /// The context for the `OptionalCache` type.
116 | private class Context: CacheContext Wrapped?> {
117 | /// Creates a context with the given value.
118 | init(_ cachedValue: Wrapped?) {
119 | super.init(
120 | cachedValue: cachedValue,
121 | id: true,
122 | constructor: { cachedValue }
123 | )
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/ColorComponents.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorComponents.swift
3 | // ColorWell
4 | //
5 |
6 | import Cocoa
7 |
8 | /// A type that contains information about the components of a color.
9 | enum ColorComponents {
10 | case rgb(red: Double, green: Double, blue: Double, alpha: Double)
11 | case cmyk(cyan: Double, magenta: Double, yellow: Double, black: Double, alpha: Double)
12 | case grayscale(white: Double, alpha: Double)
13 | case catalog(name: String)
14 | case unknown(color: NSColor)
15 | case deviceN
16 | case indexed
17 | case lab
18 | case pattern
19 |
20 | // MARK: Properties
21 |
22 | /// The name of the color space associated with this instance.
23 | var colorSpaceName: String {
24 | switch self {
25 | case .rgb:
26 | return "rgb"
27 | case .cmyk:
28 | return "cmyk"
29 | case .grayscale:
30 | return "grayscale"
31 | case .catalog:
32 | return "catalog color"
33 | case .unknown:
34 | return "unknown color space"
35 | case .deviceN:
36 | return "deviceN"
37 | case .indexed:
38 | return "indexed"
39 | case .lab:
40 | return "L*a*b*"
41 | case .pattern:
42 | return "pattern image"
43 | }
44 | }
45 |
46 | /// The raw components extracted from this instance.
47 | var extractedComponents: [Any] {
48 | switch self {
49 | case .rgb(let red, let green, let blue, let alpha):
50 | return [red, green, blue, alpha]
51 | case .cmyk(let cyan, let magenta, let yellow, let black, let alpha):
52 | return [cyan, magenta, yellow, black, alpha]
53 | case .grayscale(let white, let alpha):
54 | return [white, alpha]
55 | case .catalog(let name):
56 | return [name]
57 | case .unknown(let color):
58 | guard color.type == .componentBased else {
59 | return [String(describing: color)]
60 | }
61 |
62 | var components = [CGFloat](repeating: 0, count: color.numberOfComponents)
63 | color.getComponents(&components)
64 |
65 | return components.map { component in
66 | Double(component)
67 | }
68 | default:
69 | return []
70 | }
71 | }
72 |
73 | /// String representations of the components extracted from this instance.
74 | var extractedComponentStrings: [String] {
75 | let formatter = NumberFormatter()
76 | formatter.minimumIntegerDigits = 1
77 | formatter.minimumFractionDigits = 0
78 | formatter.maximumFractionDigits = 6
79 |
80 | return extractedComponents.compactMap { component in
81 | if let component = component as? Double {
82 | return formatter.string(for: component)
83 | }
84 | return String(describing: component)
85 | }
86 | }
87 |
88 | // MARK: Initializers
89 |
90 | /// Creates an instance from the specified color.
91 | init(color: NSColor) {
92 | switch color.type {
93 | case .componentBased:
94 | switch color.colorSpace.colorSpaceModel {
95 | case .rgb:
96 | self = .rgb(
97 | red: color.redComponent,
98 | green: color.greenComponent,
99 | blue: color.blueComponent,
100 | alpha: color.alphaComponent
101 | )
102 | case .cmyk:
103 | self = .cmyk(
104 | cyan: color.cyanComponent,
105 | magenta: color.magentaComponent,
106 | yellow: color.yellowComponent,
107 | black: color.blackComponent,
108 | alpha: color.alphaComponent
109 | )
110 | case .gray:
111 | self = .grayscale(
112 | white: color.whiteComponent,
113 | alpha: color.alphaComponent
114 | )
115 | case .deviceN:
116 | self = .deviceN
117 | case .indexed:
118 | self = .indexed
119 | case .lab:
120 | self = .lab
121 | case .patterned:
122 | self = .pattern
123 | case .unknown:
124 | self = .unknown(color: color)
125 | @unknown default:
126 | self = .unknown(color: color)
127 | }
128 | case .pattern:
129 | self = .pattern
130 | case .catalog:
131 | self = .catalog(name: color.localizedColorNameComponent)
132 | @unknown default:
133 | self = .unknown(color: color)
134 | }
135 | }
136 | }
137 |
138 | // MARK: ColorComponents: CustomStringConvertible
139 | extension ColorComponents: CustomStringConvertible {
140 | var description: String {
141 | (CollectionOfOne(colorSpaceName) + extractedComponentStrings).joined(separator: " ")
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/ConstructablePath.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConstructablePath.swift
3 | // ColorWell
4 | //
5 |
6 | import Cocoa
7 |
8 | // MARK: - Corner
9 |
10 | /// A type that represents a corner of a rectangle.
11 | enum Corner {
12 | /// The top left corner of a rectangle.
13 | case topLeft
14 |
15 | /// The top right corner of a rectangle.
16 | case topRight
17 |
18 | /// The bottom left corner of a rectangle.
19 | case bottomLeft
20 |
21 | /// The bottom right corner of a rectangle.
22 | case bottomRight
23 |
24 | /// All corners, ordered for use in color well path construction, starting
25 | /// at the top left and moving clockwise around the color well's border.
26 | static let clockwiseOrder: [Self] = [.topLeft, .topRight, .bottomRight, .bottomLeft]
27 |
28 | /// Returns the point in the given rectangle that corresponds to this corner.
29 | func point(forRect rect: CGRect) -> CGPoint {
30 | switch self {
31 | case .topLeft:
32 | return CGPoint(x: rect.minX, y: rect.maxY)
33 | case .topRight:
34 | return CGPoint(x: rect.maxX, y: rect.maxY)
35 | case .bottomLeft:
36 | return CGPoint(x: rect.minX, y: rect.minY)
37 | case .bottomRight:
38 | return CGPoint(x: rect.maxX, y: rect.minY)
39 | }
40 | }
41 | }
42 |
43 | // MARK: - Side
44 |
45 | /// A type that represents a side of a rectangle.
46 | enum Side {
47 | /// The top side of a rectangle.
48 | case top
49 |
50 | /// The bottom side of a rectangle.
51 | case bottom
52 |
53 | /// The left side of a rectangle.
54 | case left
55 |
56 | /// The right side of a rectangle.
57 | case right
58 |
59 | /// A side that contains no points.
60 | case null
61 |
62 | /// The corners that, when connected by a path, make up this side.
63 | var corners: [Corner] {
64 | switch self {
65 | case .top:
66 | return [.topLeft, .topRight]
67 | case .bottom:
68 | return [.bottomLeft, .bottomRight]
69 | case .left:
70 | return [.topLeft, .bottomLeft]
71 | case .right:
72 | return [.topRight, .bottomRight]
73 | case .null:
74 | return []
75 | }
76 | }
77 |
78 | /// The side on the opposite end of the rectangle.
79 | var opposite: Self {
80 | switch self {
81 | case .top:
82 | return .bottom
83 | case .bottom:
84 | return .top
85 | case .left:
86 | return .right
87 | case .right:
88 | return .left
89 | case .null:
90 | return .null
91 | }
92 | }
93 | }
94 |
95 | // MARK: - ConstructablePathComponent
96 |
97 | /// A type that represents a component in a constructable path.
98 | enum ConstructablePathComponent {
99 | /// Closes the path.
100 | case close
101 |
102 | /// Moves the path to the given point.
103 | case move(to: CGPoint)
104 |
105 | /// Draws a line in the path from its current point to the given point.
106 | case line(to: CGPoint)
107 |
108 | /// Draws a curved line in the path from its current point to the given
109 | /// point, using the provided control points to determine the curve's shape.
110 | case curve(to: CGPoint, control1: CGPoint, control2: CGPoint)
111 |
112 | /// Draws an arc in the path from its current point, through the given
113 | /// midpoint, to the given endpoint, curving the path according to the
114 | /// specified radius.
115 | case arc(through: CGPoint, to: CGPoint, radius: CGFloat)
116 |
117 | /// A component that nests other components.
118 | ///
119 | /// This case can also be created using array literal syntax.
120 | /// In the following example, `c1` and `c2` are equivalent.
121 | ///
122 | /// ```swift
123 | /// let c1 = ConstructablePathComponent.compound([
124 | /// .move(to: point1),
125 | /// .line(to: point2),
126 | /// ])
127 | ///
128 | /// let c2: ConstructablePathComponent = [
129 | /// .move(to: point1),
130 | /// .line(to: point2),
131 | /// ]
132 | ///
133 | /// print(c1 == c2) // Prints: true
134 | /// ```
135 | indirect case compound([Self])
136 |
137 | /// Returns a compound component that constructs a right angle curve around
138 | /// the given corner of the provided rectangle, using the specified radius.
139 | static func rightAngleCurve(around corner: Corner, ofRect rect: CGRect, radius: CGFloat) -> Self {
140 | let mid = corner.point(forRect: rect)
141 |
142 | let start: CGPoint
143 | let end: CGPoint
144 |
145 | switch corner {
146 | case .topLeft:
147 | start = mid.translating(y: -radius)
148 | end = mid.translating(x: radius)
149 | case .topRight:
150 | start = mid.translating(x: -radius)
151 | end = mid.translating(y: -radius)
152 | case .bottomRight:
153 | start = mid.translating(y: radius)
154 | end = mid.translating(x: -radius)
155 | case .bottomLeft:
156 | start = mid.translating(x: radius)
157 | end = mid.translating(y: radius)
158 | }
159 |
160 | return [
161 | .line(to: start),
162 | .arc(through: mid, to: end, radius: radius),
163 | ]
164 | }
165 | }
166 |
167 | // MARK: ConstructablePathComponent: Equatable
168 | extension ConstructablePathComponent: Equatable { }
169 |
170 | // MARK: ConstructablePathComponent: ExpressibleByArrayLiteral
171 | extension ConstructablePathComponent: ExpressibleByArrayLiteral {
172 | init(arrayLiteral elements: Self...) {
173 | self = .compound(elements)
174 | }
175 | }
176 |
177 | // MARK: - ConstructablePath
178 |
179 | /// A type that can produce a version of itself that can be constructed
180 | /// from `ConstructablePathComponent` values.
181 | protocol ConstructablePath where MutablePath.Constructed == Constructed {
182 | /// The constructed result type of this path type.
183 | associatedtype Constructed: ConstructablePath
184 |
185 | /// A mutable version of this path type that produces the same
186 | /// constructed result.
187 | associatedtype MutablePath: MutableConstructablePath
188 |
189 | /// This path, as its constructed result type.
190 | var asConstructedType: Constructed { get }
191 |
192 | /// Constructs a path from the given components.
193 | ///
194 | /// - Parameter components: The components to use to construct the path.
195 | /// - Returns: A `Constructed`-typed path, constructed using `components`.
196 | static func construct(with components: [ConstructablePathComponent]) -> Constructed
197 | }
198 |
199 | // MARK: ConstructablePath where Constructed == Self
200 | extension ConstructablePath where Constructed == Self {
201 | var asConstructedType: Constructed { self }
202 | }
203 |
204 | // MARK: ConstructablePath Static Methods
205 | extension ConstructablePath {
206 | // Documented in protocol definition.
207 | static func construct(with components: [ConstructablePathComponent]) -> Constructed {
208 | let path = MutablePath()
209 | for component in components {
210 | path.apply(component)
211 | }
212 | return path.asConstructedType
213 | }
214 |
215 | /// Produces a path for a part of a color well.
216 | ///
217 | /// - Parameters:
218 | /// - rect: The rectangle to draw the path in.
219 | /// - corners: The corners that should be drawn with sharp right angles.
220 | /// Corners not provided here will be rounded.
221 | static func colorWellPath(rect: CGRect, squaredCorners corners: [Corner]) -> Constructed {
222 | var components: [ConstructablePathComponent] = Corner.clockwiseOrder.map { corner in
223 | if corners.contains(corner) {
224 | return .line(to: corner.point(forRect: rect))
225 | }
226 | return .rightAngleCurve(around: corner, ofRect: rect, radius: 5)
227 | }
228 | components.append(.close)
229 | return construct(with: components)
230 | }
231 |
232 | /// Produces a partial path for the specified side of a color well.
233 | ///
234 | /// - Parameters:
235 | /// - rect: The rectangle to draw the path in.
236 | /// - side: The side of `rect` that the path should be drawn in. This
237 | /// parameter provides information about which corners should be rounded
238 | /// and which corners should be drawn with sharp right angles.
239 | static func partialColorWellPath(rect: CGRect, side: Side) -> Constructed {
240 | colorWellPath(rect: rect, squaredCorners: side.opposite.corners)
241 | }
242 |
243 | /// Produces a full path for a color well, that is, a path with
244 | /// none of its corners squared.
245 | ///
246 | /// - Parameter rect: The rectangle to draw the path in.
247 | static func fullColorWellPath(rect: CGRect) -> Constructed {
248 | colorWellPath(rect: rect, squaredCorners: [])
249 | }
250 | }
251 |
252 | // MARK: - MutableConstructablePath
253 |
254 | /// A constructable path type whose instances can be mutated
255 | /// after their creation.
256 | protocol MutableConstructablePath: ConstructablePath {
257 | /// Creates an empty path.
258 | init()
259 |
260 | /// Applies the given path component to this path.
261 | func apply(_ component: ConstructablePathComponent)
262 | }
263 |
264 | // MARK: NSBezierPath: MutableConstructablePath
265 | extension NSBezierPath: MutableConstructablePath {
266 | typealias MutablePath = NSBezierPath
267 |
268 | func apply(_ component: ConstructablePathComponent) {
269 | switch component {
270 | case .close:
271 | close()
272 | case .move(let point):
273 | move(to: point)
274 | case .line(let point):
275 | if isEmpty {
276 | move(to: point)
277 | } else {
278 | line(to: point)
279 | }
280 | case .curve(let point, let control1, let control2):
281 | if isEmpty {
282 | move(to: point)
283 | } else {
284 | curve(to: point, controlPoint1: control1, controlPoint2: control2)
285 | }
286 | case .arc(let midPoint, let endPoint, let radius):
287 | if isEmpty {
288 | move(to: endPoint)
289 | } else {
290 | appendArc(from: midPoint, to: endPoint, radius: radius)
291 | }
292 | case .compound(let components):
293 | for component in components {
294 | apply(component)
295 | }
296 | }
297 | }
298 | }
299 |
300 | // MARK: CGMutablePath: MutableConstructablePath
301 | extension CGMutablePath: MutableConstructablePath {
302 | func apply(_ component: ConstructablePathComponent) {
303 | switch component {
304 | case .close:
305 | closeSubpath()
306 | case .move(let point):
307 | move(to: point)
308 | case .line(let point):
309 | if isEmpty {
310 | move(to: point)
311 | } else {
312 | addLine(to: point)
313 | }
314 | case .curve(let point, let control1, let control2):
315 | if isEmpty {
316 | move(to: point)
317 | } else {
318 | addCurve(to: point, control1: control1, control2: control2)
319 | }
320 | case .arc(let midPoint, let endPoint, let radius):
321 | if isEmpty {
322 | move(to: endPoint)
323 | } else {
324 | addArc(tangent1End: midPoint, tangent2End: endPoint, radius: radius)
325 | }
326 | case .compound(let components):
327 | for component in components {
328 | apply(component)
329 | }
330 | }
331 | }
332 | }
333 |
334 | // MARK: CGPath: ConstructablePath
335 | extension CGPath: ConstructablePath {
336 | typealias MutablePath = CGMutablePath
337 | }
338 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/DrawingStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DrawingStyle.swift
3 | // ColorWell
4 | //
5 |
6 | import Cocoa
7 |
8 | /// Constants that represent the drawing styles used by the
9 | /// system to render colors and assets.
10 | ///
11 | /// These values serve mainly to indicate whether a value of
12 | /// the more complex `NSAppearance` type represents a light
13 | /// or dark appearance. The drawing style that corresponds to
14 | /// the currently active appearance can be retrieved through
15 | /// the `DrawingStyle.current` static property.
16 | enum DrawingStyle {
17 | /// A drawing style that indicates a dark appearance.
18 | case dark
19 |
20 | /// A drawing style that indicates a light appearance.
21 | case light
22 |
23 | // MARK: Static Properties
24 |
25 | /// The drawing style of the current appearance.
26 | static var current: Self {
27 | let currentAppearance: NSAppearance = {
28 | guard #available(macOS 11.0, *) else {
29 | return .current
30 | }
31 | return .currentDrawing()
32 | }()
33 | return currentAppearance.drawingStyle
34 | }
35 |
36 | // MARK: Initializers
37 |
38 | /// Creates a drawing style that corresponds to the specified appearance.
39 | init(appearance: NSAppearance) {
40 | enum LocalCache {
41 | static let systemDarkNames: Set = {
42 | if #available(macOS 10.14, *) {
43 | return [
44 | .darkAqua,
45 | .vibrantDark,
46 | .accessibilityHighContrastDarkAqua,
47 | .accessibilityHighContrastVibrantDark,
48 | ]
49 | }
50 | return [.vibrantDark]
51 | }()
52 | }
53 |
54 | switch appearance.name {
55 | case let name where (
56 | LocalCache.systemDarkNames.contains(name) ||
57 | name.rawValue.lowercased().contains("dark")
58 | ):
59 | self = .dark
60 | default:
61 | self = .light
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/ColorWell/Utilities/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // ColorWell
4 | //
5 |
6 | import Cocoa
7 |
8 | // MARK: - CGPoint
9 |
10 | extension CGPoint {
11 | /// Returns a new point resulting from a translation of the current
12 | /// point by the given x and y amounts.
13 | func translating(x: CGFloat = 0, y: CGFloat = 0) -> Self {
14 | applying(CGAffineTransform(translationX: x, y: y))
15 | }
16 | }
17 |
18 | // MARK: - CGRect
19 |
20 | extension CGRect {
21 | /// Returns a rectangle that is the result of centering the current
22 | /// rectangle within the bounds of another rectangle.
23 | func centered(in otherRect: Self) -> Self {
24 | var new = self
25 | new.origin.x = otherRect.midX - (new.width / 2)
26 | new.origin.y = otherRect.midY - (new.height / 2)
27 | return new
28 | }
29 | }
30 |
31 | // MARK: - CGSize
32 |
33 | extension CGSize {
34 | /// Returns the size that is the result of subtracting the specified
35 | /// edge insets from the current size.
36 | func applying(insets: NSEdgeInsets) -> Self {
37 | Self(width: width - insets.horizontal, height: height - insets.vertical)
38 | }
39 |
40 | /// Returns a new size by applying the given insets to this size.
41 | ///
42 | /// The returned size is calculated according to the following
43 | /// expression, where `w` and `h` represent the width and height
44 | /// of the original size:
45 | ///
46 | /// (w - (dx * 2), h - (dy * 2))
47 | ///
48 | /// If `dx` and `dy` are positive values, the size is decreased.
49 | /// If they are negative values, the size is increased.
50 | func insetBy(dx: CGFloat, dy: CGFloat) -> Self {
51 | Self(width: width - (dx * 2), height: height - (dy * 2))
52 | }
53 | }
54 |
55 | // MARK: - Comparable
56 |
57 | extension Comparable {
58 | /// Returns this comparable value, clamped to the given limiting range.
59 | func clamped(to limits: ClosedRange) -> Self {
60 | min(max(self, limits.lowerBound), limits.upperBound)
61 | }
62 | }
63 |
64 | // MARK: - NSAppearance
65 |
66 | extension NSAppearance {
67 | /// The drawing style that corresponds to this appearance.
68 | var drawingStyle: DrawingStyle {
69 | DrawingStyle(appearance: self)
70 | }
71 | }
72 |
73 | // MARK: - NSColor
74 |
75 | extension NSColor {
76 | /// The default fill color for a color well segment.
77 | static var colorWellSegmentColor: NSColor {
78 | switch DrawingStyle.current {
79 | case .dark:
80 | return .selectedControlColor
81 | case .light:
82 | return .controlColor
83 | }
84 | }
85 |
86 | /// The fill color for a highlighted color well segment.
87 | static var highlightedColorWellSegmentColor: NSColor {
88 | switch DrawingStyle.current {
89 | case .dark:
90 | return colorWellSegmentColor.blendedAndClamped(withFraction: 0.2, of: .highlightColor)
91 | case .light:
92 | return colorWellSegmentColor.blendedAndClamped(withFraction: 0.5, of: .selectedControlColor)
93 | }
94 | }
95 |
96 | /// The fill color for a selected color well segment.
97 | static var selectedColorWellSegmentColor: NSColor {
98 | switch DrawingStyle.current {
99 | case .dark:
100 | return colorWellSegmentColor.withAlphaComponent(colorWellSegmentColor.alphaComponent + 0.25)
101 | case .light:
102 | return .selectedControlColor
103 | }
104 | }
105 |
106 | /// Returns the average of this color's red, green, and blue components,
107 | /// approximating the brightness of the color.
108 | var averageBrightness: CGFloat {
109 | guard let sRGB = usingColorSpace(.sRGB) else {
110 | return 0
111 | }
112 | return (sRGB.redComponent + sRGB.greenComponent + sRGB.blueComponent) / 3
113 | }
114 |
115 | /// Creates a color from a hexadecimal string.
116 | convenience init?(hexString: String) {
117 | let hexString = hexString.trimmingCharacters(in: ["#"]).lowercased()
118 | let count = hexString.count
119 |
120 | guard
121 | count >= 6,
122 | count.isMultiple(of: 2)
123 | else {
124 | return nil
125 | }
126 |
127 | let hexArray = hexString.map { String($0) }
128 |
129 | let rString = hexArray[0..<2].joined()
130 | let gString = hexArray[2..<4].joined()
131 | let bString = hexArray[4..<6].joined()
132 | let aString = count == 6 ? "ff" : hexArray[6..<8].joined()
133 |
134 | guard
135 | let rInt = Int(rString, radix: 16),
136 | let gInt = Int(gString, radix: 16),
137 | let bInt = Int(bString, radix: 16),
138 | let aInt = Int(aString, radix: 16)
139 | else {
140 | return nil
141 | }
142 |
143 | let rFloat = CGFloat(rInt) / 255
144 | let gFloat = CGFloat(gInt) / 255
145 | let bFloat = CGFloat(bInt) / 255
146 | let aFloat = CGFloat(aInt) / 255
147 |
148 | self.init(srgbRed: rFloat, green: gFloat, blue: bFloat, alpha: aFloat)
149 | }
150 |
151 | /// Creates a new color object whose component values are a weighted sum
152 | /// of the current and specified color objects.
153 | ///
154 | /// This method converts both colors to RGB before blending. If either
155 | /// color is unable to be converted, this method returns the current color
156 | /// unaltered.
157 | ///
158 | /// - Parameters:
159 | /// - fraction: The amount of `color` to blend with the current color.
160 | /// - color: The color to blend with the current color.
161 | ///
162 | /// - Returns: The blended color, if successful. If either color is unable
163 | /// to be converted, or if `fraction > 0`, the current color is returned
164 | /// unaltered. If `fraction < 1`, `color` is returned unaltered.
165 | func blendedAndClamped(withFraction fraction: CGFloat, of color: NSColor) -> NSColor {
166 | guard fraction > 0 else {
167 | return self
168 | }
169 |
170 | guard fraction < 1 else {
171 | return color
172 | }
173 |
174 | guard
175 | let color1 = usingColorSpace(.genericRGB),
176 | let color2 = color.usingColorSpace(.genericRGB)
177 | else {
178 | return self
179 | }
180 |
181 | var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
182 | var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
183 |
184 | color1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
185 | color2.getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
186 |
187 | let inverseFraction = 1 - fraction
188 |
189 | let r = (r2 * fraction) + (r1 * inverseFraction)
190 | let g = (g2 * fraction) + (g1 * inverseFraction)
191 | let b = (b2 * fraction) + (b1 * inverseFraction)
192 | let a = (a2 * fraction) + (a1 * inverseFraction)
193 |
194 | return NSColor(
195 | calibratedRed: r.clamped(to: 0...1),
196 | green: g.clamped(to: 0...1),
197 | blue: b.clamped(to: 0...1),
198 | alpha: a.clamped(to: 0...1)
199 | )
200 | }
201 |
202 | /// Returns a Boolean value that indicates whether this color resembles another
203 | /// color, checking in the given color space with the given tolerance.
204 | ///
205 | /// - Note: If one or both colors cannot be converted to `colorSpace`, this method
206 | /// returns `false`.
207 | ///
208 | /// - Parameters:
209 | /// - other: A color to compare this color to.
210 | /// - colorSpace: A color space to convert both colors to before running the check.
211 | /// - tolerance: A threshold value that alters how strict the comparison is.
212 | ///
213 | /// - Returns: `true` if this color is "close enough" to `other`. False otherwise.
214 | func resembles(_ other: NSColor, using colorSpace: NSColorSpace, tolerance: CGFloat) -> Bool {
215 | guard
216 | let first = usingColorSpace(colorSpace),
217 | let second = other.usingColorSpace(colorSpace)
218 | else {
219 | return false
220 | }
221 |
222 | if first == second {
223 | return true
224 | }
225 |
226 | guard first.numberOfComponents == second.numberOfComponents else {
227 | return false
228 | }
229 |
230 | // Initialize `components1` to repeat 1 instead of 0. Otherwise, we
231 | // might end up with a false positive `true` result, if copying the
232 | // components fails.
233 | var components1 = [CGFloat](repeating: 1, count: first.numberOfComponents)
234 | var components2 = [CGFloat](repeating: 0, count: second.numberOfComponents)
235 |
236 | first.getComponents(&components1)
237 | second.getComponents(&components2)
238 |
239 | return (0.. Bool {
255 | if self == other {
256 | return true
257 | }
258 |
259 | let colorSpaces: [NSColorSpace] = [
260 | // Standard
261 | .sRGB,
262 | .extendedSRGB,
263 | .adobeRGB1998,
264 | .displayP3,
265 |
266 | // Generic
267 | .genericRGB,
268 | .genericCMYK,
269 |
270 | // Device
271 | .deviceRGB,
272 | .deviceCMYK,
273 | ]
274 |
275 | return colorSpaces.contains { colorSpace in
276 | resembles(other, using: colorSpace, tolerance: tolerance)
277 | }
278 | }
279 |
280 | /// Creates a value containing a description of the color
281 | /// for use with voice-over and other accessibility features.
282 | func createAccessibilityValue() -> String {
283 | String(describing: ColorComponents(color: self))
284 | }
285 |
286 | /// Creates a copy of this color by passing it through an archiving
287 | /// and unarchiving process, returning what is effectively the same
288 | /// color, but cleared of any undesired context.
289 | func createArchivedCopy() -> NSColor {
290 | let colorData: Data = {
291 | // Don't require secure coding. This is the whole reason we even
292 | // need this function. Apparently, some NSColor-backed SwiftUI
293 | // colors don't fully support secure coding (bug on Apple's part).
294 | //
295 | // The NSColor that SwiftUI uses is actually a custom NSColor
296 | // subclass, so there's not much we can do about it. The solution
297 | // is to archive it where we know secure coding isn't needed, then
298 | // create a "true" NSColor from the archived data. We could have
299 | // gone the route of converting the color to RGB, but this method
300 | // has the added benefit of preserving the original information
301 | // of the color.
302 | let archiver = NSKeyedArchiver(requiringSecureCoding: false)
303 |
304 | encode(with: archiver)
305 | return archiver.encodedData
306 | }()
307 |
308 | guard
309 | let unarchiver = try? NSKeyedUnarchiver(forReadingFrom: colorData),
310 | let copy = NSColor(coder: unarchiver)
311 | else {
312 | // Fall back to the original color if copying fails.
313 | return self
314 | }
315 |
316 | return copy
317 | }
318 | }
319 |
320 | // MARK: - NSColorPanel
321 |
322 | extension NSColorPanel {
323 | /// Storage for the system color panel's attached color wells.
324 | private static let storage = Storage<[ColorWell]>()
325 |
326 | /// The color wells that are currently attached to the color panel.
327 | var attachedColorWells: [ColorWell] {
328 | get {
329 | Self.storage.value(forObject: self) ?? []
330 | }
331 | set {
332 | let oldMain = mainColorWell
333 | defer {
334 | let newMain = mainColorWell
335 | if oldMain != newMain {
336 | newMain?.synchronizeColorPanel()
337 | }
338 | }
339 |
340 | let oldValue = attachedColorWells
341 | defer {
342 | let difference = Set(newValue).symmetricDifference(Set(oldValue))
343 | for colorWell in difference {
344 | colorWell.updateActiveState()
345 | }
346 | }
347 |
348 | if newValue.isEmpty {
349 | Self.storage.removeValue(forObject: self)
350 | } else {
351 | Self.storage.set(newValue, forObject: self)
352 | }
353 | }
354 | }
355 |
356 | /// The main color well currently attached to the color panel.
357 | ///
358 | /// The first color well to become active during a multiple-selection
359 | /// session becomes the main color well, and remains so until it is
360 | /// deactivated. If no color wells are currently active, this property
361 | /// returns `nil`.
362 | var mainColorWell: ColorWell? {
363 | guard
364 | let first = attachedColorWells.first,
365 | first.isActive
366 | else {
367 | return nil
368 | }
369 | return first
370 | }
371 | }
372 |
373 | // MARK: - NSEdgeInsets
374 |
375 | extension NSEdgeInsets {
376 | /// The combined left and right insets of this instance.
377 | var horizontal: Double {
378 | left + right
379 | }
380 |
381 | /// The combined top and bottom insets of this instance.
382 | var vertical: Double {
383 | top + bottom
384 | }
385 | }
386 |
387 | // MARK: - NSGraphicsContext
388 |
389 | extension NSGraphicsContext {
390 | /// Executes a block of code on the current graphics context, restoring
391 | /// the graphics state after the block returns.
392 | static func withCachedGraphicsState(_ body: (NSGraphicsContext?) throws -> T) rethrows -> T {
393 | let current = current
394 | current?.saveGraphicsState()
395 | defer {
396 | current?.restoreGraphicsState()
397 | }
398 | return try body(current)
399 | }
400 |
401 | /// Executes a block of code on the current graphics context, restoring
402 | /// the graphics state after the block returns.
403 | static func withCachedGraphicsState(_ body: () throws -> T) rethrows -> T {
404 | try withCachedGraphicsState { _ in try body() }
405 | }
406 | }
407 |
408 | // MARK: - NSImage
409 |
410 | extension NSImage {
411 | /// Creates an image by drawing a swatch in the given color and size.
412 | convenience init(color: NSColor, size: NSSize, radius: CGFloat = 0) {
413 | self.init(size: size, flipped: false) { bounds in
414 | NSBezierPath(roundedRect: bounds, xRadius: radius, yRadius: radius).addClip()
415 | color.drawSwatch(in: bounds)
416 | return true
417 | }
418 | }
419 |
420 | /// Draws the specified color in the given rectangle, with the given
421 | /// clipping path.
422 | ///
423 | /// This method differs from the `drawSwatch(in:)` method on `NSColor` in
424 | /// that it allows you to set a clipping path without affecting the border
425 | /// of the swatch.
426 | ///
427 | /// The swatch that is drawn using the `NSColor` method is drawn with a
428 | /// thin border around its edges, which is affected by the current clipping
429 | /// path. This can yield undesirable results if we want to, for example,
430 | /// set our own border with a slightly different appearance (which we do).
431 | ///
432 | /// As a workaround, this method uses `NSColor`'s `drawSwatch(in:)` method
433 | /// to draw an image, then clips the image instead of the swatch path.
434 | static func drawSwatch(with color: NSColor, in rect: NSRect, clippingTo clippingPath: NSBezierPath? = nil) {
435 | NSGraphicsContext.withCachedGraphicsState {
436 | clippingPath?.addClip()
437 | NSImage(color: color, size: rect.size).draw(in: rect)
438 | }
439 | }
440 |
441 | /// Returns a new image created by clipping the current image to
442 | /// the given rectangle.
443 | func clipped(to rect: NSRect) -> NSImage {
444 | NSImage(size: rect.size, flipped: false) { bounds in
445 | NSGraphicsContext.withCachedGraphicsState {
446 | let destFrame = NSRect(origin: .zero, size: bounds.size)
447 | destFrame.clip()
448 | self.draw(in: destFrame, from: rect, operation: .copy, fraction: 1)
449 | return true
450 | }
451 | }
452 | }
453 |
454 | /// Returns a new image by clipping the current image so that its
455 | /// longest side is equal in length to its shortest side.
456 | func clippedToSquare() -> NSImage {
457 | let originalFrame = NSRect(origin: .zero, size: size)
458 | let insetDimension = min(size.width, size.height)
459 |
460 | let insetFrame = NSRect(
461 | origin: .zero,
462 | size: NSSize(width: insetDimension, height: insetDimension)
463 | ).centered(in: originalFrame)
464 |
465 | return clipped(to: insetFrame)
466 | }
467 |
468 | /// Returns a new image that has been tinted to the given color.
469 | func tinted(to color: NSColor, amount: CGFloat) -> NSImage {
470 | guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else {
471 | return self
472 | }
473 | let tintImage = NSImage(size: size, flipped: false) { bounds in
474 | NSGraphicsContext.withCachedGraphicsState { context in
475 | guard let cgContext = context?.cgContext else {
476 | return false
477 | }
478 | color.setFill()
479 | cgContext.clip(to: bounds, mask: cgImage)
480 | cgContext.fill(bounds)
481 | return true
482 | }
483 | }
484 | return NSImage(size: size, flipped: false) { bounds in
485 | self.draw(in: bounds)
486 | tintImage.draw(
487 | in: bounds,
488 | from: .zero,
489 | operation: .sourceAtop,
490 | fraction: amount
491 | )
492 | return true
493 | }
494 | }
495 | }
496 |
497 | // MARK: - NSView
498 |
499 | extension NSView {
500 | /// Returns this view's frame, converted to the coordinate system
501 | /// of its window.
502 | var frameConvertedToWindow: NSRect {
503 | superview?.convert(frame, to: nil) ?? frame
504 | }
505 | }
506 |
507 | // MARK: - RangeReplaceableCollection
508 |
509 | extension RangeReplaceableCollection {
510 | /// Creates a new instance of a collection containing the non-`nil`
511 | /// elements of a sequence.
512 | ///
513 | /// - Parameter elements: The sequence of optional elements for the
514 | /// new collection. `elements` must be finite.
515 | init(compacting elements: S) where S.Element == Element? {
516 | self.init(elements.compactMap { $0 })
517 | }
518 | }
519 |
520 | // MARK: - Set (Element == NSKeyValueObservation)
521 |
522 | extension Set where Element == NSKeyValueObservation {
523 | /// Creates an observation for the given object, keypath, options, and
524 | /// change handler, and stores it in the set.
525 | ///
526 | /// - Parameters:
527 | /// - object: The object to observe.
528 | /// - keyPath: A keypath to the observed property.
529 | /// - options: The options describing the behavior of the observation.
530 | /// - changeHandler: A change handler that will be performed when the
531 | /// observed value changes.
532 | mutating func insertObservation(
533 | for object: Object,
534 | keyPath: KeyPath