Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Pika.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
CC2000000000000000000701 /* WCAGComplianceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000700 /* WCAGComplianceTests.swift */; };
CC2000000000000000000801 /* APCAComplianceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000800 /* APCAComplianceTests.swift */; };
CC2000000000000000000901 /* NotificationNamesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000900 /* NotificationNamesTests.swift */; };
CC2000000000000000000A01 /* CGFloatFormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2000000000000000000A00 /* CGFloatFormatTests.swift */; };

220D5E9428DB154300B6285E /* AppModeButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9328DB154300B6285E /* AppModeButtons.swift */; };
220D5E9828DB158400B6285E /* AppModeToggleGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */; };
Expand All @@ -50,6 +51,8 @@
CC000001000000000000AAF2 /* NSColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AAF0 /* NSColor+Hex.swift */; };
CC000001000000000000AB01 /* NSColor+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB00 /* NSColor+RGB.swift */; };
CC000001000000000000AB02 /* NSColor+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB00 /* NSColor+RGB.swift */; };
CC000001000000000000AB11 /* CGFloat+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB10 /* CGFloat+Format.swift */; };
CC000001000000000000AB12 /* CGFloat+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000AB10 /* CGFloat+Format.swift */; };
EA0C525025AA729300AFF716 /* Visualisation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C524F25AA729300AFF716 /* Visualisation.swift */; };
EA0C526025AB5A2B00AFF716 /* NavigationMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */; };
EA0C526425AB5D1700AFF716 /* PikaWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0C526325AB5D1700AFF716 /* PikaWindow.swift */; };
Expand Down Expand Up @@ -194,6 +197,7 @@
CC2000000000000000000700 /* WCAGComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCAGComplianceTests.swift; sourceTree = "<group>"; };
CC2000000000000000000800 /* APCAComplianceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APCAComplianceTests.swift; sourceTree = "<group>"; };
CC2000000000000000000900 /* NotificationNamesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationNamesTests.swift; sourceTree = "<group>"; };
CC2000000000000000000A00 /* CGFloatFormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGFloatFormatTests.swift; sourceTree = "<group>"; };

220D5E9328DB154300B6285E /* AppModeButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeButtons.swift; sourceTree = "<group>"; };
220D5E9728DB158400B6285E /* AppModeToggleGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeToggleGroup.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -223,6 +227,7 @@
CC000001000000000000AAE0 /* NSColor+Luminance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Luminance.swift"; sourceTree = "<group>"; };
CC000001000000000000AAF0 /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = "<group>"; };
CC000001000000000000AB00 /* NSColor+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+RGB.swift"; sourceTree = "<group>"; };
CC000001000000000000AB10 /* CGFloat+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Format.swift"; sourceTree = "<group>"; };
EA0C524F25AA729300AFF716 /* Visualisation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visualisation.swift; sourceTree = "<group>"; };
EA0C525F25AB5A2B00AFF716 /* NavigationMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationMenuItems.swift; sourceTree = "<group>"; };
EA0C526325AB5D1700AFF716 /* PikaWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PikaWindow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -422,6 +427,7 @@
CC000001000000000000AAE0 /* NSColor+Luminance.swift */,
CC000001000000000000AAF0 /* NSColor+Hex.swift */,
CC000001000000000000AB00 /* NSColor+RGB.swift */,
CC000001000000000000AB10 /* CGFloat+Format.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -480,6 +486,7 @@
CC2000000000000000000700 /* WCAGComplianceTests.swift */,
CC2000000000000000000800 /* APCAComplianceTests.swift */,
CC2000000000000000000900 /* NotificationNamesTests.swift */,
CC2000000000000000000A00 /* CGFloatFormatTests.swift */,
);
path = PikaTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -875,6 +882,7 @@
CC000001000000000000AAE1 /* NSColor+Luminance.swift in Sources */,
CC000001000000000000AAF1 /* NSColor+Hex.swift in Sources */,
CC000001000000000000AB01 /* NSColor+RGB.swift in Sources */,
CC000001000000000000AB11 /* CGFloat+Format.swift in Sources */,
EAA8AE1925B8EC070049299B /* KeyboardShortcutKey.swift in Sources */,
EA0C526F25AB683400AFF716 /* EyedropperButton.swift in Sources */,
EA72BB8425A5334B008205E7 /* MetalShader.metal in Sources */,
Expand Down Expand Up @@ -918,6 +926,7 @@
CC000001000000000000AAE2 /* NSColor+Luminance.swift in Sources */,
CC000001000000000000AAF2 /* NSColor+Hex.swift in Sources */,
CC000001000000000000AB02 /* NSColor+RGB.swift in Sources */,
CC000001000000000000AB12 /* CGFloat+Format.swift in Sources */,
EAE23DAE2D032A38005BB270 /* SplashTouchBar.swift in Sources */,
EAE23DAF2D032A38005BB270 /* OverflowContentViewModifier.swift in Sources */,
EAE23DB02D032A38005BB270 /* KeyboardShortcutGrid.swift in Sources */,
Expand Down Expand Up @@ -981,6 +990,7 @@
CC2000000000000000000701 /* WCAGComplianceTests.swift in Sources */,
CC2000000000000000000801 /* APCAComplianceTests.swift in Sources */,
CC2000000000000000000901 /* NotificationNamesTests.swift in Sources */,
CC2000000000000000000A01 /* CGFloatFormatTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1143,6 +1153,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
Expand Down Expand Up @@ -1174,6 +1185,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
Expand Down Expand Up @@ -1205,6 +1217,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Pika/Pika.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
Expand Down Expand Up @@ -1237,6 +1250,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Pika/Pika.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
Expand Down
4 changes: 2 additions & 2 deletions Pika/Constants/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Defaults
import KeyboardShortcuts
import SwiftUI

// swiftlint:disable trailing_comma
// swiftlint:disable line_length

extension KeyboardShortcuts.Name {
Expand Down Expand Up @@ -305,5 +304,6 @@ enum PikaText {
static let textDuration = NSLocalizedString("preferences.overlay.duration", comment: "Duration:")
}

// swiftlint:enable trailing_comma
// swiftlint:enable line_length
// line_length is disabled above because NSLocalizedString comment strings on lines 100 and 102
// exceed 120 characters and cannot be shortened without losing meaning for localizers.
2 changes: 2 additions & 0 deletions Pika/Extensions/APCACompliance.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Cocoa

// swiftlint:disable identifier_name
// identifier_name is disabled because the APCA algorithm uses conventional single-letter
// variable names (c, r, g, b, y, s) from the specification that would be misleading if renamed.

extension NSColor {
struct APCA {
Expand Down
17 changes: 17 additions & 0 deletions Pika/Extensions/CGFloat+Format.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import CoreGraphics
import Foundation

extension CGFloat {
/// Formats the value with up to `maxDecimalPlaces` decimal places, stripping trailing zeros.
/// e.g. 0.5000 → "0.5", 0.0000 → "0", 56.78 → "56.78"
func strippedDecimalString(maxDecimalPlaces: Int) -> String {
// Normalize -0.0 to +0.0 to avoid producing "-0" in output.
let value: CGFloat = self == 0 ? 0 : self
let formatted = String(format: "%.\(maxDecimalPlaces)f", value)
guard formatted.contains(".") else { return formatted }
Comment thread
superhighfives marked this conversation as resolved.
var result = formatted
while result.hasSuffix("0") { result.removeLast() }
if result.hasSuffix(".") { result.removeLast() }
return result
}
}
21 changes: 13 additions & 8 deletions Pika/Extensions/NSColor+HSL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import Cocoa
import Defaults

// swiftlint:disable identifier_name
// identifier_name is disabled because color science math uses conventional single-letter
// variable names (h, s, b, l, r, g) that would be misleading if renamed.

public struct HSBComponents { let h, s, b: CGFloat }
public struct HSLComponents { let h, s, l: CGFloat }

extension NSColor {
/*
* HSB
*/

