← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


commit 432762286b16522f3b498337b30b34b76d89a3ed
parent 2d158a50fc69bb391a6bcc75d11eba4403e27158
Author: Michael Camilleri <[email protected]>
Date:   Sun, 15 Feb 2026 05:54:36 +0900

Optimise accent colour calculations

Co-Authored-By: Claude 4.5 Sonnet <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 8--------
DListlessMac/ColorExtensions.swift | 21---------------------
MListlessMac/Views/TaskRowView.swift | 42+++++++++++++++++++-----------------------
DListlessiOS/ColorExtensions.swift | 17-----------------
MListlessiOS/Views/TaskRowView.swift | 31++++++++++++++++++-------------

5 files changed, 37 insertions(+), 82 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -9,7 +9,6 @@
 /* Begin PBXBuildFile section */
 		074A81D9DAF58E8E088CBC89 /* TaskListView+Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431B0EA72D590AF0CC868515 /* TaskListView+Toolbar.swift */; };
 		0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; };
-		0BB5D2C4FADE3F1E22202814 /* ColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 917597025B3D5D18E33982D3 /* ColorExtensions.swift */; };
 		15B71073767FB4766A6BA2BE /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */; };
 		1AA328A921EF8A7FDD03119A /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B048B19C5219862BBED2E7 /* TestHelpers.swift */; };
 		269B93D5543770B464DFB37A /* TaskStoreOrderingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3A2DDCE24E54ABCCFBBD4C /* TaskStoreOrderingTests.swift */; };
@@ -18,7 +17,6 @@
 		42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
 		5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
 		614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */; };
-		69D1909CB6D2368CF90065C7 /* ColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDF292392702661DBB94D06 /* ColorExtensions.swift */; };
 		6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
 		731477635D3F4DFF1F78D673 /* AppColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1FE1858BC9E8915A091D33 /* AppColors.swift */; };
 		7B2CD636BA3B63A586F93E31 /* TaskRowSwipeGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C16D36971A140364159FB9 /* TaskRowSwipeGesture.swift */; };
@@ -79,7 +77,6 @@
 		7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
 		81880970CCFC2CB00B65047E /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
 		82A3509AD32A54434BCC8017 /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
