Skip to content
Closed
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
29 changes: 29 additions & 0 deletions Sources/SkipUI/SkipUI/Color/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.compose.material.ContentAlpha
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.unit.dp
#endif

Expand Down Expand Up @@ -340,6 +342,33 @@ public struct Color: ShapeStyle, Renderable, Hashable {
#endif
}

public func mix(with rhs: Color, by fraction: Double, in colorSpace: Gradient.ColorSpace = .perceptual) -> Color {
#if SKIP
let clampedFraction = Self.clamp(fraction)
return Color(colorImpl: {
if colorSpace == .device {
let start = colorImpl().convert(ColorSpaces.Srgb)
let stop = rhs.colorImpl().convert(ColorSpaces.Srgb)
return androidx.compose.ui.graphics.Color(
red: start.red + (stop.red - start.red) * clampedFraction,
green: start.green + (stop.green - start.green) * clampedFraction,
blue: start.blue + (stop.blue - start.blue) * clampedFraction,
alpha: start.alpha + (stop.alpha - start.alpha) * clampedFraction)
} else {
// Compose's lerp interpolates in the Oklab color space, matching SwiftUI's .perceptual
return lerp(colorImpl(), rhs.colorImpl(), clampedFraction)
}
})
#else
return self
#endif
}

// SKIP @bridge
public func mix(with rhs: Color, by fraction: Double, bridgedColorSpace: Int) -> Color {
return mix(with: rhs, by: fraction, in: bridgedColorSpace == 0 ? .device : .perceptual)
}

// SKIP @bridge
public func saturate(by multiplier: Double) -> Color {
#if SKIP
Expand Down
10 changes: 8 additions & 2 deletions Sources/SkipUI/SkipUI/Graphics/Gradient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@ public struct Gradient : ShapeStyle, Hashable {
#endif

public struct ColorSpace : Hashable {
public static let device = Gradient.ColorSpace()
public static let perceptual = Gradient.ColorSpace()
let identifier: Int

init(identifier: Int) {
self.identifier = identifier
}

public static let device = Gradient.ColorSpace(identifier: 0)
public static let perceptual = Gradient.ColorSpace(identifier: 1)
}

public func colorSpace(_ space: Gradient.ColorSpace) -> AnyGradient {
Expand Down
31 changes: 31 additions & 0 deletions Tests/SkipUITests/ColorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,37 @@ final class ColorTests: XCSnapshotTestCase {
XCTAssertEqual("F", try render(compact: 1, view: Color.white.frame(width: 1.0, height: 1.0)).pixmap)
}

func testColorMixFullFraction() throws {
if #available(iOS 18.0, macOS 15.0, *) {
XCTAssertEqual("0", try render(compact: 1, view: Color.white.mix(with: Color.black, by: 1.0).frame(width: 1.0, height: 1.0)).pixmap)
}
}

func testColorMixFractionClampedAbove() throws {
if #available(iOS 18.0, macOS 15.0, *) {
XCTAssertEqual("F", try render(compact: 1, view: Color.black.mix(with: Color.white, by: 2.0).frame(width: 1.0, height: 1.0)).pixmap)
}
}

func testColorMixFractionClampedBelow() throws {
if #available(iOS 18.0, macOS 15.0, *) {
XCTAssertEqual("0", try render(compact: 1, view: Color.black.mix(with: Color.white, by: -1.0).frame(width: 1.0, height: 1.0)).pixmap)
}
}

func testColorMixDevice() throws {
if #available(iOS 18.0, macOS 15.0, *) {
XCTAssertEqual(plaf("808080"), try render(view: Color.black.mix(with: Color.white, by: 0.5, in: .device).frame(width: 1.0, height: 1.0)).pixmap)
}
}

func testColorMixPerceptual() throws {
if #available(iOS 18.0, macOS 15.0, *) {
// A 50% black/white mix in the default perceptual (Oklab) color space resolves to Oklab L = 0.5, i.e. #636363
XCTAssertEqual(plaf("636363"), try render(view: Color.black.mix(with: Color.white, by: 0.5).frame(width: 1.0, height: 1.0)).pixmap)
}
}

// Issue #146 follow-up: all three .colorset Input Methods must render the same color (#04F188).
// Skipped on Robolectric — Bundle.module colorset decoding falls back to gray there (pre-existing runner limitation).

Expand Down
Loading