public final func toHSBComponents() -> (h: CGFloat, s: CGFloat, b: CGFloat) {
public final func toHSBComponents() -> HSBComponents {
var h: CGFloat = 0.0
var s: CGFloat = 0.0
var b: CGFloat = 0.0
Expand All @@ -18,15 +23,15 @@ extension NSColor {
}

if toHexString() == NSColor.black.toHexString() {
return (0.0, 0.0, 0.0)
return HSBComponents(h: 0.0, s: 0.0, b: 0.0)
} else if toHexString() == NSColor.white.toHexString() {
return (0.0, 0.0, 1.0)
return HSBComponents(h: 0.0, s: 0.0, b: 1.0)
}

rgbaColor.getHue(&h, saturation: &s, brightness: &b, alpha: nil)
h = h.truncatingRemainder(dividingBy: 1.0)

return (h: h, s: s, b: b)
return HSBComponents(h: h, s: s, b: b)
}

/**
Expand Down Expand Up @@ -58,7 +63,7 @@ extension NSColor {
* HSL
*/

public final func toHSLComponents() -> (h: CGFloat, s: CGFloat, l: CGFloat) {
public final func toHSLComponents() -> HSLComponents {
var h: CGFloat = 0.0
var s: CGFloat = 0.0
var l: CGFloat = 0.0
Expand All @@ -69,9 +74,9 @@ extension NSColor {
let b = RGB.b

if toHexString() == NSColor.black.toHexString() {
return (0.0, 0.0, 0.0)
return HSLComponents(h: 0.0, s: 0.0, l: 0.0)
} else if toHexString() == NSColor.white.toHexString() {
return (0.0, 0.0, 1.0)
return HSLComponents(h: 0.0, s: 0.0, l: 1.0)
}

let min = Swift.min(Swift.min(r, g), b)
Expand Down Expand Up @@ -106,7 +111,7 @@ extension NSColor {
s = delta / (2 - max - min)
}

return (h: h, s: s, l: l)
return HSLComponents(h: h, s: s, l: l)
}

/**
Expand Down
6 changes: 3 additions & 3 deletions Pika/Extensions/NSColor+Hex.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Cocoa

extension NSColor {
func roundToHex(_ x: CGFloat) -> UInt32 {
guard x > 0 else { return 0 }
let rounded: CGFloat = round(x * 255.0)
func roundToHex(_ value: CGFloat) -> UInt32 {
guard value > 0 else { return 0 }
let rounded: CGFloat = round(value * 255.0)
return UInt32(rounded)
}

Expand Down
14 changes: 8 additions & 6 deletions Pika/Extensions/NSColor+Init.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Cocoa

extension NSColor {
// r, g, b, a are the public parameter labels for this init (NSColor(r:g:b:a:)) and cannot be renamed.
// swiftlint:disable:next identifier_name
convenience init(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat = 1) {
if (r > 1) || (g > 1) || (b > 1) {
self.init(red: r / 255, green: g / 255, blue: b / 255, alpha: a)
Expand All @@ -25,18 +27,18 @@ extension NSColor {
if hex.count == 3 {
let tmp = hex
hex = ""
for c in tmp {
hex += String([c, c])
for char in tmp {
hex += String([char, char])
}
}

let scanner = Scanner(string: hex)
var rgb: UInt64 = 0
scanner.scanHexInt64(&rgb)

let R = CGFloat((rgb >> 16) & 0xFF) / 255
let G = CGFloat((rgb >> 8) & 0xFF) / 255
let B = CGFloat(rgb & 0xFF) / 255
self.init(red: R, green: G, blue: B, alpha: alpha)
let red = CGFloat((rgb >> 16) & 0xFF) / 255
let green = CGFloat((rgb >> 8) & 0xFF) / 255
let blue = CGFloat(rgb & 0xFF) / 255
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
}
56 changes: 32 additions & 24 deletions Pika/Extensions/NSColor+Lab.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Cocoa

// swiftlint:disable identifier_name
// identifier_name is disabled because color science math uses conventional single-letter
// variable names (x, y, z, l, a, b, c, h, L, C, H) that would be misleading if renamed.

private struct XYZComponents { let x, y, z: CGFloat }
struct LabComponents { let l, a, b: CGFloat }
struct OklchComponents { let l, c, h: CGFloat }

extension NSColor {
// Shared linearization for sRGB components used by both LAB and OKLCH.
Expand All @@ -20,23 +26,25 @@ extension NSColor {
func toOpenGLString(style: CopyFormat = .css) -> String {
let RGB = toRGBAComponents()

let formatString: NSString
// %.5g strips trailing zeros but drops the decimal entirely for whole numbers,
// so append ".0" when there is no decimal point (e.g. 0 → "0.0", 1 → "1.0").
let red = { let s = String(format: "%.5g", RGB.r); return s.contains(".") ? s : "\(s).0" }()
let green = { let s = String(format: "%.5g", RGB.g); return s.contains(".") ? s : "\(s).0" }()
let blue = { let s = String(format: "%.5g", RGB.b); return s.contains(".") ? s : "\(s).0" }()

switch style {
case .css, .design, .swiftUI:
formatString = "rgba(%.5g, %.5g, %.5g, 1.0)"
return "rgba(\(red), \(green), \(blue), 1.0)"
case .unformatted:
formatString = "%.5g, %.5g, %.5g, 1.0"
return "\(red), \(green), \(blue), 1.0"
}

let openGLString = NSString(format: formatString, RGB.r, RGB.g, RGB.b)
return openGLString as String
}

/*
* CIE-LAB
*/

private func toXYZComponents() -> (x: CGFloat, y: CGFloat, z: CGFloat) {
private func toXYZComponents() -> XYZComponents {
let srgb = toRGBAComponents(in: .sRGB)
let r_lin = linearizeSRGB(srgb.r)
let g_lin = linearizeSRGB(srgb.g)
Expand All @@ -46,10 +54,10 @@ extension NSColor {
let y = r_lin * 0.2126729 + g_lin * 0.7151522 + b_lin * 0.0721750
let z = r_lin * 0.0193339 + g_lin * 0.1191920 + b_lin * 0.9503041

return (x: x, y: y, z: z)
return XYZComponents(x: x, y: y, z: z)
}

func toLabComponents() -> (l: CGFloat, a: CGFloat, b: CGFloat) {
func toLabComponents() -> LabComponents {
let xyz = toXYZComponents()

// D65 Reference White
Expand All @@ -70,30 +78,30 @@ extension NSColor {
let a_star = 500.0 * (f(xyz.x / Xn) - f(xyz.y / Yn))
let b_star = 200.0 * (f(xyz.y / Yn) - f(xyz.z / Zn))

return (l: L_star, a: a_star, b: b_star)
return LabComponents(l: L_star, a: a_star, b: b_star)
}

func toLabString(style: CopyFormat = .css) -> String {
let lab = toLabComponents()
let l_val = round(lab.l * 100) / 100
let a_val = round(lab.a * 100) / 100
let b_val = round(lab.b * 100) / 100
let l_str = (round(lab.l * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2)
let a_str = (round(lab.a * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2)
let b_str = (round(lab.b * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2)

Comment thread
superhighfives marked this conversation as resolved.
switch style {
case .css:
return String(format: "lab(%.2f %.2f %.2f)", l_val, a_val, b_val)
return "lab(\(l_str) \(a_str) \(b_str))"
case .design, .swiftUI:
return String(format: "lab(%.2f, %.2f, %.2f)", l_val, a_val, b_val)
return "lab(\(l_str), \(a_str), \(b_str))"
case .unformatted:
return String(format: "%.2f,%.2f,%.2f", l_val, a_val, b_val)
return "\(l_str), \(a_str), \(b_str)"
}
}

/*
* OKLCH
*/

func toOklchComponents() -> (l: CGFloat, c: CGFloat, h: CGFloat) {
func toOklchComponents() -> OklchComponents {
let srgb = toRGBAComponents(in: .sRGB)
let r_lin = linearizeSRGB(srgb.r)
let g_lin = linearizeSRGB(srgb.g)
Expand All @@ -115,22 +123,22 @@ extension NSColor {
var H = atan2(b, a) * 180.0 / .pi
if H < 0 { H += 360.0 }

return (l: L, c: C, h: H)
return OklchComponents(l: L, c: C, h: H)
}

func toOklchString(style: CopyFormat = .css) -> String {
let oklch = toOklchComponents()
let l_val = round(oklch.l * 10000) / 100
let c_val = round(oklch.c * 10000) / 10000
let h_val = round(oklch.h * 100) / 100
let l_str = (round(oklch.l * 10000) / 100).strippedDecimalString(maxDecimalPlaces: 2)
let c_str = (round(oklch.c * 10000) / 10000).strippedDecimalString(maxDecimalPlaces: 4)
let h_str = (round(oklch.h * 100) / 100).strippedDecimalString(maxDecimalPlaces: 2)

switch style {
case .css:
return String(format: "oklch(%.2f%% %.4f %.2f)", l_val, c_val, h_val)
return "oklch(\(l_str)% \(c_str) \(h_str))"
case .design, .swiftUI:
return String(format: "oklch(%.2f, %.4f, %.2f)", l_val, c_val, h_val)
return "oklch(\(l_str), \(c_str), \(h_str))"
case .unformatted:
return String(format: "%.2f, %.4f, %.2f", l_val, c_val, h_val)
return "\(l_str), \(c_str), \(h_str)"
}
}
}
Expand Down
Loading
Loading