-		917597025B3D5D18E33982D3 /* ColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtensions.swift; sourceTree = "<group>"; };
 		9262207DAC21619BD9EDEE15 /* Listless.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Listless.entitlements; sourceTree = "<group>"; };
 		93FF5B9F5B979D54D5DEE192 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; };
 		944BAE054AAC1B9C4FC954F9 /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
@@ -89,7 +86,6 @@
 		9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Listless.xcdatamodel; sourceTree = "<group>"; };
 		AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessiOSApp.swift; sourceTree = "<group>"; };
 		B4D74AE2501F35974F57D21F /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
-		BBDF292392702661DBB94D06 /* ColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtensions.swift; sourceTree = "<group>"; };
 		C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
 		C71466C5CD1A5BA984352F8D /* Listless iOS Unit Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Listless iOS Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		C9B14DC786A336008AAB78EE /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; };
@@ -165,7 +161,6 @@
 		954AAB8DDFF6E6D6FD6A0A2C /* ListlessiOS */ = {
 			isa = PBXGroup;
 			children = (
-				BBDF292392702661DBB94D06 /* ColorExtensions.swift */,
 				3313FEDB101EECA4B344EEF4 /* Info.plist */,
 				9262207DAC21619BD9EDEE15 /* Listless.entitlements */,
 				AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */,
@@ -199,7 +194,6 @@
 		D5B197AFF26144948D032299 /* ListlessMac */ = {
 			isa = PBXGroup;
 			children = (
-				917597025B3D5D18E33982D3 /* ColorExtensions.swift */,
 				01E141436176F83594E2F26B /* Info.plist */,
 				7C73E9D4C42CCABBF0F33543 /* Listless.entitlements */,
 				1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */,
@@ -357,7 +351,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				83378A0575640417ED41FC25 /* ClickableTextField.swift in Sources */,
-				0BB5D2C4FADE3F1E22202814 /* ColorExtensions.swift in Sources */,
 				15B71073767FB4766A6BA2BE /* HoverCursorModifier.swift in Sources */,
 				D8EFF49E9156083D675D47F0 /* KeyboardNavigationModifier.swift in Sources */,
 				96617677059FABDBB80D642B /* Listless.xcdatamodeld in Sources */,
@@ -379,7 +372,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				731477635D3F4DFF1F78D673 /* AppColors.swift in Sources */,
-				69D1909CB6D2368CF90065C7 /* ColorExtensions.swift in Sources */,
 				FEC96DAA2BF8BB5D6D8504EB /* HoverCursorModifier.swift in Sources */,
 				6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */,
 				5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */,

diff --git a/ListlessMac/ColorExtensions.swift b/ListlessMac/ColorExtensions.swift

@@ -1,21 +0,0 @@
-import AppKit
-import SwiftUI
-
-typealias PlatformColor = NSColor
-
-extension NSColor {
-    var hsba: (hue: Double, saturation: Double, brightness: Double, alpha: Double) {
-        var hue: CGFloat = 0
-        var saturation: CGFloat = 0
-        var brightness: CGFloat = 0
-        var alpha: CGFloat = 0
-
-        // Convert to RGB color space first for consistency
-        guard let rgbColor = self.usingColorSpace(.deviceRGB) else {
-            return (0, 0, 0, 0)
-        }
-        rgbColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
-
-        return (Double(hue), Double(saturation), Double(brightness), Double(alpha))
-    }
-}

diff --git a/ListlessMac/Views/TaskRowView.swift b/ListlessMac/Views/TaskRowView.swift

@@ -33,34 +33,27 @@ struct TaskRowView: View {
 
         // Gradient matches gradient.png: coral/red → pink/magenta → purple/blue
         let progress = Double(index) / Double(totalTasks - 1)
+        let top    = (h: 0.98, s: 0.85, b: 1.00)
+        let mid    = (h: 0.88, s: 0.75, b: 0.95)
+        let bottom = (h: 0.72, s: 0.65, b: 0.85)
 
-        // Define color stops based on the gradient image
-        let topColor = Color(hue: 0.98, saturation: 0.85, brightness: 1.0)  // Coral/red
-        let midColor = Color(hue: 0.88, saturation: 0.75, brightness: 0.95)  // Pink/magenta
-        let bottomColor = Color(hue: 0.72, saturation: 0.65, brightness: 0.85)  // Purple/blue
-
-        // Interpolate between colors
         if progress < 0.5 {
-            // Top half: coral → magenta
-            let localProgress = progress * 2.0
-            return interpolateColor(from: topColor, to: midColor, progress: localProgress)
+            return interpolateHSB(from: top, to: mid, progress: progress * 2.0)
         } else {
-            // Bottom half: magenta → purple/blue
-            let localProgress = (progress - 0.5) * 2.0
-            return interpolateColor(from: midColor, to: bottomColor, progress: localProgress)
+            return interpolateHSB(from: mid, to: bottom, progress: (progress - 0.5) * 2.0)
         }
     }
 
-    private func interpolateColor(from: Color, to: Color, progress: Double) -> Color {
-        // Extract HSB components and interpolate
-        let fromHSB = PlatformColor(from).hsba
-        let toHSB = PlatformColor(to).hsba
-
-        let hue = fromHSB.hue + (toHSB.hue - fromHSB.hue) * progress
-        let saturation = fromHSB.saturation + (toHSB.saturation - fromHSB.saturation) * progress
-        let brightness = fromHSB.brightness + (toHSB.brightness - fromHSB.brightness) * progress
-
-        return Color(hue: hue, saturation: saturation, brightness: brightness)
+    private func interpolateHSB(
+        from: (h: Double, s: Double, b: Double),
+        to: (h: Double, s: Double, b: Double),
+        progress: Double
+    ) -> Color {
+        Color(
+            hue: from.h + (to.h - from.h) * progress,
+            saturation: from.s + (to.s - from.s) * progress,
+            brightness: from.b + (to.b - from.b) * progress
+        )
     }
 
     init(
@@ -191,7 +184,10 @@ struct TaskRowView: View {
                 editingTitle = newValue
             }
         }
-        .onChange(of: "\(index)-\(totalTasks)") { _, _ in
+        .onChange(of: index) { _, _ in
+            cachedAccentColor = computeAccentColor()
+        }
+        .onChange(of: totalTasks) { _, _ in
             cachedAccentColor = computeAccentColor()
         }
         .onAppear {

diff --git a/ListlessiOS/ColorExtensions.swift b/ListlessiOS/ColorExtensions.swift

@@ -1,17 +0,0 @@
-import SwiftUI
-import UIKit
-
-typealias PlatformColor = UIColor
-
-extension UIColor {
-    var hsba: (hue: Double, saturation: Double, brightness: Double, alpha: Double) {
-        var hue: CGFloat = 0
-        var saturation: CGFloat = 0
-        var brightness: CGFloat = 0
-        var alpha: CGFloat = 0
-
-        getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
-
-        return (Double(hue), Double(saturation), Double(brightness), Double(alpha))
-    }
-}

diff --git a/ListlessiOS/Views/TaskRowView.swift b/ListlessiOS/Views/TaskRowView.swift

@@ -136,7 +136,10 @@ struct TaskRowView: View {
                 editingTitle = newValue
             }
         }
-        .onChange(of: "\(index)-\(totalTasks)") { _, _ in
+        .onChange(of: index) { _, _ in
+            cachedAccentColor = computeAccentColor()
+        }
+        .onChange(of: totalTasks) { _, _ in
             cachedAccentColor = computeAccentColor()
         }
         .taskSwipeGesture(
@@ -165,24 +168,26 @@ struct TaskRowView: View {
 
         // Gradient: coral/red → pink/magenta → purple/blue (matches macOS)
         let progress = Double(index) / Double(totalTasks - 1)
-        let topColor = Color(hue: 0.98, saturation: 0.85, brightness: 1.0)
-        let midColor = Color(hue: 0.88, saturation: 0.75, brightness: 0.95)
-        let bottomColor = Color(hue: 0.72, saturation: 0.65, brightness: 0.85)
+        let top    = (h: 0.98, s: 0.85, b: 1.00)
+        let mid    = (h: 0.88, s: 0.75, b: 0.95)
+        let bottom = (h: 0.72, s: 0.65, b: 0.85)
 
         if progress < 0.5 {
-            return interpolateColor(from: topColor, to: midColor, progress: progress * 2.0)
+            return interpolateHSB(from: top, to: mid, progress: progress * 2.0)
         } else {
-            return interpolateColor(from: midColor, to: bottomColor, progress: (progress - 0.5) * 2.0)
+            return interpolateHSB(from: mid, to: bottom, progress: (progress - 0.5) * 2.0)
         }
     }
 
-    private func interpolateColor(from: Color, to: Color, progress: Double) -> Color {
-        let fromHSB = PlatformColor(from).hsba
-        let toHSB = PlatformColor(to).hsba
-        return Color(
-            hue: fromHSB.hue + (toHSB.hue - fromHSB.hue) * progress,
-            saturation: fromHSB.saturation + (toHSB.saturation - fromHSB.saturation) * progress,
-            brightness: fromHSB.brightness + (toHSB.brightness - fromHSB.brightness) * progress
+    private func interpolateHSB(
+        from: (h: Double, s: Double, b: Double),
+        to: (h: Double, s: Double, b: Double),
+        progress: Double
+    ) -> Color {
+        Color(
+            hue: from.h + (to.h - from.h) * progress,
+            saturation: from.s + (to.s - from.s) * progress,
+            brightness: from.b + (to.b - from.b) * progress
         )
     }