diff --git a/.github/workflows/compilation-check.yml b/.github/workflows/compilation-check.yml index d1b1b48..fc5c2df 100644 --- a/.github/workflows/compilation-check.yml +++ b/.github/workflows/compilation-check.yml @@ -20,6 +20,8 @@ jobs: java-version: 17 - name: Check build run: ./gradlew detektWithoutTests build publishToMavenLocal + - name: Check unit tests + run: ./gradlew test - name: Install pods run: cd sample/ios-app && pod install if: matrix.os == 'macOS-latest' diff --git a/README.md b/README.md index 7c6bef8..6c53cec 100755 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:graphics:0.9.0") + commonMainApi("dev.icerock.moko:graphics:0.10.1") } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74fba13..d3f16da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] androidAppCompatVersion = "1.6.1" androidAnnotationVersion = "1.8.0" -mokoGraphicsVersion = "0.10.0" +mokoGraphicsVersion = "0.10.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } annotation = { module = "androidx.annotation:annotation", version.ref = "androidAnnotationVersion" } mokoGraphics = { module = "dev.icerock.moko:graphics", version.ref = "mokoGraphicsVersion" } - +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" } diff --git a/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt b/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt index 0487719..b9c2a99 100644 --- a/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt +++ b/graphics/src/commonMain/kotlin/dev/icerock/moko/graphics/ColorHEX.kt @@ -4,21 +4,67 @@ package dev.icerock.moko.graphics +/** + * Parses a hexadecimal color string into a [Color] object. + * + * This function supports multiple hex color formats with optional hash prefix: + * - **3 digits (RGB)**: Each digit is expanded to two digits (e.g., "F0A" → "FF00AA") + * - **4 digits (ARGB)**: Each digit is expanded to two digits (e.g., "8F0A" → "88FF00AA") + * - **6 digits (RRGGBB)**: Standard RGB format with full alpha (255) + * - **8 digits (AARRGGBB)**: Full ARGB format with explicit alpha channel + * + * The hash prefix (#) is optional and will be automatically removed if present. + * All input is converted to uppercase for consistent parsing. + * + * @param colorHEX + * The hexadecimal color string to parse. Can include optional '#' prefix. + * Supports formats: RGB, ARGB, RRGGBB, AARRGGBB (case-insensitive). + * + * @return A [Color] object with ARGB values in the range 0-255. + * + * @throws IllegalArgumentException + * if the input string is not a valid hex color format or contains invalid hexadecimal characters. + * + */ @Suppress("MagicNumber") -fun Color.Companion.parseColor(colorHEX: String): Color { - require(colorHEX[0] != '#') { "Unknown color" } +public fun Color.Companion.parseColor(colorHEX: String): Color { + val clean = colorHEX.removePrefix("#").uppercase() - var colorARGB = colorHEX.substring(1).toLong(16) - if (colorHEX.length == 7) { - colorARGB = colorARGB or 0x00000000ff000000 - } else { - require(colorHEX.length != 9) { "Unknown color" } - } + return when (clean.length) { + 3 -> { + // RGB -> RRGGBB + val r = clean[0].digitToInt(16) * 17 + val g = clean[1].digitToInt(16) * 17 + val b = clean[2].digitToInt(16) * 17 + Color(red = r, green = g, blue = b, alpha = 255) + } + + 4 -> { + // ARGB + val a = clean[0].digitToInt(radix = 16) * 17 + val r = clean[1].digitToInt(16) * 17 + val g = clean[2].digitToInt(16) * 17 + val b = clean[3].digitToInt(16) * 17 + Color(alpha = a, red = r, green = g, blue = b) + } - return Color( - alpha = (colorARGB.shr(24) and 0xFF).toInt(), - red = (colorARGB.shr(16) and 0xFF).toInt(), - green = (colorARGB.shr(8) and 0xFF).toInt(), - blue = (colorARGB.shr(0) and 0xFF).toInt(), - ) + 6 -> { + // RRGGBB + val r = clean.substring(0, 2).toInt(16) + val g = clean.substring(2, 4).toInt(16) + val b = clean.substring(4, 6).toInt(16) + Color(red = r, green = g, blue = b, alpha = 255) + } + + 8 -> { + // AARRGGBB + val a = clean.substring(0, 2).toInt(16) + val r = clean.substring(2, 4).toInt(16) + val g = clean.substring(4, 6).toInt(16) + val b = clean.substring(6, 8).toInt(16) + Color(alpha = a, red = r, green = g, blue = b) + } + + else -> throw IllegalArgumentException("Invalid Hex color: $colorHEX") + } } diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 5634658..a1f257d 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -27,6 +27,8 @@ kotlin { dependencies { commonMainApi(projects.graphics) + + commonTestImplementation(libs.kotlin.test) } android { diff --git a/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt new file mode 100644 index 0000000..6679649 --- /dev/null +++ b/sample/mpp-library/src/commonTest/kotlin/com/icerockdev/library/GraphicsTests.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package com.icerockdev.library + +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.graphics.parseColor +import kotlin.test.Test +import kotlin.test.assertEquals + +class GraphicsTests { + + @Test + fun parseWithoutAlphaTest() { + val colorRGB = Color.parseColor("#88AAFF") + val colorFull = Color.parseColor("#FF88AAFF") + val colorRGBShort = Color.parseColor("#8AF") + val colorAlphaShort = Color.parseColor("#F8AF") + assertEquals( + expected = colorRGB.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorFull.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorRGBShort.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorAlphaShort.argb, + actual = 0xFF88AAFF + ) + } + + @Test + fun parseWithAlphaTest() { + val colorFull = Color.parseColor("#7788AAFF") + val colorAlphaShort = Color.parseColor("#78AF") + assertEquals( + expected = colorAlphaShort.argb, + actual = 0x7788AAFF + ) + assertEquals( + expected = colorFull.argb, + actual = 0x7788AAFF + ) + } + + @Test + fun parseWithoutPrefixTest() { + val colorRGB = Color.parseColor("88AAFF") + val colorFull = Color.parseColor("FF88AAFF") + val colorRGBShort = Color.parseColor("8AF") + val colorAlphaShort = Color.parseColor("F8AF") + assertEquals( + expected = colorRGB.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorFull.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorRGBShort.argb, + actual = 0xFF88AAFF + ) + assertEquals( + expected = colorAlphaShort.argb, + actual = 0xFF88AAFF + ) + } +} \ No newline at end of file