From 2dd8d3223fb7d0e4475d7d1c520ffcb6b1caf813 Mon Sep 17 00:00:00 2001 From: Nikita Lipsky Date: Wed, 15 Feb 2023 16:32:54 +0200 Subject: [PATCH] Support for XML Vector Drawables in resources library. (#2749) Support for XML Vector Drawables. --- components/resources/demo/.gitignore | 6 +- .../demo/androidApp/build.gradle.kts | 7 +- components/resources/demo/iosApp/Podfile | 2 +- .../project.pbxproj | 46 +-- .../resources/demo/shared/UseResources.kt | 6 + .../src/commonMain/resources/vector.xml | 34 +++ components/resources/library/build.gradle.kts | 8 +- .../resources/ComposeResource.common.kt | 56 +++- .../compose/resources/vector/ValueParsers.kt | 104 +++++++ .../resources/vector/XmlVectorParser.kt | 276 ++++++++++++++++++ .../resources/vector/xmldom/Element.kt | 11 + .../compose/resources/vector/xmldom/Node.kt | 14 + .../resources/vector/xmldom/NodeList.kt | 10 + .../compose/resources/Resource.ios.kt | 5 +- .../compose/resources/xmldom/DomXmlParser.kt | 114 ++++++++ .../compose/resources/Resource.js.kt | 5 + .../resources/Resource.jvmandandroid.kt | 22 ++ .../compose/resources/xmldom/ElementImpl.kt | 10 + .../compose/resources/xmldom/NodeImpl.kt | 28 ++ .../compose/resources/Resource.macos.kt | 5 + 20 files changed, 723 insertions(+), 46 deletions(-) rename components/resources/demo/iosApp/{ResourcesDemo.xcodeproj => iosApp.xcodeproj}/project.pbxproj (85%) create mode 100644 components/resources/demo/shared/src/commonMain/resources/vector.xml create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt create mode 100644 components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt create mode 100644 components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt create mode 100644 components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt create mode 100644 components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt create mode 100644 components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt diff --git a/components/resources/demo/.gitignore b/components/resources/demo/.gitignore index 3c2c4931fd..e81ac1f55c 100644 --- a/components/resources/demo/.gitignore +++ b/components/resources/demo/.gitignore @@ -1,6 +1,6 @@ iosApp/Podfile.lock iosApp/Pods/* -iosApp/ResourcesDemo.xcworkspace/* -iosApp/ResourcesDemo.xcodeproj/* -!iosApp/ResourcesDemo.xcodeproj/project.pbxproj +iosApp/iosApp.xcworkspace/* +iosApp/iosApp.xcodeproj/* +!iosApp/iosApp.xcodeproj/project.pbxproj shared/shared.podspec diff --git a/components/resources/demo/androidApp/build.gradle.kts b/components/resources/demo/androidApp/build.gradle.kts index 4aa54613a1..56f228a7c8 100644 --- a/components/resources/demo/androidApp/build.gradle.kts +++ b/components/resources/demo/androidApp/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") kotlin("android") + id("org.jetbrains.compose") } dependencies { @@ -12,12 +13,6 @@ dependencies { } android { - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.3.2" - } compileSdk = 33 defaultConfig { applicationId = "me.user.androidApp" diff --git a/components/resources/demo/iosApp/Podfile b/components/resources/demo/iosApp/Podfile index f66ae95df2..aff9c517b2 100644 --- a/components/resources/demo/iosApp/Podfile +++ b/components/resources/demo/iosApp/Podfile @@ -1,4 +1,4 @@ -target 'ResourcesDemo' do +target 'iosApp' do use_frameworks! platform :ios, '14.1' pod 'shared', :path => '../shared' diff --git a/components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj b/components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj similarity index 85% rename from components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj rename to components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj index 3f014cf4ad..595f09262d 100644 --- a/components/resources/demo/iosApp/ResourcesDemo.xcodeproj/project.pbxproj +++ b/components/resources/demo/iosApp/iosApp.xcodeproj/project.pbxproj @@ -8,16 +8,16 @@ /* Begin PBXBuildFile section */ 2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; }; - C1FC908188C4E8695729CB06 /* Pods_ResourcesDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */; }; + C1FC908188C4E8695729CB06 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ResourcesDemo.debug.xcconfig"; path = "Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo.debug.xcconfig"; sourceTree = ""; }; + 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; - 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ResourcesDemo.release.xcconfig"; path = "Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo.release.xcconfig"; sourceTree = ""; }; + 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; 7555FF7B242A565900829871 /* ResourcesDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResourcesDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ResourcesDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AB3632DC29227652001CCB65 /* TeamId.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TeamId.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -26,7 +26,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C1FC908188C4E8695729CB06 /* Pods_ResourcesDemo.framework in Frameworks */, + C1FC908188C4E8695729CB06 /* Pods_iosApp.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,7 +72,7 @@ B62309C7396AD7BF607A63B2 /* Frameworks */ = { isa = PBXGroup; children = ( - 8DE96E47030356CE6AD9794A /* Pods_ResourcesDemo.framework */, + 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */, ); name = Frameworks; sourceTree = ""; @@ -80,8 +80,8 @@ E1DAFBE8E1CFC0878361EF0E /* Pods */ = { isa = PBXGroup; children = ( - 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */, - 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */, + 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */, + 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -89,9 +89,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 7555FF7A242A565900829871 /* ResourcesDemo */ = { + 7555FF7A242A565900829871 /* iosApp */ = { isa = PBXNativeTarget; - buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "ResourcesDemo" */; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; buildPhases = ( E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */, 7555FF77242A565900829871 /* Sources */, @@ -103,7 +103,7 @@ ); dependencies = ( ); - name = ResourcesDemo; + name = iosApp; productName = iosApp; productReference = 7555FF7B242A565900829871 /* ResourcesDemo.app */; productType = "com.apple.product-type.application"; @@ -123,7 +123,7 @@ }; }; }; - buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "ResourcesDemo" */; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; @@ -136,7 +136,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 7555FF7A242A565900829871 /* ResourcesDemo */, + 7555FF7A242A565900829871 /* iosApp */, ); }; /* End PBXProject section */ @@ -158,15 +158,15 @@ files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ResourcesDemo/Pods-ResourcesDemo-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; showEnvVarsInLog = 0; }; E8D673591E7196AEA2EA10E2 /* [CP] Check Pods Manifest.lock */ = { @@ -184,7 +184,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ResourcesDemo-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -325,7 +325,7 @@ }; 7555FFA6242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-ResourcesDemo.debug.xcconfig */; + baseConfigurationReference = 1EB65E27D2C0F884D0A1A133 /* Pods-iosApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; @@ -340,7 +340,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "ResourcesDemo"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -349,7 +349,7 @@ }; 7555FFA7242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-ResourcesDemo.release.xcconfig */; + baseConfigurationReference = 3D7A606AB0AD7636269BD9D0 /* Pods-iosApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "Apple Development"; @@ -364,7 +364,7 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.ResourcesDemo${TEAM_ID}"; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "ResourcesDemo"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -374,7 +374,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 7555FF76242A565900829871 /* Build configuration list for PBXProject "ResourcesDemo" */ = { + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 7555FFA3242A565B00829871 /* Debug */, @@ -383,7 +383,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "ResourcesDemo" */ = { + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 7555FFA6242A565B00829871 /* Debug */, diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt index 9b89bae350..bc5f514132 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt +++ b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/UseResources.kt @@ -2,8 +2,10 @@ package org.jetbrains.compose.resources.demo.shared import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity import org.jetbrains.compose.resources.* @OptIn(ExperimentalResourceApi::class) @@ -19,5 +21,9 @@ internal fun UseResources() { bitmap = resource("img.webp").rememberImageBitmap().orEmpty(), contentDescription = null, ) + Icon( + imageVector = resource("vector.xml").rememberImageVector(LocalDensity.current).orEmpty(), + contentDescription = null + ) } } diff --git a/components/resources/demo/shared/src/commonMain/resources/vector.xml b/components/resources/demo/shared/src/commonMain/resources/vector.xml new file mode 100644 index 0000000000..1f6bb29060 --- /dev/null +++ b/components/resources/demo/shared/src/commonMain/resources/vector.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/components/resources/library/build.gradle.kts b/components/resources/library/build.gradle.kts index 796ef6019e..bd26802d18 100644 --- a/components/resources/library/build.gradle.kts +++ b/components/resources/library/build.gradle.kts @@ -38,8 +38,12 @@ kotlin { val skikoMain by creating { dependsOn(commonMain) } + val jvmAndAndroidMain by creating { + dependsOn(commonMain) + } val desktopMain by getting { dependsOn(skikoMain) + dependsOn(jvmAndAndroidMain) } val desktopTest by getting { dependencies { @@ -48,7 +52,9 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4") } } - val androidMain by getting {} + val androidMain by getting { + dependsOn(jvmAndAndroidMain) + } val androidTest by getting { dependencies { diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt index f39c1fcde1..c09f0f2b6a 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ComposeResource.common.kt @@ -7,19 +7,25 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.* import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Density +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.parseVectorRoot +import androidx.compose.ui.unit.dp private val emptyImageBitmap: ImageBitmap by lazy { ImageBitmap(1, 1) } -/** - * Get and remember resource. While loading and if resource not exists result will be null. - */ -@ExperimentalResourceApi +private val emptyImageVector: ImageVector by lazy { + ImageVector.Builder(defaultWidth = 1.dp, defaultHeight = 1.dp, viewportWidth = 1f, viewportHeight = 1f).build() +} + +@OptIn(ExperimentalResourceApi::class) @Composable -fun Resource.rememberImageBitmap(): LoadState { - val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } +private fun Resource.rememberLoadingResource(fromByteArrayConverter: ByteArray.()->T): LoadState { + val state: MutableState> = remember(this) { mutableStateOf(LoadState.Loading()) } LaunchedEffect(this) { state.value = try { - LoadState.Success(readBytes().toImageBitmap()) + LoadState.Success(readBytes().fromByteArrayConverter()) } catch (e: Exception) { LoadState.Error(e) } @@ -28,13 +34,41 @@ fun Resource.rememberImageBitmap(): LoadState { } /** - * return current ImageBitmap or return empty while loading + * Get and remember resource. While loading and if resource not exists result will be null. + */ +@ExperimentalResourceApi +@Composable +fun Resource.rememberImageBitmap(): LoadState = + rememberLoadingResource { toImageBitmap() } + +/** + * Get and remember resource. While loading and if resource not exists result will be null. */ @ExperimentalResourceApi -fun LoadState.orEmpty(): ImageBitmap = when (this) { - is LoadState.Loading -> emptyImageBitmap +@Composable +fun Resource.rememberImageVector(density: Density): LoadState = + rememberLoadingResource { toImageVector(density) } + +private fun LoadState.orEmpty(emptyValue: T): T = when (this) { + is LoadState.Loading -> emptyValue is LoadState.Success -> this.value - is LoadState.Error -> emptyImageBitmap + is LoadState.Error -> emptyValue } +/** + * return current ImageBitmap or return empty while loading + */ +@ExperimentalResourceApi +fun LoadState.orEmpty(): ImageBitmap = orEmpty(emptyImageBitmap) + +/** + * return current ImageVector or return empty while loading + */ +@ExperimentalResourceApi +fun LoadState.orEmpty(): ImageVector = orEmpty(emptyImageVector) + internal expect fun ByteArray.toImageBitmap(): ImageBitmap + +internal expect fun parseXML(byteArray: ByteArray): Element + +internal fun ByteArray.toImageVector(density: Density): ImageVector = parseXML(this).parseVectorRoot(density) diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt new file mode 100644 index 0000000000..cf065f4bfc --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/ValueParsers.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.resources.vector + +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +private const val ALPHA_MASK = 0xFF000000.toInt() + +// parseColorValue is copied from Android: +// https://cs.android.com/android-studio/platform/tools/base/+/05fadd8cb2aaafb77da02048c7a240b2147ff293:sdk-common/src/main/java/com/android/ide/common/vectordrawable/VdUtil.kt;l=58 +/** + * Parses a color value in #AARRGGBB format. + * + * @param color the color value string + * @return the integer color value + */ +internal fun parseColorValue(color: String): Int { + require(color.startsWith("#")) { "Invalid color value $color" } + + return when (color.length) { + 7 -> { + // #RRGGBB + color.substring(1).toUInt(16).toInt() or ALPHA_MASK + } + 9 -> { + // #AARRGGBB + color.substring(1).toUInt(16).toInt() + } + 4 -> { + // #RGB + val v = color.substring(1).toUInt(16).toInt() + var k = (v shr 8 and 0xF) * 0x110000 + k = k or (v shr 4 and 0xF) * 0x1100 + k = k or (v and 0xF) * 0x11 + k or ALPHA_MASK + } + 5 -> { + // #ARGB + val v = color.substring(1).toUInt(16).toInt() + var k = (v shr 12 and 0xF) * 0x11000000 + k = k or (v shr 8 and 0xF) * 0x110000 + k = k or (v shr 4 and 0xF) * 0x1100 + k = k or (v and 0xF) * 0x11 + k or ALPHA_MASK + } + else -> ALPHA_MASK + } +} + +internal fun parseFillType(fillType: String): PathFillType = when (fillType) { + "nonZero" -> PathFillType.NonZero + "evenOdd" -> PathFillType.EvenOdd + else -> throw UnsupportedOperationException("unknown fillType: $fillType") +} + +internal fun parseStrokeCap(strokeCap: String): StrokeCap = when (strokeCap) { + "butt" -> StrokeCap.Butt + "round" -> StrokeCap.Round + "square" -> StrokeCap.Square + else -> throw UnsupportedOperationException("unknown strokeCap: $strokeCap") +} + +internal fun parseStrokeJoin(strokeJoin: String): StrokeJoin = when (strokeJoin) { + "miter" -> StrokeJoin.Miter + "round" -> StrokeJoin.Round + "bevel" -> StrokeJoin.Bevel + else -> throw UnsupportedOperationException("unknown strokeJoin: $strokeJoin") +} + +internal fun parseTileMode(tileMode: String): TileMode = when (tileMode) { + "clamp" -> TileMode.Clamp + "repeated" -> TileMode.Repeated + "mirror" -> TileMode.Mirror + else -> throw throw UnsupportedOperationException("unknown tileMode: $tileMode") +} + +internal fun String?.parseDp(density: Density): Dp = with(density) { + return when { + this@parseDp == null -> 0f.dp + endsWith("dp") -> removeSuffix("dp").toFloat().dp + endsWith("px") -> removeSuffix("px").toFloat().toDp() + else -> throw UnsupportedOperationException("value should ends with dp or px") + } +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt new file mode 100644 index 0000000000..c669f6e9c8 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/XmlVectorParser.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.compose.resources.vector + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.vector.DefaultPivotX +import androidx.compose.ui.graphics.vector.DefaultPivotY +import androidx.compose.ui.graphics.vector.DefaultRotation +import androidx.compose.ui.graphics.vector.DefaultScaleX +import androidx.compose.ui.graphics.vector.DefaultScaleY +import androidx.compose.ui.graphics.vector.DefaultTranslationX +import androidx.compose.ui.graphics.vector.DefaultTranslationY +import androidx.compose.ui.graphics.vector.EmptyPath +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.addPathNodes +import org.jetbrains.compose.resources.vector.BuildContext.Group +import androidx.compose.ui.unit.Density +import org.jetbrains.compose.resources.vector.xmldom.* + + +// Parsing logic is the same as in Android implementation +// (compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/graphics/vector/compat/XmlVectorParser.kt) +// +// Except there is no support for linking with external resources +// (for example, we can't reference to color defined in another file) +// +// Specification: +// https://developer.android.com/reference/android/graphics/drawable/VectorDrawable + +private const val ANDROID_NS = "http://schemas.android.com/apk/res/android" +private const val AAPT_NS = "http://schemas.android.com/aapt" + +private class BuildContext { + val currentGroups = mutableListOf() + + enum class Group { + /** + * Group that exists in xml file + */ + Real, + + /** + * Group that doesn't exist in xml file. We add it manually when we see node. + * It will be automatically popped when the real group will be popped. + */ + Virtual + } +} + +internal fun Element.parseVectorRoot(density: Density): ImageVector { + val context = BuildContext() + val builder = ImageVector.Builder( + defaultWidth = attributeOrNull(ANDROID_NS, "width").parseDp(density), + defaultHeight = attributeOrNull(ANDROID_NS, "height").parseDp(density), + viewportWidth = attributeOrNull(ANDROID_NS, "viewportWidth")?.toFloat() ?: 0f, + viewportHeight = attributeOrNull(ANDROID_NS, "viewportHeight")?.toFloat() ?: 0f + ) + parseVectorNodes(builder, context) + return builder.build() +} + +private fun Element.parseVectorNodes(builder: ImageVector.Builder, context: BuildContext) { + childrenSequence + .filterIsInstance() + .forEach { + it.parseVectorNode(builder, context) + } +} + +private fun Element.parseVectorNode(builder: ImageVector.Builder, context: BuildContext) { + when (nodeName) { + "path" -> parsePath(builder) + "clip-path" -> parseClipPath(builder, context) + "group" -> parseGroup(builder, context) + } +} + +private fun Element.parsePath(builder: ImageVector.Builder) { + builder.addPath( + pathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")), + pathFillType = attributeOrNull(ANDROID_NS, "fillType") + ?.let(::parseFillType) ?: PathFillType.NonZero, + name = attributeOrNull(ANDROID_NS, "name") ?: "", + fill = attributeOrNull(ANDROID_NS, "fillColor")?.let(::parseStringBrush) + ?: apptAttr(ANDROID_NS, "fillColor")?.let(Element::parseElementBrush), + fillAlpha = attributeOrNull(ANDROID_NS, "fillAlpha")?.toFloat() ?: 1.0f, + stroke = attributeOrNull(ANDROID_NS, "strokeColor")?.let(::parseStringBrush) + ?: apptAttr(ANDROID_NS, "strokeColor")?.let(Element::parseElementBrush), + strokeAlpha = attributeOrNull(ANDROID_NS, "strokeAlpha")?.toFloat() ?: 1.0f, + strokeLineWidth = attributeOrNull(ANDROID_NS, "strokeWidth")?.toFloat() ?: 1.0f, + strokeLineCap = attributeOrNull(ANDROID_NS, "strokeLineCap") + ?.let(::parseStrokeCap) ?: StrokeCap.Butt, + strokeLineJoin = attributeOrNull(ANDROID_NS, "strokeLineJoin") + ?.let(::parseStrokeJoin) ?: StrokeJoin.Miter, + strokeLineMiter = attributeOrNull(ANDROID_NS, "strokeMiterLimit")?.toFloat() ?: 1.0f, + trimPathStart = attributeOrNull(ANDROID_NS, "trimPathStart")?.toFloat() ?: 0.0f, + trimPathEnd = attributeOrNull(ANDROID_NS, "trimPathEnd")?.toFloat() ?: 1.0f, + trimPathOffset = attributeOrNull(ANDROID_NS, "trimPathOffset")?.toFloat() ?: 0.0f + ) +} + +private fun Element.parseClipPath(builder: ImageVector.Builder, context: BuildContext) { + builder.addGroup( + name = attributeOrNull(ANDROID_NS, "name") ?: "", + clipPathData = addPathNodes(attributeOrNull(ANDROID_NS, "pathData")) + ) + context.currentGroups.add(Group.Virtual) +} + +private fun Element.parseGroup(builder: ImageVector.Builder, context: BuildContext) { + builder.addGroup( + attributeOrNull(ANDROID_NS, "name") ?: "", + attributeOrNull(ANDROID_NS, "rotation")?.toFloat() ?: DefaultRotation, + attributeOrNull(ANDROID_NS, "pivotX")?.toFloat() ?: DefaultPivotX, + attributeOrNull(ANDROID_NS, "pivotY")?.toFloat() ?: DefaultPivotY, + attributeOrNull(ANDROID_NS, "scaleX")?.toFloat() ?: DefaultScaleX, + attributeOrNull(ANDROID_NS, "scaleY")?.toFloat() ?: DefaultScaleY, + attributeOrNull(ANDROID_NS, "translateX")?.toFloat() ?: DefaultTranslationX, + attributeOrNull(ANDROID_NS, "translateY")?.toFloat() ?: DefaultTranslationY, + EmptyPath + ) + context.currentGroups.add(Group.Real) + + parseVectorNodes(builder, context) + + do { + val removedGroup = context.currentGroups.removeLastOrNull() + builder.clearGroup() + } while (removedGroup == Group.Virtual) +} + +private fun parseStringBrush(str: String) = SolidColor(Color(parseColorValue(str))) + +private fun Element.parseElementBrush(): Brush? = + childrenSequence + .filterIsInstance() + .find { it.nodeName == "gradient" } + ?.parseGradient() + +private fun Element.parseGradient(): Brush? { + return when (attributeOrNull(ANDROID_NS, "type")) { + "linear" -> parseLinearGradient() + "radial" -> parseRadialGradient() + "sweep" -> parseSweepGradient() + else -> null + } +} + +@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") +private fun Element.parseLinearGradient() = Brush.linearGradient( + colorStops = parseColorStops(), + start = Offset( + attributeOrNull(ANDROID_NS, "startX")?.toFloat() ?: 0f, + attributeOrNull(ANDROID_NS, "startY")?.toFloat() ?: 0f + ), + end = Offset( + attributeOrNull(ANDROID_NS, "endX")?.toFloat() ?: 0f, + attributeOrNull(ANDROID_NS, "endY")?.toFloat() ?: 0f + ), + tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp +) + +@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") +private fun Element.parseRadialGradient() = Brush.radialGradient( + colorStops = parseColorStops(), + center = Offset( + attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f, + attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f + ), + radius = attributeOrNull(ANDROID_NS, "gradientRadius")?.toFloat() ?: 0f, + tileMode = attributeOrNull(ANDROID_NS, "tileMode")?.let(::parseTileMode) ?: TileMode.Clamp +) + +@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS") +private fun Element.parseSweepGradient() = Brush.sweepGradient( + colorStops = parseColorStops(), + center = Offset( + attributeOrNull(ANDROID_NS, "centerX")?.toFloat() ?: 0f, + attributeOrNull(ANDROID_NS, "centerY")?.toFloat() ?: 0f, + ) +) + +private fun Element.parseColorStops(): Array> { + val items = childrenSequence + .filterIsInstance() + .filter { it.nodeName == "item" } + .toList() + + val colorStops = items.mapIndexedNotNullTo(mutableListOf()) { index, item -> + item.parseColorStop(defaultOffset = index.toFloat() / items.lastIndex.coerceAtLeast(1)) + } + + if (colorStops.isEmpty()) { + val startColor = attributeOrNull(ANDROID_NS, "startColor")?.let(::parseColorValue) + val centerColor = attributeOrNull(ANDROID_NS, "centerColor")?.let(::parseColorValue) + val endColor = attributeOrNull(ANDROID_NS, "endColor")?.let(::parseColorValue) + + if (startColor != null) { + colorStops.add(0f to Color(startColor)) + } + if (centerColor != null) { + colorStops.add(0.5f to Color(centerColor)) + } + if (endColor != null) { + colorStops.add(1f to Color(endColor)) + } + } + + return colorStops.toTypedArray() +} + +private fun Element.parseColorStop(defaultOffset: Float): Pair? { + val offset = attributeOrNull(ANDROID_NS, "offset")?.toFloat() ?: defaultOffset + val color = attributeOrNull(ANDROID_NS, "color")?.let(::parseColorValue) ?: return null + return offset to Color(color) +} + +private fun Element.attributeOrNull(namespace: String, name: String): String? { + val value = getAttributeNS(namespace, name) + return if (value.isNotBlank()) value else null +} + +/** + * Attribute of an element can be represented as a separate child: + * + * + * + * + * + * + * + * instead of: + * + * + */ +private fun Element.apptAttr( + namespace: String, + name: String +): Element? { + val prefix = lookupPrefix(namespace) + return childrenSequence + .filterIsInstance() + .find { + it.namespaceURI == AAPT_NS && it.localName == "attr" && + it.getAttribute("name") == "$prefix:$name" + } +} + +private val Element.childrenSequence get() = sequence { + for (i in 0 until childNodes.length) { + yield(childNodes.item(i)) + } +} diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt new file mode 100644 index 0000000000..c4a3d03819 --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Element.kt @@ -0,0 +1,11 @@ +package org.jetbrains.compose.resources.vector.xmldom + +/** + * XML DOM Element. + */ +internal interface Element: Node { + + fun getAttributeNS(nameSpaceURI: String, localName: String): String + + fun getAttribute(name: String): String +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt new file mode 100644 index 0000000000..f8f97c113c --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/Node.kt @@ -0,0 +1,14 @@ +package org.jetbrains.compose.resources.vector.xmldom + +/** + * XML DOM Node. + */ +internal interface Node { + val nodeName: String + val localName: String + + val childNodes: NodeList + val namespaceURI: String + + fun lookupPrefix(namespaceURI: String): String +} \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt new file mode 100644 index 0000000000..db6a029e1d --- /dev/null +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/NodeList.kt @@ -0,0 +1,10 @@ +package org.jetbrains.compose.resources.vector.xmldom + +/** + * XML DOM Node list. + */ +internal interface NodeList { + fun item(i: Int): Node + + val length: Int +} \ No newline at end of file diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt index c081555e1b..6e39c00bc0 100644 --- a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/Resource.ios.kt @@ -5,9 +5,10 @@ package org.jetbrains.compose.resources - +import org.jetbrains.compose.resources.vector.xmldom.Element import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned +import org.jetbrains.compose.resources.vector.xmldom.parse import platform.Foundation.NSBundle import platform.Foundation.NSData import platform.Foundation.NSFileManager @@ -35,3 +36,5 @@ private class UIKitResourceImpl(path: String) : AbstractResourceImpl(path) { internal actual class MissingResourceException actual constructor(path: String) : Exception("Missing resource with path: $path") + +internal actual fun parseXML(byteArray: ByteArray): Element = parse(byteArray.decodeToString()) \ No newline at end of file diff --git a/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt new file mode 100644 index 0000000000..442107e5b4 --- /dev/null +++ b/components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/xmldom/DomXmlParser.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ +package org.jetbrains.compose.resources.vector.xmldom + +import platform.Foundation.* +import platform.darwin.NSObject + +internal fun parse(xml: String): Element { + val parser = DomXmlParser() + NSXMLParser((xml as NSString).dataUsingEncoding(NSUTF8StringEncoding)!!).apply { + shouldReportNamespacePrefixes = true + shouldProcessNamespaces = true + delegate = parser + }.parse() + return parser.root!! +} + +class MalformedXMLException(message: String?) : Exception(message) + +private class ElementImpl(override val localName: String, + override val nodeName: String, + override val namespaceURI: String, + val prefixMap: Map, + val attributes: Map): Element { + + override val childNodes: NodeList + get() = object : NodeList { + override fun item(i: Int): Node { + return childs[i] + } + + override val length: Int + get() = childs.size + } + + + var childs = mutableListOf() + + override fun getAttributeNS(nameSpaceURI: String, localName: String): String { + val prefix = prefixMap[nameSpaceURI] + val attrKey = if (prefix == null) localName else "$prefix:$localName" + return getAttribute(attrKey) + } + + override fun getAttribute(name: String): String = attributes[name] as String? ?:"" + + override fun lookupPrefix(uri: String): String = prefixMap[uri]?:"" +} + +@Suppress("CONFLICTING_OVERLOADS") +private class DomXmlParser( + +) : NSObject(), NSXMLParserDelegateProtocol { + + val curPrefixMapInverted = mutableMapOf() + + var curPrefixMap: Map = emptyMap() + + val nodeStack = mutableListOf() + + var root: Element? = null + + override fun parser( + parser: NSXMLParser, + didStartElement: String, + namespaceURI: String?, + qualifiedName: String?, + attributes: Map + ) { + val node = ElementImpl(didStartElement, qualifiedName!!, namespaceURI?:"", + curPrefixMap, attributes) + + if (root == null) root = node + + if (!nodeStack.isEmpty()) + nodeStack.last().childs.add(node) + + nodeStack.add(node) + } + + override fun parser( + parser: NSXMLParser, + didEndElement: String, + namespaceURI: String?, + qualifiedName: String? + ) { + val node = nodeStack.removeLast() + assert(node.localName.equals(didEndElement)) + } + + override fun parser(parser: NSXMLParser, + didStartMappingPrefix: String, + toURI: String + ) { + curPrefixMapInverted.put(didStartMappingPrefix, toURI) + curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key }) + } + + override fun parser(parser: NSXMLParser, didEndMappingPrefix: String) { + curPrefixMapInverted.remove(didEndMappingPrefix) + curPrefixMap = curPrefixMapInverted.entries.associateBy({ it.value }, { it.key }) + } + + override fun parser(parser: NSXMLParser, validationErrorOccurred: NSError) { + throw MalformedXMLException("validation error occurred") + } + + override fun parser(parser: NSXMLParser, parseErrorOccurred: NSError) { + throw MalformedXMLException("parse error occurred") + } +} + diff --git a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt index e0a78a5193..b05f8a49ad 100644 --- a/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt +++ b/components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/Resource.js.kt @@ -5,6 +5,7 @@ package org.jetbrains.compose.resources +import org.jetbrains.compose.resources.vector.xmldom.Element import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.Int8Array import org.w3c.xhr.ARRAYBUFFER @@ -42,3 +43,7 @@ private fun ArrayBuffer.toByteArray() = Int8Array(this, 0, byteLength).unsafeCas internal actual class MissingResourceException actual constructor(path: String) : Exception("Missing resource with path: $path") + +internal actual fun parseXML(byteArray: ByteArray): Element { + throw UnsupportedOperationException("XML Vector Drawables are not supported for Web target") +} \ No newline at end of file diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt new file mode 100644 index 0000000000..f4866cc5a8 --- /dev/null +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/Resource.jvmandandroid.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.resources + +import org.jetbrains.compose.resources.vector.xmldom.Element +import org.jetbrains.compose.resources.vector.xmldom.ElementImpl +import org.xml.sax.InputSource +import java.io.ByteArrayInputStream +import javax.xml.parsers.DocumentBuilderFactory + +internal actual fun parseXML(byteArray: ByteArray): Element = + ElementImpl( + DocumentBuilderFactory + .newInstance().apply { + isNamespaceAware = true + } + .newDocumentBuilder() + .parse(InputSource(ByteArrayInputStream(byteArray))) + .documentElement) diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt new file mode 100644 index 0000000000..7533f9c828 --- /dev/null +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/ElementImpl.kt @@ -0,0 +1,10 @@ +package org.jetbrains.compose.resources.vector.xmldom + +import org.w3c.dom.Element as DomElement + +internal class ElementImpl(val element: DomElement): NodeImpl(element), Element { + override fun getAttributeNS(nameSpaceURI: String, localName: String): String = + element.getAttributeNS(nameSpaceURI, localName) + + override fun getAttribute(name: String): String = element.getAttribute(name) +} \ No newline at end of file diff --git a/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt new file mode 100644 index 0000000000..5392327ba0 --- /dev/null +++ b/components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/xmldom/NodeImpl.kt @@ -0,0 +1,28 @@ +package org.jetbrains.compose.resources.vector.xmldom + +import org.w3c.dom.Node as DomNode +import org.w3c.dom.Element as DomElement + +internal open class NodeImpl(val n: DomNode): Node { + override val nodeName: String + get() = n.nodeName + override val localName: String + get() = n.localName + override val childNodes: NodeList + get() = + object: NodeList { + override fun item(i: Int): Node { + val child = n.childNodes.item(i) + return if (child is DomElement) ElementImpl(child) else NodeImpl(child) + } + + override val length: Int = n.childNodes.length + + } + + override val namespaceURI: String + get() = n.namespaceURI + + override fun lookupPrefix(namespaceURI: String): String = n.lookupPrefix(namespaceURI) + +} \ No newline at end of file diff --git a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt index fedc16c725..c1dcf6ad30 100644 --- a/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt +++ b/components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/Resource.macos.kt @@ -5,6 +5,7 @@ package org.jetbrains.compose.resources +import org.jetbrains.compose.resources.vector.xmldom.Element import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned import platform.Foundation.NSData @@ -37,3 +38,7 @@ private class MacOSResourceImpl(path: String) : AbstractResourceImpl(path) { internal actual class MissingResourceException actual constructor(path: String) : Exception("Missing resource with path: $path") + +internal actual fun parseXML(byteArray: ByteArray): Element { + throw UnsupportedOperationException("XML Vector Drawables are not supported for MacOS target") +}