Browse Source

Merge branch 'master' into support/1.6.0

pull/4382/head
dima.avdeev 9 months ago
parent
commit
e92e5838f5
  1. 141
      CHANGELOG.md
  2. 2
      benchmarks/kn-performance/gradle.properties
  3. 8
      components/AnimatedImage/demo/build.gradle.kts
  4. 8
      components/AnimatedImage/library/build.gradle.kts
  5. 10
      components/SplitPane/demo/build.gradle.kts
  6. 8
      components/SplitPane/library/build.gradle.kts
  7. 20
      components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt
  8. 14
      components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt
  9. 19
      components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt
  10. 5
      components/gradle.properties
  11. 15
      components/gradle/libs.versions.toml
  12. 2
      components/gradle/wrapper/gradle-wrapper.properties
  13. 8
      components/resources/demo/androidApp/build.gradle.kts
  14. 8
      components/resources/demo/desktopApp/build.gradle.kts
  15. 25
      components/resources/demo/shared/build.gradle.kts
  16. 1
      components/resources/demo/shared/src/androidMain/composeResources/files/platform-text.txt
  17. BIN
      components/resources/demo/shared/src/commonMain/composeResources/font/Workbench-Regular.ttf
  18. 13
      components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml
  19. 49
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt
  20. 38
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt
  21. 30
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt
  22. 1
      components/resources/demo/shared/src/desktopMain/composeResources/files/platform-text.txt
  23. 1
      components/resources/demo/shared/src/iosMain/composeResources/files/platform-text.txt
  24. 1
      components/resources/demo/shared/src/jsMain/composeResources/files/platform-text.txt
  25. 1
      components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt
  26. 1
      components/resources/demo/shared/src/wasmJsMain/composeResources/files/platform-text.txt
  27. 22
      components/resources/library/build.gradle.kts
  28. 169
      components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt
  29. 18
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt
  30. 12
      components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt
  31. 40
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  32. 156
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  33. 11
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt
  34. 6
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt
  35. 1
      components/resources/library/src/commonTest/resources/strings.xml
  36. 0
      components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt
  37. 169
      components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt
  38. 7
      components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt
  39. 1
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt
  40. 4
      components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
  41. 17
      components/resources/library/src/wasmJsTest/kotlin/org/jetbrains/compose/resources/TestUtils.wasmJs.kt
  42. 6
      components/test.sh
  43. 8
      components/ui-tooling-preview/demo/desktopApp/build.gradle.kts
  44. 26
      components/ui-tooling-preview/demo/shared/build.gradle.kts
  45. 18
      components/ui-tooling-preview/library/build.gradle.kts
  46. 34
      compose/integrations/composable-test-cases/testcases/inheritance/composableInterface/lib/src/commonMain/kotlin/Dependencies.kt
  47. 51
      compose/integrations/composable-test-cases/testcases/inheritance/composableInterface/main/src/commonTest/kotlin/CollectionOfComposablesTests.kt
  48. 2
      experimental/components/VideoPlayer/library/build.gradle.kts
  49. 1
      experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt
  50. 22
      experimental/components/build.gradle.kts
  51. 13
      experimental/components/gradle.properties
  52. 67
      experimental/components/gradle/verification-metadata.xml
  53. 2
      experimental/components/gradle/wrapper/gradle-wrapper.properties
  54. 0
      experimental/components/gradlew
  55. 20
      experimental/components/settings.gradle.kts
  56. 3
      gradle-plugins/compose/build.gradle.kts
  57. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerCompatibility.kt
  58. 23
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt
  59. 42
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt
  60. 55
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt
  61. 148
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt
  62. 158
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  63. 292
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt
  64. 4
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt
  65. 1
      gradle-plugins/compose/src/test/test-projects/application/mpp/gradle.properties
  66. 25
      gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/app/build.gradle.kts
  67. 9
      gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/app/src/commonMain/kotlin/App.kt
  68. 4
      gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/build.gradle.kts
  69. 1
      gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/gradle.properties
  70. 24
      gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/settings.gradle.kts
  71. 27
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts
  72. 229
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  73. 3
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties
  74. BIN
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/key/debug.keystore
  75. 2
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts
  76. 4
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/androidMain/AndroidManifest.xml
  77. 36
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/drawable/camelCaseName.xml
  78. 3
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml
  79. 2
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt
  80. 6
      gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt
  81. 27
      gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt
  82. 2
      gradle-plugins/gradle.properties
  83. 2
      gradle-plugins/gradle/libs.versions.toml
  84. 29
      html/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheet.kt
  85. 49
      html/core/src/jsTest/kotlin/css/AnimationTests.kt
  86. 30
      html/core/src/jsTest/kotlin/css/StyleSheetTests.kt

141
CHANGELOG.md

@ -1,3 +1,138 @@
# 1.6.0-rc01 (February 2024)
_Changes since 1.6.0-beta02_
## iOS/desktop/web
### Fixes
- [Fix "DropdownMenu performs onDismissRequest twice"](https://github.com/JetBrains/compose-multiplatform-core/pull/1057)
- [Use a large rectangle for the picture bounds in RenderNodeLayer.drawLayer to prevent clipping](https://github.com/JetBrains/compose-multiplatform-core/pull/1090)
- [Fix closing scene during scroll animation](https://github.com/JetBrains/compose-multiplatform-core/pull/1096)
- [Fix "Using `painterResource(:DrawableResource)` outside desktop Window can produce `UnsatisfiedLinkError`"](https://github.com/JetBrains/skiko/pull/866)
- [Fix "Rect::makeLTRB expected l <= r" crashes](https://github.com/JetBrains/skiko/pull/867)
- _(prerelease fix)_ [Commonize BasicTooltipBox](https://github.com/JetBrains/compose-multiplatform-core/pull/1092)
- _(prerelease fix)_ [Fix sharing composition locals with new platform layers](https://github.com/JetBrains/compose-multiplatform-core/pull/1086)
## iOS
### Fixes
- [Fixed unexpected fling animation over scrolling content](https://github.com/JetBrains/compose-multiplatform-core/pull/1039)
- _(prerelease fix)_ [Fix "Wrong scroll behaviour of LazyColumn inside HorizontalPager"](https://github.com/JetBrains/compose-multiplatform-core/pull/1097)
- _(prerelease fix)_ [Fix scene size after sending the app to background or changing orientation with open modal](https://github.com/JetBrains/compose-multiplatform-core/pull/1093)
## Desktop
### Fixes
- [Protect against `MouseInfo.getPointerInfo()` returning null in `WindowDraggableArea`](https://github.com/JetBrains/compose-multiplatform-core/pull/1049)
- [Support Rtl in `SplitPane`](https://github.com/JetBrains/compose-multiplatform/pull/4265)
- [Fix a native crash on `makeGL`](https://github.com/JetBrains/skiko/pull/869)
- _(prerelease fix)_ [Fix "Skiko RenderException" when creating `ComposePanel`](https://github.com/JetBrains/skiko/pull/858)
## Web
### Fixes
- [Add a `SystemThemeObserver` implementation for wasmJs](https://github.com/JetBrains/compose-multiplatform-core/pull/998)
- [Fix keyboard events with meta key on wasm/js targets](https://github.com/JetBrains/compose-multiplatform-core/pull/1088)
- [Added WASM to `components.uiToolingPreview` library](https://github.com/JetBrains/compose-multiplatform/pull/4286)
- [Fix "The cursor is invisible in compose web"](https://github.com/JetBrains/skiko/pull/846)
## Gradle Plugin
### Fixes
- _(prerelease fix)_ [Relocate a bundled `KotlinPoet` to the internal package](https://github.com/JetBrains/compose-multiplatform/pull/4239)
## Resource library
### Fixes
- _(prerelease fix)_ [Add a type name to the resource initializers](https://github.com/JetBrains/compose-multiplatform/pull/4240)
- _(prerelease fix)_ [Don't make resource IDs lowercased](https://github.com/JetBrains/compose-multiplatform/pull/4253)
- _(prerelease fix)_ [Clean code-gen directory if there was deleted a dependency on the res library](https://github.com/JetBrains/compose-multiplatform/pull/4257)
- _(prerelease fix)_ [Register all hierarchical compose resources in android compilation](https://github.com/JetBrains/compose-multiplatform/pull/4274)
- _(prerelease fix)_ [Fix fonts duplication in android app](https://github.com/JetBrains/compose-multiplatform/pull/4284)
## Dependencies
This version of Compose Multiplatform is based on the next Jetpack Compose libraries:
- [Compiler 1.5.8](https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.8)
- [Runtime 1.6.1](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.6.1)
- [UI 1.6.1](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.6.1)
- [Foundation 1.6.1](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.6.1)
- [Material 1.6.1](https://developer.android.com/jetpack/androidx/releases/compose-material#1.6.1)
- [Material3 1.2.0](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.2.0)
# 1.6.0-beta02 (February 2024)
_Changes since 1.6.0-beta01_
## Highlights
- [Basic accessibility support](https://github.com/JetBrains/compose-multiplatform-core/pull/1025) <sub>_iOS_</sub>
- [Popups/Dialogs can now be displayed outside a ViewController over native components on iOS by default](https://github.com/JetBrains/compose-multiplatform-core/pull/1031) <sub>_iOS_</sub>
- [Allow selecting `Text` in `SelectionContainer` by double and triple tap](https://github.com/JetBrains/compose-multiplatform-core/pull/984) <sub>_iOS_</sub>
- [Add support for text decoration line styles via PlatformTextStyle](https://github.com/JetBrains/compose-multiplatform-core/pull/999) <sub>_iOS, desktop, web_</sub>
- Bugfixes in the resource library (see below for the details)
## iOS/desktop/web
### Fixes
- [Fix "`TextLayoutResult#getLineForVerticalPosition` returns wrong value + slow performance"](https://github.com/JetBrains/compose-multiplatform-core/pull/1012)
- [Run all effects before sending synthetic events](https://github.com/JetBrains/compose-multiplatform-core/pull/1034)
- _(prerelease bug)_ [Fix the pointer icon in `SelectionContainer`](https://github.com/JetBrains/compose-multiplatform-core/pull/1014)
## iOS
### Features
- [Adjust overscroll effect params to match iOS animations](https://github.com/JetBrains/compose-multiplatform-core/pull/1010/files)
- [Add ability to change opacity for compose view](https://github.com/JetBrains/compose-multiplatform-core/pull/1022)
### Fixes
- [Fix UIKitView z-order](https://github.com/JetBrains/compose-multiplatform-core/pull/965)
- [Fix missing case for loading `SystemFont` on iOS](https://github.com/JetBrains/compose-multiplatform-core/pull/1013)
- [Fix selection container crash](https://github.com/JetBrains/compose-multiplatform-core/pull/1016)
- [Fix `WindowInfo.containerSize` without `platformLayers` flag](https://github.com/JetBrains/compose-multiplatform-core/pull/1028)
- _(prerelease fix)_ [Fix "textfield with visual transformation crashes after single tap"](https://github.com/JetBrains/compose-multiplatform-core/pull/1045)
- _(prerelease fix)_ [Fix selection handles crossed](https://github.com/JetBrains/compose-multiplatform-core/pull/1017)
- _(prerelease fix)_ [Fix CMPViewControllerMisuse error](https://github.com/JetBrains/compose-multiplatform-core/pull/1027)
- _(prerelease fix)_ [Fix selection handles with platformLayers=true](https://github.com/JetBrains/compose-multiplatform-core/pull/1023)
- _(prerelease fix)_ [Fix interaction handling for interop views](https://github.com/JetBrains/compose-multiplatform-core/pull/1032)
## Desktop
### Fixes
- [Add Arial and Consolas as backup fonts on Linux and mention font name when one fails to load](https://github.com/JetBrains/compose-multiplatform-core/pull/994)
### Breaking changes and deprecated API
- [Remove deprecated APIs in `TooltipArea` and `PointerEvent`](https://github.com/JetBrains/compose-multiplatform-core/pull/1029)
## HTML library
### Features
- [Add opportunity to use custom prefixes in `StyleSheet`](https://github.com/JetBrains/compose-multiplatform/pull/3015)
## Gradle Plugin
### Features
- [Add `ui-tooling-preview` alias](https://github.com/JetBrains/compose-multiplatform/pull/4190)
## Resource library
### Fixes
- _(prerelease fix)_ [Configure Android resources after AGP is applied and ignore hidden files in resources](https://github.com/JetBrains/compose-multiplatform/commit/3040ea85bbc81cb6d1e22d6928646509ee8b601f)
- _(prerelease fix)_ [Generate Res class if there is no common composeResource dir](https://github.com/JetBrains/compose-multiplatform/pull/4176)
- _(prerelease fix)_ [Support Res class generation in JVM only compose projects](https://github.com/JetBrains/compose-multiplatform/pull/4183)
- _(prerelease fix)_ [Support Compose resources for iOS tests](https://github.com/JetBrains/compose-multiplatform/pull/4185)
- _(prerelease fix)_ [Fix sub-module gradle properties for res class generation](https://github.com/JetBrains/compose-multiplatform/commit/ee26bf8beea595dce67fbe880aa86a8363d428ae)
- _(prerelease fix)_ [Fix Native xml parser](https://github.com/JetBrains/compose-multiplatform/pull/4207)
- _(prerelease fix)_ [Generate initializer functions in the Res file to avoid the `MethodTooLargeException`](https://github.com/JetBrains/compose-multiplatform/pull/4205)
- _(prerelease fix)_ [Improve handling of special characters in string resources](https://github.com/JetBrains/compose-multiplatform/pull/4220)
- _(prerelease fix)_ [Add a `ttf` font to the resources demo app](https://github.com/JetBrains/compose-multiplatform/commit/3c7260ea51157d423b3799bd339b682ffabdce06)
## Dependencies
This version of Compose Multiplatform is based on the next Jetpack Compose libraries:
- [Compiler 1.5.8](https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.8)
- [Runtime 1.6.0](https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.6.0)
- [UI 1.6.0](https://developer.android.com/jetpack/androidx/releases/compose-ui#1.6.0)
- [Foundation 1.6.0](https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.6.0)
- [Material 1.6.0](https://developer.android.com/jetpack/androidx/releases/compose-material#1.6.0)
- [Material3 1.2.0-rc01](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.2.0-rc01)
# 1.5.12
## Common
@ -19,16 +154,16 @@ This version of Compose Multiplatform is based on the next Jetpack Compose libra
* [Material 1.5.4](https://developer.android.com/jetpack/androidx/releases/compose-material#1.5.4)
* [Material3 1.1.2](https://developer.android.com/jetpack/androidx/releases/compose-material3#1.1.2)
# 1.6.0-beta01 (February 2024)
# 1.6.0-beta01 (January 2024)
## Highlights
- Resource library improvements ([demo project](https://github.com/JetBrains/compose-multiplatform/tree/c31c761e09212eaa13014f4d0d2a6516511f859a/gradle-plugins/compose/src/test/test-projects/misc/commonResources))
- Resource library improvements ([an example project](https://github.com/JetBrains/compose-multiplatform/tree/8ee7531c424421657842a24a5c365db53ba19e18/components/resources/demo))
- [Compile-time checking of resources through a generated `Res` class](https://github.com/JetBrains/compose-multiplatform/pull/3961)
- [Introduce top level `composeResources` dir with `drawable`, `font`, `files`, `values/strings.xml` support](https://github.com/JetBrains/compose-multiplatform/pull/4127)
- [Support for various screen densities, multiple languages and regions, and light and dark themes](https://github.com/JetBrains/compose-multiplatform/pull/4018)
- [Experimental support is available for tests in common code](https://github.com/JetBrains/compose-multiplatform-core/pull/978)
- [Compose for Web (Wasm) artifacts are available in Maven Central](https://github.com/JetBrains/compose-multiplatform-core/pull/914)
- [Compose for Web (Wasm) artifacts are available in Maven Central](https://github.com/JetBrains/compose-multiplatform-core/pull/914). **Warning**: Kotlin 1.9.21 has [an issue](https://github.com/JetBrains/compose-multiplatform/issues/4230) with web target. Use Kotlin 1.9.22.
- iOS. Native-like caret behaviour by long/single taps in textfields([1](https://github.com/JetBrains/compose-multiplatform-core/pull/913), [2](https://github.com/JetBrains/compose-multiplatform-core/pull/858))
- [Support `LineHeightStyle.Trim`](https://github.com/JetBrains/compose-multiplatform-core/pull/897)
- [Desktop. Proper clipping of `SwingPanel` interop](https://github.com/JetBrains/compose-multiplatform-core/pull/915) _(under an experimental flag, see the link)_

2
benchmarks/kn-performance/gradle.properties

@ -1,4 +1,4 @@
compose.version=1.5.1
compose.version=1.5.10
kotlin.version=1.9.20
org.gradle.jvmargs=-Xmx3g
kotlin.native.useEmbeddableCompilerJar=true

8
components/AnimatedImage/demo/build.gradle.kts

@ -11,11 +11,9 @@ kotlin {
optIn("kotlin.RequiresOptIn")
}
}
val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":AnimatedImage:library"))
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":AnimatedImage:library"))
}
}
}

8
components/AnimatedImage/library/build.gradle.kts

@ -12,11 +12,9 @@ kotlin {
optIn("kotlin.RequiresOptIn")
}
}
val commonMain by getting {
dependencies {
api(compose.runtime)
api(compose.foundation)
}
commonMain.dependencies {
api(compose.runtime)
api(compose.foundation)
}
}
}

10
components/SplitPane/demo/build.gradle.kts

@ -1,5 +1,3 @@
import org.jetbrains.compose.compose
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
@ -14,11 +12,9 @@ kotlin {
}
}
val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":SplitPane:library"))
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":SplitPane:library"))
}
}
}

8
components/SplitPane/library/build.gradle.kts

@ -14,11 +14,9 @@ kotlin {
}
}
val commonMain by getting {
dependencies {
api(compose.runtime)
api(compose.foundation)
}
commonMain.dependencies {
api(compose.runtime)
api(compose.foundation)
}
}
}

20
components/SplitPane/library/src/commonMain/kotlin/org/jetbrains/compose/splitpane/SplitPaneDSL.kt

@ -4,8 +4,11 @@ import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
/** Receiver scope which is used by [HorizontalSplitPane] and [VerticalSplitPane] */
@ -83,12 +86,17 @@ interface SplitterScope {
internal class HandleScopeImpl(
private val containerScope: SplitPaneScopeImpl
) : HandleScope {
override fun Modifier.markAsHandle(): Modifier = this.pointerInput(containerScope.splitPaneState) {
detectDragGestures { change, _ ->
change.consume()
containerScope.splitPaneState.dispatchRawMovement(
if (containerScope.isHorizontal) change.position.x else change.position.y
)
override fun Modifier.markAsHandle(): Modifier = composed {
val layoutDirection = LocalLayoutDirection.current
pointerInput(containerScope.splitPaneState) {
detectDragGestures { change, _ ->
change.consume()
containerScope.splitPaneState.dispatchRawMovement(
if (containerScope.isHorizontal)
if (layoutDirection == LayoutDirection.Ltr) change.position.x else -change.position.x
else change.position.y
)
}
}
}
}

14
components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitPane.kt

@ -121,18 +121,18 @@ internal actual fun SplitPane(
}
layout(constraints.maxWidth, constraints.maxHeight) {
firstPlaceable.place(0, 0)
firstPlaceable.placeRelative(0, 0)
if (isHorizontal) {
secondPlaceable.place(secondPlaceablePosition, 0)
splitterPlaceable.place(position, 0)
secondPlaceable.placeRelative(secondPlaceablePosition, 0)
splitterPlaceable.placeRelative(position, 0)
if (moveEnabled) {
handlePlaceable.place(handlePosition, 0)
handlePlaceable.placeRelative(handlePosition, 0)
}
} else {
secondPlaceable.place(0, secondPlaceablePosition)
splitterPlaceable.place(0, position)
secondPlaceable.placeRelative(0, secondPlaceablePosition)
splitterPlaceable.placeRelative(0, position)
if (moveEnabled) {
handlePlaceable.place(0, handlePosition)
handlePlaceable.placeRelative(0, handlePosition)
}
}
}

19
components/SplitPane/library/src/desktopMain/kotlin/org/jetbrains/compose/splitpane/DesktopSplitter.kt

@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import java.awt.Cursor
@ -20,12 +22,17 @@ private fun DesktopHandle(
splitPaneState: SplitPaneState
) = Box(
Modifier
.pointerInput(splitPaneState) {
detectDragGestures { change, _ ->
change.consumeAllChanges()
splitPaneState.dispatchRawMovement(
if (isHorizontal) change.position.x else change.position.y
)
.run {
val layoutDirection = LocalLayoutDirection.current
pointerInput(splitPaneState) {
detectDragGestures { change, _ ->
change.consume()
splitPaneState.dispatchRawMovement(
if (isHorizontal)
if (layoutDirection == LayoutDirection.Ltr) change.position.x else -change.position.x
else change.position.y
)
}
}
}
.cursorForHorizontalResize(isHorizontal)

5
components/gradle.properties

@ -1,13 +1,13 @@
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
#org.gradle.configuration-cache=true //stable since kotlin 1.9.20
org.gradle.configuration-cache=true
#Android
android.useAndroidX=true
#Versions
kotlin.version=1.9.21
kotlin.version=1.9.22
compose.version=1.6.0-dev1397
agp.version=8.1.2
@ -23,5 +23,4 @@ kotlin.code.style=official
kotlin.js.compiler=ir
kotlin.js.webpack.major.version=4
kotlin.native.useEmbeddableCompilerJar=true
kotlin.native.binary.memoryModel=experimental
xcodeproj=./resources/demo/iosApp

15
components/gradle/libs.versions.toml

@ -1,7 +1,16 @@
[versions]
kotlinx-coroutines = "1.8.0-RC"
kotlinx-coroutines = "1.7.3"
androidx-appcompat = "1.6.1"
androidx-activity-compose = "1.8.2"
androidx-test = "1.5.0"
androidx-compose = "1.6.0"
[libraries]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" }
androidx-compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "androidx-compose" }
androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-compose" }
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-compose" }

2
components/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

8
components/resources/demo/androidApp/build.gradle.kts

@ -19,10 +19,10 @@ android {
targetCompatibility = JavaVersion.VERSION_11
}
dependencies {
implementation(compose.ui)
implementation(compose.foundation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
implementation(project(":resources:demo:shared"))
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-compose:1.8.0")
implementation("androidx.compose.foundation:foundation:1.5.3")
implementation("androidx.compose.ui:ui:1.5.3")
}
}

8
components/resources/demo/desktopApp/build.gradle.kts

@ -6,11 +6,9 @@ plugins {
kotlin {
jvm()
sourceSets {
val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":resources:demo:shared"))
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":resources:demo:shared"))
}
}
}

25
components/resources/demo/shared/build.gradle.kts

@ -1,4 +1,3 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
@ -8,8 +7,6 @@ plugins {
}
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
targetHierarchy.default()
androidTarget {
compilations.all {
kotlinOptions {
@ -59,17 +56,14 @@ kotlin {
optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
}
}
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(project(":resources:library"))
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(project(":resources:library"))
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.common)
}
val desktopMain by getting
desktopMain.dependencies {
implementation(compose.desktop.common)
}
}
}
@ -89,8 +83,3 @@ android {
compose.experimental {
web.application {}
}
// TODO: remove this block after we update on a newer kotlin. Currently there is an error: `error:0308010C:digital envelope routines::unsupported`
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}

1
components/resources/demo/shared/src/androidMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
Android platform

BIN
components/resources/demo/shared/src/commonMain/composeResources/font/Workbench-Regular.ttf

Binary file not shown.

13
components/resources/demo/shared/src/commonMain/composeResources/values/strings.xml

@ -1,13 +1,12 @@
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="multi_line">Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</string>
<string name="str_template">Hello, %1$s! You have %2$d new messages.</string>
<string name="multi_line">Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</string>
<string name="str_template">Hello, %1$s!\nYou have %2$d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
<item>item \u2605</item>
<item>item \u2318</item>
<item>item \u00BD</item>
</string-array>
</resources>

49
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FileRes.kt

@ -1,22 +1,11 @@
package org.jetbrains.compose.resources.demo.shared
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
@ -59,5 +48,37 @@ fun FileRes(paddingValues: PaddingValues) {
Text(bytes.decodeToString())
""".trimIndent()
)
Text(
modifier = Modifier.padding(16.dp),
text = "File: 'files/platform-text.txt'",
style = MaterialTheme.typography.titleLarge
)
OutlinedCard(
modifier = Modifier.padding(horizontal = 16.dp),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
var bytes by remember { mutableStateOf(ByteArray(0)) }
LaunchedEffect(Unit) {
bytes = Res.readBytes("files/platform-text.txt")
}
Text(
modifier = Modifier.padding(8.dp),
text = bytes.decodeToString(),
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Text(
modifier = Modifier.padding(16.dp),
text = """
var bytes by remember {
mutableStateOf(ByteArray(0))
}
LaunchedEffect(Unit) {
bytes = Res.readFileBytes("files/platform-text.txt")
}
Text(bytes.decodeToString())
""".trimIndent()
)
}
}

38
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt

@ -29,13 +29,37 @@ fun FontRes(paddingValues: PaddingValues) {
Text(
modifier = Modifier.padding(8.dp),
text = """
val fontAwesome = FontFamily(Font(Res.fonts.font_awesome))
val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6)
Text(
modifier = Modifier.padding(16.dp),
fontFamily = fontAwesome,
fontFamily = FontFamily(Font(Res.font.workbench_regular)),
style = MaterialTheme.typography.headlineLarge,
text = symbols.joinToString(" ") { it.toChar().toString() }
text = "brown fox jumps over the lazy dog"
)
""".trimIndent(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
softWrap = false
)
}
Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(Font(Res.font.Workbench_Regular)),
style = MaterialTheme.typography.headlineLarge,
text = "brown fox jumps over the lazy dog"
)
OutlinedCard(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Text(
modifier = Modifier.padding(8.dp),
text = """
Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(Font(Res.font.font_awesome)),
style = MaterialTheme.typography.headlineLarge,
text ="\uf1ba \uf238 \uf21a \uf1bb \uf1b8 \uf09b \uf269 \uf1d0 \uf15a \uf293 \uf1c6"
)
""".trimIndent(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
@ -43,13 +67,11 @@ fun FontRes(paddingValues: PaddingValues) {
)
}
val fontAwesome = FontFamily(Font(Res.font.font_awesome))
val symbols = arrayOf(0xf1ba, 0xf238, 0xf21a, 0xf1bb, 0xf1b8, 0xf09b, 0xf269, 0xf1d0, 0xf15a, 0xf293, 0xf1c6)
Text(
modifier = Modifier.padding(16.dp),
fontFamily = fontAwesome,
fontFamily = FontFamily(Font(Res.font.font_awesome)),
style = MaterialTheme.typography.headlineLarge,
text = symbols.joinToString(" ") { it.toChar().toString() }
text ="\uf1ba \uf238 \uf21a \uf1bb \uf1b8 \uf09b \uf269 \uf1d0 \uf15a \uf293 \uf1c6"
)
}
}

30
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/StringRes.kt

@ -1,30 +1,16 @@
package org.jetbrains.compose.resources.demo.shared
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.stringArrayResource
import org.jetbrains.compose.resources.readResourceBytes
import org.jetbrains.compose.resources.stringResource
@Composable
fun StringRes(paddingValues: PaddingValues) {
@ -56,7 +42,7 @@ fun StringRes(paddingValues: PaddingValues) {
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringResource(Res.string.app_name),
onValueChange = {},
label = { Text("Text(stringResource(Res.string.app_name)") },
label = { Text("Text(stringResource(Res.string.app_name))") },
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -68,7 +54,7 @@ fun StringRes(paddingValues: PaddingValues) {
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringResource(Res.string.hello),
onValueChange = {},
label = { Text("Text(stringResource(Res.string.hello)") },
label = { Text("Text(stringResource(Res.string.hello))") },
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -80,7 +66,7 @@ fun StringRes(paddingValues: PaddingValues) {
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringResource(Res.string.multi_line),
onValueChange = {},
label = { Text("Text(stringResource(Res.string.multi_line)") },
label = { Text("Text(stringResource(Res.string.multi_line))") },
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,
@ -92,7 +78,7 @@ fun StringRes(paddingValues: PaddingValues) {
modifier = Modifier.padding(16.dp).fillMaxWidth(),
value = stringResource(Res.string.str_template, "User_name", 100),
onValueChange = {},
label = { Text("Text(stringResource(Res.string.str_template, \"User_name\", 100)") },
label = { Text("Text(stringResource(Res.string.str_template, \"User_name\", 100))") },
enabled = false,
colors = TextFieldDefaults.colors(
disabledTextColor = MaterialTheme.colorScheme.onSurface,

1
components/resources/demo/shared/src/desktopMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
Desktop platform

1
components/resources/demo/shared/src/iosMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
iOS platform

1
components/resources/demo/shared/src/jsMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
JS platform

1
components/resources/demo/shared/src/macosMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
macOS platform

1
components/resources/demo/shared/src/wasmJsMain/composeResources/files/platform-text.txt

@ -0,0 +1 @@
WasmJS platform

22
components/resources/library/build.gradle.kts

@ -1,3 +1,4 @@
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
@ -11,8 +12,6 @@ plugins {
val composeVersion = extra["compose.version"] as String
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
targetHierarchy.default()
jvm("desktop")
androidTarget {
publishLibraryVariants("release")
@ -47,6 +46,7 @@ kotlin {
macosX64()
macosArm64()
applyDefaultHierarchyTemplate()
sourceSets {
all {
languageSettings {
@ -74,8 +74,11 @@ kotlin {
}
val commonTest by getting {
dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
implementation(compose.material3)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.uiTest)
}
}
val blockingMain by creating {
@ -92,9 +95,6 @@ kotlin {
}
val jvmAndAndroidMain by creating {
dependsOn(blockingMain)
dependencies {
implementation(compose.material3)
}
}
val jvmAndAndroidTest by creating {
dependsOn(blockingTest)
@ -108,8 +108,6 @@ kotlin {
dependsOn(jvmAndAndroidTest)
dependencies {
implementation(compose.desktop.currentOs)
implementation(compose.desktop.uiTestJUnit4)
implementation(libs.kotlinx.coroutines.swing)
}
}
val androidMain by getting {
@ -118,10 +116,10 @@ kotlin {
val androidInstrumentedTest by getting {
dependsOn(jvmAndAndroidTest)
dependencies {
implementation("androidx.test:core:1.5.0")
implementation("androidx.compose.ui:ui-test-manifest:1.5.4")
implementation("androidx.compose.ui:ui-test:1.5.4")
implementation("androidx.compose.ui:ui-test-junit4:1.5.4")
implementation(libs.androidx.test.core)
implementation(libs.androidx.compose.ui.test)
implementation(libs.androidx.compose.ui.test.manifest)
implementation(libs.androidx.compose.ui.test.junit4)
}
}
val androidUnitTest by getting {

169
components/resources/library/src/androidInstrumentedTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.android.kt

@ -1,169 +0,0 @@
package org.jetbrains.compose.resources
import androidx.compose.foundation.Image
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class, InternalResourceApi::class)
class ComposeResourceTest {
@Before
fun dropCaches() {
dropStringsCache()
dropImageCache()
}
@Before
fun configureTestEnvironment() {
getResourceEnvironment = ::getTestEnvironment
}
@Test
fun testCountRecompositions() = runComposeUiTest {
runBlockingTest {
val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
val recompositionsCounter = RecompositionsCounter()
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
val res by imagePathFlow.collectAsState()
val imgRes = imageResource(res)
recompositionsCounter.content {
Image(bitmap = imgRes, contentDescription = null)
}
}
}
awaitIdle()
imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle()
assertEquals(2, recompositionsCounter.count)
}
}
@Test
fun testImageResourceCache() = runComposeUiTest {
runBlockingTest {
val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
val res by imagePathFlow.collectAsState()
Image(painterResource(res), null)
}
}
awaitIdle()
imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle()
imagePathFlow.emit(DrawableResource("1.png"))
awaitIdle()
assertEquals(
expected = listOf("1.png", "2.png"), //no second read of 1.png
actual = testResourceReader.readPaths
)
}
}
@Test
fun testStringResourceCache() = runComposeUiTest {
runBlockingTest {
val testResourceReader = TestResourceReader()
val stringIdFlow = MutableStateFlow(TestStringResource("app_name"))
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
val res by stringIdFlow.collectAsState()
Text(stringResource(res))
Text(stringArrayResource(TestStringResource("str_arr")).joinToString())
}
}
awaitIdle()
stringIdFlow.emit(TestStringResource("hello"))
awaitIdle()
stringIdFlow.emit(TestStringResource("app_name"))
awaitIdle()
assertEquals(
expected = listOf("strings.xml"), //just one string.xml read
actual = testResourceReader.readPaths
)
}
}
@Test
fun testReadStringResource() = runComposeUiTest {
runBlockingTest {
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
assertEquals(
"Compose Resources App",
stringResource(TestStringResource("app_name"))
)
assertEquals(
"Hello, test-name! You have 42 new messages.",
stringResource(TestStringResource("str_template"), "test-name", 42)
)
assertEquals(
listOf("item 1", "item 2", "item 3"),
stringArrayResource(TestStringResource("str_arr"))
)
}
}
awaitIdle()
}
}
@Test
fun testLoadStringResource() = runBlockingTest {
kotlin.test.assertEquals("Compose Resources App", getString(TestStringResource("app_name")))
kotlin.test.assertEquals(
"Hello, test-name! You have 42 new messages.",
getString(TestStringResource("str_template"), "test-name", 42)
)
kotlin.test.assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
}
@Test
fun testMissingResource() = runBlockingTest {
assertFailsWith<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
getString(TestStringResource("unknown_id"))
}
kotlin.test.assertEquals("String ID=`unknown_id` is not found!", error.message)
}
@Test
fun testReadFileResource() = runBlockingTest {
val bytes = readResourceBytes("strings.xml")
kotlin.test.assertEquals(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
}

18
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt

@ -0,0 +1,18 @@
package org.jetbrains.compose.resources
import java.io.File
private object AndroidResourceReader
@OptIn(ExperimentalResourceApi::class)
@InternalResourceApi
actual suspend fun readResourceBytes(path: String): ByteArray {
val classLoader = Thread.currentThread().contextClassLoader ?: AndroidResourceReader.javaClass.classLoader
val resource = classLoader.getResourceAsStream(path) ?: run {
//try to find a font in the android assets
if (File(path).parentFile?.name.orEmpty() == "font") {
classLoader.getResourceAsStream("assets/$path")
} else null
} ?: throw MissingResourceException(path)
return resource.readBytes()
}

12
components/resources/library/src/blockingTest/kotlin/org/jetbrains/compose/resources/TestUtils.blocking.kt

@ -1,12 +0,0 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
actual typealias TestReturnType = Unit
actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): TestReturnType {
return runBlocking { block() }
}

40
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt

@ -20,7 +20,7 @@ private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")
@ExperimentalResourceApi
@Immutable
class StringResource
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>): Resource(id, items)
@InternalResourceApi constructor(id: String, val key: String, items: Set<ResourceItem>) : Resource(id, items)
private sealed interface StringItem {
data class Value(val text: String) : StringItem
@ -53,11 +53,13 @@ private suspend fun getParsedStrings(
private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map<String, StringItem> {
val nodes = resourceReader.read(path).toXmlElement().childNodes
val strings = nodes.getElementsWithName("string").associate { element ->
element.getAttribute("name") to StringItem.Value(element.textContent.orEmpty())
val rawString = element.textContent.orEmpty()
element.getAttribute("name") to StringItem.Value(handleSpecialCharacters(rawString))
}
val arrays = nodes.getElementsWithName("string-array").associate { arrayElement ->
val items = arrayElement.childNodes.getElementsWithName("item").map { element ->
element.textContent.orEmpty()
val rawString = element.textContent.orEmpty()
handleSpecialCharacters(rawString)
}
arrayElement.getAttribute("name") to StringItem.Array(items)
}
@ -203,4 +205,34 @@ private suspend fun loadStringArray(
private fun NodeList.getElementsWithName(name: String): List<Element> =
List(length) { item(it) }
.filterIsInstance<Element>()
.filter { it.localName == name }
.filter { it.localName == name }
//https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes
/**
* Replaces
*
* '\n' -> new line
*
* '\t' -> tab
*
* '\uXXXX' -> unicode symbol
*
* '\\' -> '\'
*
* @param string The input string to handle.
* @return The string with special characters replaced according to the logic.
*/
internal fun handleSpecialCharacters(string: String): String {
val unicodeNewLineTabRegex = Regex("""\\u[a-fA-F\d]{4}|\\n|\\t""")
val doubleSlashRegex = Regex("""\\\\""")
val doubleSlashIndexes = doubleSlashRegex.findAll(string).map { it.range.first }
val handledString = unicodeNewLineTabRegex.replace(string) { matchResult ->
if (doubleSlashIndexes.contains(matchResult.range.first - 1)) matchResult.value
else when (matchResult.value) {
"\\n" -> "\n"
"\\t" -> "\t"
else -> matchResult.value.substring(2).toInt(16).toChar().toString()
}
}.replace("""\\""", """\""")
return handledString
}

156
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt

@ -0,0 +1,156 @@
package org.jetbrains.compose.resources
import androidx.compose.foundation.Image
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.test.runTest
import kotlin.test.*
@OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class, InternalResourceApi::class)
class ComposeResourceTest {
init {
dropStringsCache()
dropImageCache()
getResourceEnvironment = ::getTestEnvironment
}
@Test
fun testCountRecompositions() = runComposeUiTest {
var res by mutableStateOf(DrawableResource("1.png"))
val recompositionsCounter = RecompositionsCounter()
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
val imgRes = imageResource(res)
recompositionsCounter.content {
Image(bitmap = imgRes, contentDescription = null)
}
}
}
waitForIdle()
res = DrawableResource("2.png")
waitForIdle()
assertEquals(2, recompositionsCounter.count)
}
@Test
fun testImageResourceCache() = runComposeUiTest {
val testResourceReader = TestResourceReader()
var res by mutableStateOf(DrawableResource("1.png"))
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
Image(painterResource(res), null)
}
}
waitForIdle()
res = DrawableResource("2.png")
waitForIdle()
res = DrawableResource("1.png")
waitForIdle()
assertEquals(
expected = listOf("1.png", "2.png"), //no second read of 1.png
actual = testResourceReader.readPaths
)
}
@Test
fun testStringResourceCache() = runComposeUiTest {
val testResourceReader = TestResourceReader()
var res by mutableStateOf(TestStringResource("app_name"))
var str = ""
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
str = stringResource(res)
Text(str)
Text(stringArrayResource(TestStringResource("str_arr")).joinToString())
}
}
waitForIdle()
assertEquals(str, "Compose Resources App")
res = TestStringResource("hello")
waitForIdle()
assertEquals(str, "\uD83D\uDE0A Hello world!")
res = TestStringResource("app_name")
waitForIdle()
assertEquals(str, "Compose Resources App")
assertEquals(
expected = listOf("strings.xml"), //just one string.xml read
actual = testResourceReader.readPaths
)
}
@Test
fun testReadStringResource() = runComposeUiTest {
var app_name = ""
var accentuated_characters = ""
var str_template = ""
var str_arr = emptyList<String>()
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
app_name = stringResource(TestStringResource("app_name"))
accentuated_characters = stringResource(TestStringResource("accentuated_characters"))
str_template = stringResource(TestStringResource("str_template"), "test-name", 42)
str_arr = stringArrayResource(TestStringResource("str_arr"))
}
}
waitForIdle()
assertEquals("Compose Resources App", app_name)
assertEquals("Créer une table", accentuated_characters)
assertEquals("Hello, test-name! You have 42 new messages.", str_template)
assertEquals(listOf("item 1", "item 2", "item 3"), str_arr)
}
@Test
fun testLoadStringResource() = runTest {
assertEquals("Compose Resources App", getString(TestStringResource("app_name")))
assertEquals(
"Hello, test-name! You have 42 new messages.",
getString(TestStringResource("str_template"), "test-name", 42)
)
assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
}
@Test
fun testMissingResource() = runTest {
assertFailsWith<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
getString(TestStringResource("unknown_id"))
}
assertEquals("String ID=`unknown_id` is not found!", error.message)
}
@Test
fun testReadFileResource() = runTest {
val bytes = readResourceBytes("strings.xml")
assertEquals(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="accentuated_characters">Créer une table</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
}

11
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ResourceTest.kt

@ -13,12 +13,12 @@ import kotlin.test.*
@OptIn(ExperimentalResourceApi::class, InternalResourceApi::class)
class ResourceTest {
@Test
fun testResourceEquals() = runBlockingTest {
fun testResourceEquals() {
assertEquals(DrawableResource("a"), DrawableResource("a"))
}
@Test
fun testResourceNotEquals() = runBlockingTest {
fun testResourceNotEquals() {
assertNotEquals(DrawableResource("a"), DrawableResource("b"))
}
@ -98,6 +98,13 @@ class ResourceTest {
}.message.let { msg ->
assertEquals("Resource with ID='ImageResource:test3' has more than one file: en1, en2", msg)
}
}
@Test
fun testEscapedSymbols() {
assertEquals(
"abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \\u355g",
handleSpecialCharacters("""abc \n \\n \t \\t \u1234 \ua45f \\u1234 \\ \u355g""")
)
}
}

6
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestUtils.kt

@ -1,11 +1,5 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.CoroutineScope
expect class TestReturnType
expect fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): TestReturnType
@OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
internal fun TestStringResource(key: String) = StringResource(
"STRING:$key",

1
components/resources/library/src/commonTest/resources/strings.xml

@ -1,6 +1,7 @@
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="accentuated_characters">Créer une table</string>
<string name="str_template">Hello, %1$s! You have %2$d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>

0
components/resources/library/src/jvmAndAndroidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvmAndAndroid.kt → components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt

169
components/resources/library/src/desktopTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.desktop.kt

@ -1,169 +0,0 @@
package org.jetbrains.compose.resources
import androidx.compose.foundation.Image
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class, InternalResourceApi::class)
class ComposeResourceTest {
@Before
fun dropCaches() {
dropStringsCache()
dropImageCache()
}
@Before
fun configureTestEnvironment() {
getResourceEnvironment = ::getTestEnvironment
}
@Test
fun testCountRecompositions() = runComposeUiTest {
runBlockingTest {
val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
val recompositionsCounter = RecompositionsCounter()
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
val res by imagePathFlow.collectAsState()
val imgRes = imageResource(res)
recompositionsCounter.content {
Image(bitmap = imgRes, contentDescription = null)
}
}
}
awaitIdle()
imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle()
assertEquals(2, recompositionsCounter.count)
}
}
@Test
fun testImageResourceCache() = runComposeUiTest {
runBlockingTest {
val testResourceReader = TestResourceReader()
val imagePathFlow = MutableStateFlow(DrawableResource("1.png"))
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
val res by imagePathFlow.collectAsState()
Image(painterResource(res), null)
}
}
awaitIdle()
imagePathFlow.emit(DrawableResource("2.png"))
awaitIdle()
imagePathFlow.emit(DrawableResource("1.png"))
awaitIdle()
assertEquals(
expected = listOf("1.png", "2.png"), //no second read of 1.png
actual = testResourceReader.readPaths
)
}
}
@Test
fun testStringResourceCache() = runComposeUiTest {
runBlockingTest {
val testResourceReader = TestResourceReader()
val stringIdFlow = MutableStateFlow(TestStringResource("app_name"))
setContent {
CompositionLocalProvider(
LocalResourceReader provides testResourceReader,
LocalComposeEnvironment provides TestComposeEnvironment
) {
val res by stringIdFlow.collectAsState()
Text(stringResource(res))
Text(stringArrayResource(TestStringResource("str_arr")).joinToString())
}
}
awaitIdle()
stringIdFlow.emit(TestStringResource("hello"))
awaitIdle()
stringIdFlow.emit(TestStringResource("app_name"))
awaitIdle()
assertEquals(
expected = listOf("strings.xml"), //just one string.xml read
actual = testResourceReader.readPaths
)
}
}
@Test
fun testReadStringResource() = runComposeUiTest {
runBlockingTest {
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
assertEquals(
"Compose Resources App",
stringResource(TestStringResource("app_name"))
)
assertEquals(
"Hello, test-name! You have 42 new messages.",
stringResource(TestStringResource("str_template"), "test-name", 42)
)
assertEquals(
listOf("item 1", "item 2", "item 3"),
stringArrayResource(TestStringResource("str_arr"))
)
}
}
awaitIdle()
}
}
@Test
fun testLoadStringResource() = runBlockingTest {
kotlin.test.assertEquals("Compose Resources App", getString(TestStringResource("app_name")))
kotlin.test.assertEquals(
"Hello, test-name! You have 42 new messages.",
getString(TestStringResource("str_template"), "test-name", 42)
)
kotlin.test.assertEquals(listOf("item 1", "item 2", "item 3"), getStringArray(TestStringResource("str_arr")))
}
@Test
fun testMissingResource() = runBlockingTest {
assertFailsWith<MissingResourceException> {
readResourceBytes("missing.png")
}
val error = assertFailsWith<IllegalStateException> {
getString(TestStringResource("unknown_id"))
}
kotlin.test.assertEquals("String ID=`unknown_id` is not found!", error.message)
}
@Test
fun testReadFileResource() = runBlockingTest {
val bytes = readResourceBytes("strings.xml")
kotlin.test.assertEquals(
"""
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="str_template">Hello, %1${'$'}s! You have %2${'$'}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
</resources>
""".trimIndent(),
bytes.decodeToString()
)
}
}

7
components/resources/library/src/jsTest/kotlin/org/jetbrains/compose/resources/TestUtils.js.kt

@ -1,7 +0,0 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.*
actual typealias TestReturnType = Any
actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): TestReturnType =
TODO("Implement if necessary. We focus on k/wasm target for now")

1
components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt

@ -13,6 +13,7 @@ actual suspend fun readResourceBytes(path: String): ByteArray {
//todo in future bundle resources with app and use all sourceSets (skikoMain, nativeMain)
contentsAtPath("$currentDirectoryPath/src/macosMain/resources/$path")
?: contentsAtPath("$currentDirectoryPath/src/commonMain/resources/$path")
?: contentsAtPath("$currentDirectoryPath/src/commonTest/resources/$path")
} ?: throw MissingResourceException(path)
return ByteArray(contentsAtPath.length.toInt()).apply {
usePinned {

4
components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt

@ -94,7 +94,9 @@ private class DomXmlParser : NSObject(), NSXMLParserDelegateProtocol {
}
override fun parser(parser: NSXMLParser, foundCharacters: String) {
nodeStack.lastOrNull()?.textContent = foundCharacters
nodeStack.lastOrNull()?.let { node ->
node.textContent = node.textContent.orEmpty() + foundCharacters
}
}
override fun parser(

17
components/resources/library/src/wasmJsTest/kotlin/org/jetbrains/compose/resources/TestUtils.wasmJs.kt

@ -1,17 +0,0 @@
package org.jetbrains.compose.resources
import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
@JsFun("() => ''")
private external fun jsRef(): JsAny
actual typealias TestReturnType = Any
/**
* Runs the [block] in a coroutine.
*/
actual fun runBlockingTest(block: suspend CoroutineScope.() -> Unit): TestReturnType = MainScope().promise {
block()
jsRef()
}

6
components/test.sh

@ -2,10 +2,6 @@
cd "$(dirname "$0")" # Run always in current dir
set -euo pipefail # Fail fast
# Unit tests
./gradlew :resources:library:test
./gradlew :resources:library:desktopTest
# Android integration tests
./gradlew :resources:library:pixel5DebugAndroidTest
./gradlew :resources:library:iosSimulatorArm64Test

8
components/ui-tooling-preview/demo/desktopApp/build.gradle.kts

@ -6,11 +6,9 @@ plugins {
kotlin {
jvm()
sourceSets {
val jvmMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":ui-tooling-preview:demo:shared"))
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":ui-tooling-preview:demo:shared"))
}
}
}

26
components/ui-tooling-preview/demo/shared/build.gradle.kts

@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
plugins {
kotlin("multiplatform")
id("com.android.library")
@ -7,8 +5,6 @@ plugins {
}
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
targetHierarchy.default()
androidTarget {
compilations.all {
kotlinOptions {
@ -48,17 +44,14 @@ kotlin {
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(project(":ui-tooling-preview:library"))
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.material3)
implementation(project(":ui-tooling-preview:library"))
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.common)
}
val desktopMain by getting
desktopMain.dependencies {
implementation(compose.desktop.common)
}
}
}
@ -78,8 +71,3 @@ android {
compose.experimental {
web.application {}
}
// TODO: remove this block after we update on a newer kotlin. Currently there is an error: `error:0308010C:digital envelope routines::unsupported`
rootProject.plugins.withType<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin> {
rootProject.the<org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension>().nodeVersion = "16.0.0"
}

18
components/ui-tooling-preview/library/build.gradle.kts

@ -1,4 +1,5 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
kotlin("multiplatform")
@ -9,8 +10,6 @@ plugins {
val composeVersion = extra["compose.version"] as String
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
targetHierarchy.default()
jvm("desktop")
androidTarget {
publishLibraryVariants("release")
@ -25,9 +24,11 @@ kotlin {
iosSimulatorArm64()
js {
browser {
testTask(Action {
enabled = false
})
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser {
}
}
macosX64()
@ -53,3 +54,10 @@ configureMavenPublication(
name = "Experimental Compose Multiplatform tooling library API. This library provides the API required to declare " +
"@Preview composables in user apps."
)
afterEvaluate {
// TODO(o.k.): remove this after we refactor jsAndWasmMain source set in skiko to get rid of broken "common" js-interop
tasks.configureEach {
if (name == "compileWebMainKotlinMetadata") enabled = false
}
}

34
compose/integrations/composable-test-cases/testcases/inheritance/composableInterface/lib/src/commonMain/kotlin/Dependencies.kt

@ -1,4 +1,6 @@
import androidx.compose.runtime.Composable
import com.example.common.TextLeafNode
import kotlin.jvm.JvmInline
interface ComposableContent {
@ -14,3 +16,35 @@ interface CollectionOfComposable {
fun iterator(): Iterator<@Composable () -> Unit>
}
interface DefaultComposableContent {
@Composable
@Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE")
fun ComposableContent(
any: String = "any" // default value is required to reproduce
) {
TextLeafNode("DefaultComposableContent - $any")
}
}
abstract class AbstractGreeter {
@Composable
protected abstract fun Greeting()
@Composable
fun Hi() {
Greeting()
}
}
class Greeter(val target: String) : AbstractGreeter() {
@Composable
override fun Greeting() {
TextLeafNode("Hello, $target!")
}
}
@JvmInline
value class ValClass(val key: Int) {
constructor(a: Int, b: Int) : this(a + b)
}

51
compose/integrations/composable-test-cases/testcases/inheritance/composableInterface/main/src/commonTest/kotlin/CollectionOfComposablesTests.kt

@ -1,3 +1,4 @@
import androidx.compose.runtime.Composable
import com.example.common.TextContainerNode
import com.example.common.TextLeafNode
import com.example.common.composeText
@ -37,4 +38,54 @@ class CollectionOfComposablesTests {
actual = root.dump()
)
}
/** Default args for overridden composable produces corrupted function definitions
* https://github.com/JetBrains/compose-multiplatform/issues/3318
*/
@Test
fun testDefaultArgsForOverridden() = runTest {
class Impl : DefaultComposableContent
val root = composeText {
Impl().ComposableContent()
}
assertEquals(
expected = "root:{DefaultComposableContent - any}",
actual = root.dump()
)
}
/** Override a protected @Composable method leads to Compilation Failed on iOS target
* https://github.com/JetBrains/compose-multiplatform/issues/4055
*/
@Test
fun testOverrideProtected() = runTest {
val root = composeText {
Greeter("Bob").Hi()
}
assertEquals(
expected = "root:{Hello, Bob!}",
actual = root.dump()
)
}
/** Default params for value type defined in separate module may result in compilation failure on iOS
* https://github.com/JetBrains/compose-multiplatform/issues/3643
*/
@Test
fun testDefaultParamValueClass() = runTest {
@Composable
fun test(qualifiers: ValClass = ValClass(123)): String = "${qualifiers.key}"
val root = composeText { TextLeafNode(test()) }
assertEquals(
expected = "root:{123}",
actual = root.dump()
)
}
}

2
experimental/components/VideoPlayer/library/build.gradle.kts

@ -18,7 +18,7 @@ kotlin {
}
named("desktopMain") {
dependencies {
implementation("uk.co.caprica:vlcj:4.7.0")
implementation("uk.co.caprica:vlcj:4.8.2")
}
}
}

1
experimental/components/VideoPlayer/library/src/commonMain/kotlin/org/jetbrains/compose/videoplayer/VideoPlayer.kt

@ -31,6 +31,7 @@ fun VideoPlayer(
onFinish = onFinish
)
@Composable
internal expect fun VideoPlayerImpl(
url: String,
isResumed: Boolean,

22
experimental/components/build.gradle.kts

@ -1,27 +1,11 @@
buildscript {
val composeVersion = property("compose.version")
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion")
classpath(kotlin("gradle-plugin", version = "1.5.31"))
}
plugins {
kotlin("multiplatform").apply(false)
id("org.jetbrains.compose").apply(false)
}
subprojects {
version = findProperty("deploy.version") ?: property("compose.version")!!
repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins.withId("java") {
configureIfExists<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_11

13
experimental/components/gradle.properties

@ -1,5 +1,8 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
android.enableJetifier=true
kotlin.code.style=official
compose.version=1.0.0
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.configuration-cache=true
compose.version=1.5.11
kotlin.version=1.9.21
kotlin.code.style=official

67
experimental/components/gradle/verification-metadata.xml

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>true</verify-signatures>
<ignored-keys>
<ignored-key id="4A3DA032DA2BBE01" reason="vlcj key is not certified with a trusted signature"/>
</ignored-keys>
<trusted-keys>
<trusted-key id="1BD97A6A154E7810EE0BC832E2F38302C8075E3D" group="org.gradle.kotlin" name="gradle-kotlin-dsl-plugins" version="4.2.1"/>
<trusted-key id="20723A6399BC060154283B37CFAE163B64AC9189" group="^org[.]jetbrains($|([.].*))" regex="true"/>
<trusted-key id="2E3A1AFFE42B5F53AF19F780BCF4173966770193" group="org.jetbrains" name="annotations" version="13.0"/>
<trusted-key id="33FD4BFD33554634053D73C0C2148900BCD3C2AF" group="org.jetbrains" name="annotations" version="23.0.0"/>
<trusted-key id="6F538074CCEBF35F28AF9B066A0975F8B1127B83" group="org.jetbrains.kotlin"/>
<trusted-key id="8756C4F765C9AC3CB6B85D62379CE192D401AB61" group="org.jetbrains.intellij.deps" name="trove4j" version="1.0.20200330"/>
<trusted-key id="E7DC75FC24FB3C8DFE8086AD3D5839A2262CBBFB" group="org.jetbrains.kotlinx"/>
<trusted-key id="FA7929F83AD44C4590F6CC6815C71C0A4E0B8EDD" group="net.java.dev.jna"/>
</trusted-keys>
</configuration>
<components>
<component group="org.gradle.kotlin.kotlin-dsl" name="org.gradle.kotlin.kotlin-dsl.gradle.plugin" version="4.2.1">
<artifact name="org.gradle.kotlin.kotlin-dsl.gradle.plugin-4.2.1.pom">
<sha256 value="311ff9eca17b0f6c9cc8104ebb88c73d38a4985a0488b01670bc2be09e5a2320" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="org.jetbrains.compose" name="compose-gradle-plugin" version="1.5.11">
<artifact name="compose-gradle-plugin-1.5.11.jar">
<ignored-keys>
<ignored-key id="20723A6399BC060154283B37CFAE163B64AC9189" reason="PGP verification failed"/>
</ignored-keys>
<sha256 value="d54be424f035dd452f2317b56ed54e0080135bed1936fc84f7371f59b9e9edea" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
</artifact>
<artifact name="compose-gradle-plugin-1.5.11.module">
<ignored-keys>
<ignored-key id="20723A6399BC060154283B37CFAE163B64AC9189" reason="PGP verification failed"/>
</ignored-keys>
<sha256 value="e74b7aedc30d01e7b8e3d53280b5a67e168b3cd44632e340f094560e8364c7f5" origin="Generated by Gradle" reason="PGP signature verification failed!"/>
</artifact>
</component>
<component group="org.jetbrains.compose" name="org.jetbrains.compose.gradle.plugin" version="1.5.11">
<artifact name="org.jetbrains.compose.gradle.plugin-1.5.11.pom">
<sha256 value="0915dc92e751288db3b6ced9b24d4c786f940e68a1d7fff4a0ce151d5f19fbc4" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="org.jetbrains.kotlin.multiplatform" name="org.jetbrains.kotlin.multiplatform.gradle.plugin" version="1.9.21">
<artifact name="org.jetbrains.kotlin.multiplatform.gradle.plugin-1.9.21.pom">
<sha256 value="4c7095d560a1c73dcff7b4d4d3b58a3ba5e895d10075f8fde24b7d6b300d6856" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="uk.co.caprica" name="vlcj" version="4.8.2">
<artifact name="vlcj-4.8.2.jar">
<sha256 value="304d585e780be8765baa4fa83b376c21920789f9e570aecf888912d40eedabb5" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
<artifact name="vlcj-4.8.2.pom">
<sha256 value="ef72f58a845a737e948b015213db48b69f9c5137f73b507cfd978e545c5b2009" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="uk.co.caprica" name="vlcj-natives" version="4.8.1">
<artifact name="vlcj-natives-4.8.1.jar">
<sha256 value="f47cef91dfdf335611b6d11945c9d1794e85811b3884c1fd31f9ed76ab19da50" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
<artifact name="vlcj-natives-4.8.1.pom">
<sha256 value="a676774073e4ee3d782e47129679877f7d7c0ae72e9dd4b04025d56672292edc" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
</components>
</verification-metadata>

2
experimental/components/gradle/wrapper/gradle-wrapper.properties vendored

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
experimental/components/gradlew vendored

20
experimental/components/settings.gradle.kts

@ -1,2 +1,22 @@
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
plugins {
kotlin("multiplatform").version(extra["kotlin.version"] as String)
id("org.jetbrains.compose").version(extra["compose.version"] as String)
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
include(":VideoPlayer:library")
include(":VideoPlayer:demo")

3
gradle-plugins/compose/build.gradle.kts

@ -63,6 +63,7 @@ dependencies {
compileOnly(libs.plugin.android)
compileOnly(libs.plugin.android.api)
testImplementation(kotlin("test"))
testImplementation(gradleTestKit())
testImplementation(kotlin("gradle-plugin-api"))
@ -72,7 +73,7 @@ dependencies {
embedded(project(":jdk-version-probe"))
}
val packagesToRelocate = listOf("de.undercouch")
val packagesToRelocate = listOf("de.undercouch", "com.squareup.kotlinpoet")
val shadow = tasks.named<ShadowJar>("shadowJar") {
for (packageToRelocate in packagesToRelocate) {

2
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposeCompilerCompatibility.kt

@ -22,7 +22,7 @@ internal object ComposeCompilerCompatibility {
"1.9.20-RC2" to "1.5.3-rc01",
"1.9.20" to "1.5.3",
"1.9.21" to "1.5.4",
"1.9.22" to "1.5.8-beta01",
"1.9.22" to "1.5.8.1-beta02",
"2.0.0-Beta1" to "1.5.4-dev1-kt2.0.0-Beta1",
)

23
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/ComposePlugin.kt

@ -19,8 +19,8 @@ import org.jetbrains.compose.desktop.DesktopExtension
import org.jetbrains.compose.desktop.application.internal.configureDesktop
import org.jetbrains.compose.desktop.preview.internal.initializePreview
import org.jetbrains.compose.experimental.dsl.ExperimentalExtension
import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck
import org.jetbrains.compose.experimental.internal.configureExperimental
import org.jetbrains.compose.experimental.internal.configureExperimentalTargetsFlagsCheck
import org.jetbrains.compose.experimental.internal.configureNativeCompilerCaching
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.mppExt
@ -31,11 +31,11 @@ import org.jetbrains.compose.internal.utils.currentTarget
import org.jetbrains.compose.resources.configureComposeResources
import org.jetbrains.compose.resources.ios.configureSyncTask
import org.jetbrains.compose.web.WebExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
internal val composeVersion get() = ComposeBuildConfig.composeVersion
@ -87,6 +87,23 @@ abstract class ComposePlugin : Plugin<Project> {
disableSignatureClashCheck(project)
}
// TODO: remove this (https://youtrack.jetbrains.com/issue/COMPOSE-939)
// we substitute the coroutines version for web targets in user projects,
// so they don't need to do that manually
project.configurations.all {
val isWeb = it.name.startsWith("wasmJs") || it.name.startsWith("js")
if (isWeb) {
it.resolutionStrategy.eachDependency {
if (it.requested.group.startsWith("org.jetbrains.kotlinx") &&
it.requested.name.startsWith("kotlinx-coroutines-")) {
if (it.requested.version?.startsWith("1.7") == true) {
it.useVersion("1.8.0-RC2")
}
}
}
}
}
}
private fun disableSignatureClashCheck(project: Project) {

42
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/AndroidTargetConfiguration.kt

@ -1,42 +0,0 @@
package org.jetbrains.compose.resources
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.tasks.MergeSourceSetFolders
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.SourceSet
import org.jetbrains.compose.internal.utils.registerTask
import java.io.File
internal fun Project.configureAndroidResources(
commonResourcesDir: Provider<File>,
androidFontsDir: Provider<File>,
onlyIfProvider: Provider<Boolean>
) {
val androidExtension = project.extensions.findByName("android") as? BaseExtension ?: return
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
val androidMainSourceSet = androidExtension.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
androidMainSourceSet.resources.srcDir(commonResourcesDir)
androidMainSourceSet.assets.srcDir(androidFontsDir)
val copyFonts = registerTask<Copy>("copyFontsToAndroidAssets") {
includeEmptyDirs = false
from(commonResourcesDir)
include("**/font*/*")
into(androidFontsDir)
onlyIf { onlyIfProvider.get() }
}
androidComponents.onVariants { variant ->
variant.sources?.assets?.addGeneratedSourceDirectory(
taskProvider = copyFonts,
wiredWith = {
objects.directoryProperty().fileProvider(
copyFonts.map { t -> t.destinationDir }
)
}
)
}
}

55
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt

@ -12,10 +12,13 @@ import kotlin.io.path.relativeTo
/**
* This task should be FAST and SAFE! Because it is being run during IDE import.
*/
abstract class GenerateResClassTask : DefaultTask() {
internal abstract class GenerateResClassTask : DefaultTask() {
@get:Input
abstract val packageName: Property<String>
@get:Input
abstract val shouldGenerateResClass: Property<Boolean>
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resDir: Property<File>
@ -26,32 +29,37 @@ abstract class GenerateResClassTask : DefaultTask() {
@TaskAction
fun generate() {
try {
val rootResDir = resDir.get()
logger.info("Generate resources for $rootResDir")
val kotlinDir = codeDir.get().asFile
logger.info("Clean directory $kotlinDir")
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()
//get first level dirs
val dirs = rootResDir.listNotHiddenFiles()
if (shouldGenerateResClass.get()) {
val rootResDir = resDir.get()
logger.info("Generate resources for $rootResDir")
dirs.forEach { f ->
if (!f.isDirectory) {
error("${f.name} is not directory! Raw files should be placed in '${rootResDir.name}/files' directory.")
}
}
//get first level dirs
val dirs = rootResDir.listNotHiddenFiles()
//type -> id -> resource item
val resources: Map<ResourceType, Map<String, List<ResourceItem>>> = dirs
.flatMap { dir ->
dir.listNotHiddenFiles()
.mapNotNull { it.fileToResourceItems(rootResDir.toPath()) }
.flatten()
dirs.forEach { f ->
if (!f.isDirectory) {
error("${f.name} is not directory! Raw files should be placed in '${rootResDir.name}/files' directory.")
}
}
.groupBy { it.type }
.mapValues { (_, items) -> items.groupBy { it.name } }
val kotlinDir = codeDir.get().asFile
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()
getResFileSpec(resources, packageName.get()).writeTo(kotlinDir)
//type -> id -> resource item
val resources: Map<ResourceType, Map<String, List<ResourceItem>>> = dirs
.flatMap { dir ->
dir.listNotHiddenFiles()
.mapNotNull { it.fileToResourceItems(rootResDir.toPath()) }
.flatten()
}
.groupBy { it.type }
.mapValues { (_, items) -> items.groupBy { it.name } }
getResFileSpec(resources, packageName.get()).writeTo(kotlinDir)
} else {
logger.info("Generation Res class is disabled")
}
} catch (e: Exception) {
//message must contain two ':' symbols to be parsed by IDE UI!
logger.error("e: GenerateResClassTask was failed:", e)
@ -106,6 +114,5 @@ abstract class GenerateResClassTask : DefaultTask() {
}
internal fun String.asUnderscoredIdentifier(): String =
lowercase()
.replace('-', '_')
replace('-', '_')
.let { if (it.isNotEmpty() && it.first().isDigit()) "_$it" else it }

148
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt

@ -1,18 +1,33 @@
package org.jetbrains.compose.resources
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.tasks.ProcessJavaResTask
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.*
import org.jetbrains.compose.ComposePlugin
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID
import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID
import org.jetbrains.compose.internal.utils.*
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.sources.android.androidSourceSetInfoOrNull
import org.jetbrains.kotlin.gradle.utils.ObservableSet
import java.io.File
import javax.inject.Inject
internal const val COMPOSE_RESOURCES_DIR = "composeResources"
private const val RES_GEN_DIR = "generated/compose/resourceGenerator"
internal const val RES_GEN_DIR = "generated/compose/resourceGenerator"
private val androidPluginIds = listOf(
"com.android.application",
"com.android.library"
@ -20,30 +35,93 @@ private val androidPluginIds = listOf(
internal fun Project.configureComposeResources() {
plugins.withId(KOTLIN_MPP_PLUGIN_ID) {
configureComposeResources(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
val kotlinExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
configureComposeResources(kotlinExtension, KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
//when applied AGP then configure android resources
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
configureAndroidComposeResources(kotlinExtension, androidExtension)
}
}
}
plugins.withId(KOTLIN_JVM_PLUGIN_ID) {
configureComposeResources(SourceSet.MAIN_SOURCE_SET_NAME)
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
configureComposeResources(kotlinExtension, SourceSet.MAIN_SOURCE_SET_NAME)
}
}
private fun Project.configureComposeResources(commonSourceSetName: String) {
val kotlinExtension = project.extensions.getByType(KotlinProjectExtension::class.java)
private fun Project.configureComposeResources(kotlinExtension: KotlinProjectExtension, commonSourceSetName: String) {
kotlinExtension.sourceSets.all { sourceSet ->
val sourceSetName = sourceSet.name
val composeResourcesPath = project.projectDir.resolve("src/$sourceSetName/$COMPOSE_RESOURCES_DIR")
//To compose resources will be packed to a final artefact we need to mark them as resources
//sourceSet.resources works for all targets except ANDROID!
sourceSet.resources.srcDirs(composeResourcesPath)
if (sourceSetName == commonSourceSetName) {
configureResourceGenerator(composeResourcesPath, sourceSet)
}
}
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)
private fun Project.configureAndroidComposeResources(
kotlinExtension: KotlinMultiplatformExtension,
androidExtension: BaseExtension
) {
val commonResourcesDir = projectDir.resolve("src/${KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME}/$COMPOSE_RESOURCES_DIR")
//Copy common compose resources except fonts to android resources
val copyCommonAndroidComposeResources = registerTask<CopyCommonAndroidComposeResources>(
"copyCommonAndroidComposeResources"
) {
from.set(commonResourcesDir)
outputDirectory.set(layout.buildDirectory.dir("$RES_GEN_DIR/commonAndroidComposeResources"))
}
tasks.configureEachWithType<ProcessJavaResTask> { dependsOn(copyCommonAndroidComposeResources) }
//mark all composeResources as Android resources
kotlinExtension.targets.withType(KotlinAndroidTarget::class.java).all { androidTarget ->
androidTarget.compilations.all { compilation: KotlinCompilation<*> ->
compilation.defaultSourceSet.androidSourceSetInfoOrNull?.let { kotlinAndroidSourceSet ->
androidExtension.sourceSets
.matching { it.name == kotlinAndroidSourceSet.androidSourceSetName }
.all { androidSourceSet ->
androidSourceSet.resources.srcDir(copyCommonAndroidComposeResources.flatMap { it.outputDirectory.asFile })
(compilation.allKotlinSourceSets as? ObservableSet<KotlinSourceSet>)?.forAll { kotlinSourceSet ->
if (kotlinSourceSet.name != KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) {
androidSourceSet.resources.srcDir(
projectDir.resolve("src/${kotlinSourceSet.name}/$COMPOSE_RESOURCES_DIR")
)
}
}
}
}
}
}
//copy fonts from the compose resources dir to android assets
val androidComponents = project.extensions.findByType(AndroidComponentsExtension::class.java) ?: return
androidComponents.onVariants { variant ->
val copyFonts = registerTask<CopyAndroidFontsToAssetsTask>(
"copy${variant.name.uppercaseFirstChar()}FontsToAndroidAssets"
) {
from.set(commonResourcesDir)
}
variant.sources?.assets?.addGeneratedSourceDirectory(
taskProvider = copyFonts,
wiredWith = CopyAndroidFontsToAssetsTask::outputDirectory
)
}
}
private fun Project.configureResourceGenerator(commonComposeResourcesDir: File, commonSourceSet: KotlinSourceSet) {
val commonComposeResources = provider { commonComposeResourcesDir }
val packageName = provider {
buildString {
val group = project.group.toString().asUnderscoredIdentifier()
val group = project.group.toString().lowercase().asUnderscoredIdentifier()
append(group)
if (group.isNotEmpty()) append(".")
append(project.name.lowercase())
@ -54,7 +132,7 @@ private fun Project.configureResourceGenerator(commonComposeResourcesDir: File,
fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) })
//lazy check a dependency on the Resources library
val shouldGenerateResourceAccessors: Provider<Boolean> = provider {
val shouldGenerateResClass: Provider<Boolean> = provider {
if (ComposeProperties.alwaysGenerateResourceAccessors(project).get()) {
true
} else {
@ -72,9 +150,9 @@ private fun Project.configureResourceGenerator(commonComposeResourcesDir: File,
GenerateResClassTask::class.java
) {
it.packageName.set(packageName)
it.resDir.set(commonComposeResources)
it.shouldGenerateResClass.set(shouldGenerateResClass)
it.resDir.set(commonComposeResourcesDir)
it.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))
it.onlyIf { shouldGenerateResourceAccessors.get() }
}
//register generated source set
@ -86,15 +164,47 @@ private fun Project.configureResourceGenerator(commonComposeResourcesDir: File,
it.dependsOn(genTask)
}
}
}
internal abstract class CopyCommonAndroidComposeResources : DefaultTask() {
@get:Inject
abstract val fileSystem: FileSystemOperations
@get:InputFiles
abstract val from: Property<File>
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
//when applied AGP then configure android resources
androidPluginIds.forEach { pluginId ->
plugins.withId(pluginId) {
configureAndroidResources(
commonComposeResources,
buildDir("$RES_GEN_DIR/androidFonts").map { it.asFile },
shouldGenerateResourceAccessors
)
@TaskAction
fun action() {
fileSystem.copy {
it.includeEmptyDirs = false
it.from(from)
it.exclude("**/font*/*")
it.into(outputDirectory)
}
}
}
//Copy task doesn't work with 'variant.sources?.assets?.addGeneratedSourceDirectory' API
internal abstract class CopyAndroidFontsToAssetsTask : DefaultTask() {
@get:Inject
abstract val fileSystem: FileSystemOperations
@get:Input
abstract val from: Property<File>
@get:OutputDirectory
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun action() {
fileSystem.copy {
it.includeEmptyDirs = false
it.from(from)
it.include("**/font*/*")
it.into(outputDirectory)
}
}
}

158
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

@ -2,6 +2,8 @@ package org.jetbrains.compose.resources
import com.squareup.kotlinpoet.*
import java.nio.file.Path
import java.util.SortedMap
import java.util.TreeMap
import kotlin.io.path.invariantSeparatorsPathString
internal enum class ResourceType(val typeName: String) {
@ -26,12 +28,14 @@ internal data class ResourceItem(
val path: Path
)
private fun ResourceItem.getClassName(): ClassName = when (type) {
private fun ResourceType.getClassName(): ClassName = when (this) {
ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource")
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource")
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource")
}
private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder {
val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier")
val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier")
@ -101,85 +105,121 @@ internal fun getResFileSpec(
//type -> id -> items
resources: Map<ResourceType, Map<String, List<ResourceItem>>>,
packageName: String
): FileSpec = FileSpec.builder(packageName, "Res").apply {
addType(TypeSpec.objectBuilder("Res").apply {
addModifiers(KModifier.INTERNAL)
): FileSpec =
FileSpec.builder(packageName, "Res").apply {
addAnnotation(
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
.addMember("org.jetbrains.compose.resources.InternalResourceApi::class")
.build()
)
addAnnotation(
AnnotationSpec.builder(ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi"))
.addMember("org.jetbrains.compose.resources.ExperimentalResourceApi::class")
.build()
)
//readFileBytes
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
addFunction(
FunSpec.builder("readBytes")
.addKdoc("""
//we need to sort it to generate the same code on different platforms
val sortedResources = sortResources(resources)
addType(TypeSpec.objectBuilder("Res").apply {
addModifiers(KModifier.INTERNAL)
addAnnotation(
AnnotationSpec.builder(
ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi")
).build()
)
//readFileBytes
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes")
addFunction(
FunSpec.builder("readBytes")
.addKdoc(
"""
Reads the content of the resource file at the specified path and returns it as a byte array.
Example: `val bytes = Res.readBytes("files/key.bin")`
@param path The path of the file to read in the compose resource's directory.
@return The content of the file as a byte array.
""".trimIndent())
.addParameter("path", String::class)
.addModifiers(KModifier.SUSPEND)
.returns(ByteArray::class)
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
.build()
)
""".trimIndent()
)
.addParameter("path", String::class)
.addModifiers(KModifier.SUSPEND)
.returns(ByteArray::class)
.addStatement("return %M(path)", readResourceBytes) //todo: add module ID here
.build()
)
val types = sortedResources.map { (type, idToResources) ->
getResourceTypeObject(type, idToResources)
}
addTypes(types)
}.build())
sortedResources
.flatMap { (type, idToResources) ->
idToResources.map { (name, items) ->
getResourceInitializer(name, type, items)
}
}
.forEach { addFunction(it) }
}.build()
val types = resources.map { (type, idToResources) ->
getResourceTypeObject(type, idToResources)
}.sortedBy { it.name }
addTypes(types)
}.build())
}.build()
private fun getterName(resourceType: ResourceType, resourceName: String): String =
"get_${resourceType.typeName}_$resourceName"
private fun getResourceTypeObject(type: ResourceType, nameToResources: Map<String, List<ResourceItem>>) =
TypeSpec.objectBuilder(type.typeName).apply {
nameToResources.entries
.sortedBy { it.key }
.forEach { (name, items) ->
addResourceProperty(name, items.sortedBy { it.path })
nameToResources.keys
.forEach { name ->
addProperty(
PropertySpec
.builder(name, type.getClassName())
.initializer(getterName(type, name) + "()")
.build()
)
}
}.build()
private fun TypeSpec.Builder.addResourceProperty(name: String, items: List<ResourceItem>) {
val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem")
val first = items.first()
val propertyClassName = first.getClassName()
val resourceId = first.let { "${it.type}:${it.name}" }
val initializer = CodeBlock.builder()
.add("%T(\n", propertyClassName).withIndent {
add("\"$resourceId\",\n")
if (first.type == ResourceType.STRING) {
add("\"${first.name}\",\n")
}
add("setOf(\n").withIndent {
items.forEach { item ->
add("%T(\n", resourceItemClass).withIndent {
add("setOf(").addQualifiers(item).add("),\n")
//file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"\n") //todo: add module ID here
private fun getResourceInitializer(name: String, type: ResourceType, items: List<ResourceItem>): FunSpec {
val propertyTypeName = type.getClassName()
val resourceId = "${type}:${name}"
return FunSpec.builder(getterName(type, name))
.addModifiers(KModifier.PRIVATE)
.returns(propertyTypeName)
.addStatement(
CodeBlock.builder()
.add("return %T(\n", propertyTypeName).withIndent {
add("\"$resourceId\",")
if (type == ResourceType.STRING) add(" \"$name\",")
withIndent {
add("\nsetOf(\n").withIndent {
items.forEach { item ->
add("%T(", resourceItemClass)
add("setOf(").addQualifiers(item).add("), ")
//file separator should be '/' on all platforms
add("\"${item.path.invariantSeparatorsPathString}\"") //todo: add module ID here
add("),\n")
}
}
add(")\n")
}
add("),\n")
}
}
add(")\n")
}
.add(")")
.add(")")
.build().toString()
)
.build()
}
addProperty(
PropertySpec.builder(name, propertyClassName)
.initializer(initializer)
.build()
)
private fun sortResources(
resources: Map<ResourceType, Map<String, List<ResourceItem>>>
): TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>> {
val result = TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>>()
resources
.entries
.forEach { (type, items) ->
val typeResult = TreeMap<String, List<ResourceItem>>()
items
.entries
.forEach { (name, resItems) ->
typeResult[name] = resItems.sortedBy { it.path }
}
result[type] = typeResult
}
return result
}

292
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/tests/integration/ResourcesTest.kt

@ -1,15 +1,14 @@
package org.jetbrains.compose.test.tests.integration
import org.jetbrains.compose.test.utils.GradlePluginTestBase
import org.jetbrains.compose.test.utils.assertEqualTextFiles
import org.jetbrains.compose.test.utils.assertNotEqualTextFiles
import org.jetbrains.compose.test.utils.checks
import org.jetbrains.compose.test.utils.*
import org.junit.jupiter.api.Test
import kotlin.io.path.Path
import java.io.File
import java.util.zip.ZipFile
import kotlin.test.*
class ResourcesTest : GradlePluginTestBase() {
@Test
fun testGeneratedAccessorsAndCopiedFonts(): Unit = with(testProject("misc/commonResources")) {
fun testGeneratedAccessors(): Unit = with(testProject("misc/commonResources")) {
//check generated resource's accessors
gradle("generateComposeResClass").checks {
assertEqualTextFiles(
@ -33,72 +32,88 @@ class ResourcesTest : GradlePluginTestBase() {
file("src/commonMain/composeResources/drawable-ren")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
contains unknown qualifier: 'ren'.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/drawable-ren").renameTo(
file("src/commonMain/composeResources/drawable-rUS-en")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
Region qualifier must be declared after language: 'en-rUS'.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/drawable-rUS-en").renameTo(
file("src/commonMain/composeResources/drawable-rUS")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
Region qualifier must be used only with language.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/drawable-rUS").renameTo(
file("src/commonMain/composeResources/drawable-en-fr")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
contains repetitive qualifiers: 'en' and 'fr'.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/drawable-en-fr").renameTo(
file("src/commonMain/composeResources/image")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
Unknown resource type: 'image'
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/image").renameTo(
file("src/commonMain/composeResources/files-de")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
The 'files' directory doesn't support qualifiers: 'files-de'.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/files-de").renameTo(
file("src/commonMain/composeResources/strings")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
Unknown resource type: 'strings'.
""".trimIndent())
""".trimIndent()
)
}
file("src/commonMain/composeResources/strings").renameTo(
file("src/commonMain/composeResources/string-us")
)
gradle("generateComposeResClass").checks {
check.logContains("""
check.logContains(
"""
Forbidden directory name 'string-us'! String resources should be declared in 'values/strings.xml'.
""".trimIndent())
""".trimIndent()
)
}
//restore defaults
@ -111,9 +126,145 @@ class ResourcesTest : GradlePluginTestBase() {
}
@Test
fun testCopyFontsInAndroidApp(): Unit = with(testProject("misc/commonResources")) {
gradle("assembleDebug").checks {
check.taskSuccessful(":copyFontsToAndroidAssets")
fun testFinalArtefacts(): Unit = with(testProject("misc/commonResources")) {
//https://developer.android.com/build/build-variants?utm_source=android-studio#product-flavors
file("build.gradle.kts").appendText("""
kotlin {
js {
browser {
testTask(Action {
enabled = false
})
}
binaries.executable()
}
}
android {
flavorDimensions += "version"
productFlavors {
create("demo")
create("full")
}
}
""".trimIndent())
file("src/androidDemoDebug/composeResources/files/platform.txt").writeNewFile("android demo-debug")
file("src/androidDemoRelease/composeResources/files/platform.txt").writeNewFile("android demo-release")
file("src/androidFullDebug/composeResources/files/platform.txt").writeNewFile("android full-debug")
file("src/androidFullRelease/composeResources/files/platform.txt").writeNewFile("android full-release")
file("src/desktopMain/composeResources/files/platform.txt").writeNewFile("desktop")
file("src/jsMain/composeResources/files/platform.txt").writeNewFile("js")
val commonResourcesDir = file("src/commonMain/composeResources")
val commonResourcesFiles = commonResourcesDir.walkTopDown()
.filter { !it.isDirectory && !it.isHidden }
.map { it.relativeTo(commonResourcesDir).invariantSeparatorsPath }
gradle("build").checks {
check.taskSuccessful(":copyDemoDebugFontsToAndroidAssets")
check.taskSuccessful(":copyDemoReleaseFontsToAndroidAssets")
check.taskSuccessful(":copyFullDebugFontsToAndroidAssets")
check.taskSuccessful(":copyFullReleaseFontsToAndroidAssets")
checkAndroidApk("demo", "debug", commonResourcesFiles)
checkAndroidApk("demo", "release", commonResourcesFiles)
checkAndroidApk("full", "debug", commonResourcesFiles)
checkAndroidApk("full", "release", commonResourcesFiles)
val desktopJar = file("build/libs/resources_test-desktop.jar")
assertTrue(desktopJar.exists())
ZipFile(desktopJar).use { zip ->
commonResourcesFiles.forEach { res ->
assertNotNull(zip.getEntry(res))
}
val platformTxt = zip.getEntry("files/platform.txt")
assertNotNull(platformTxt)
val text = zip.getInputStream(platformTxt).readBytes().decodeToString()
assertEquals("desktop", text)
}
val jsBuildDir = file("build/dist/js/productionExecutable")
commonResourcesFiles.forEach { res ->
assertTrue(jsBuildDir.resolve(res).exists())
}
assertEquals("js", jsBuildDir.resolve("files/platform.txt").readText())
}
}
private fun File.writeNewFile(text: String) {
parentFile.mkdirs()
createNewFile()
writeText(text)
}
private fun TestProject.checkAndroidApk(flavor: String, type: String, commonResourcesFiles: Sequence<String>) {
val apk = file("build/outputs/apk/$flavor/$type/resources_test-$flavor-$type.apk")
assertTrue(apk.exists())
ZipFile(apk).use { zip ->
commonResourcesFiles.forEach { res ->
if (res == "font/emptyFont.otf") {
//android fonts should be only in assets
assertNull(zip.getEntry(res))
} else {
assertNotNull(zip.getEntry(res))
}
}
assertNotNull(zip.getEntry("assets/font/emptyFont.otf"))
val platformTxt = zip.getEntry("files/platform.txt")
assertNotNull(platformTxt)
val text = zip.getInputStream(platformTxt).readBytes().decodeToString()
assertEquals("android $flavor-$type", text)
}
}
@Test
fun testUpToDateChecks(): Unit = with(testProject("misc/commonResources")) {
gradle("prepareKotlinIdeaImport").checks {
check.taskSuccessful(":generateComposeResClass")
assertTrue(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists())
}
gradle("prepareKotlinIdeaImport").checks {
check.taskUpToDate(":generateComposeResClass")
}
modifyText("build.gradle.kts") { str ->
str.replace(
"implementation(compose.components.resources)",
"//implementation(compose.components.resources)"
)
}
gradle("prepareKotlinIdeaImport").checks {
check.taskSuccessful(":generateComposeResClass")
assertFalse(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists())
}
gradle("prepareKotlinIdeaImport", "-Pcompose.resources.always.generate.accessors=true").checks {
check.taskSuccessful(":generateComposeResClass")
assertTrue(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists())
}
modifyText("build.gradle.kts") { str ->
str.replace(
"//implementation(compose.components.resources)",
"implementation(compose.components.resources)"
)
}
gradle("prepareKotlinIdeaImport").checks {
check.taskUpToDate(":generateComposeResClass")
assertTrue(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists())
}
modifyText("build.gradle.kts") { str ->
str.replace(
"group = \"app.group\"",
"group = \"io.company\""
)
}
gradle("prepareKotlinIdeaImport").checks {
check.taskSuccessful(":generateComposeResClass")
assertFalse(file("build/generated/compose/resourceGenerator/kotlin/app/group/resources_test/generated/resources/Res.kt").exists())
assertTrue(file("build/generated/compose/resourceGenerator/kotlin/io/company/resources_test/generated/resources/Res.kt").exists())
}
}
@ -137,4 +288,95 @@ class ResourcesTest : GradlePluginTestBase() {
}
gradle("jar")
}
//https://github.com/JetBrains/compose-multiplatform/issues/4194
@Test
fun testHugeNumberOfStrings(): Unit = with(
//disable cache for the test because the generateStringFiles task doesn't support it
testProject("misc/commonResources", defaultTestEnvironment.copy(useGradleConfigurationCache = false))
) {
file("build.gradle.kts").let { f ->
val originText = f.readText()
f.writeText(
buildString {
appendLine("import java.util.Locale")
append(originText)
appendLine()
append("""
val template = ""${'"'}
<resources>
<string name="app_name">Compose Resources App</string>
<string name="hello">😊 Hello world!</string>
<string name="multi_line">Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Donec eget turpis ac sem ultricies consequat.</string>
<string name="str_template">Hello, %1${'$'}{"$"}s! You have %2${'$'}{"$"}d new messages.</string>
<string-array name="str_arr">
<item>item 1</item>
<item>item 2</item>
<item>item 3</item>
</string-array>
[ADDITIONAL_STRINGS]
</resources>
""${'"'}.trimIndent()
val generateStringFiles = tasks.register("generateStringFiles") {
val numberOfLanguages = 20
val numberOfStrings = 500
val langs = Locale.getAvailableLocales()
.map { it.language }
.filter { it.count() == 2 }
.sorted()
.distinct()
.take(numberOfLanguages)
.toList()
val resourcesFolder = project.file("src/commonMain/composeResources")
doLast {
// THIS REMOVES THE `values` FOLDER IN `composeResources`
// THIS REMOVES THE `values` FOLDER IN `composeResources`
// Necessary when reducing the number of languages.
resourcesFolder.listFiles()?.filter { it.name.startsWith("values") }?.forEach {
it.deleteRecursively()
}
langs.forEachIndexed { langIndex, lang ->
val additionalStrings =
(0 until numberOfStrings).joinToString(System.lineSeparator()) { index ->
""${'"'}
<string name="string_${'$'}{index.toString().padStart(4, '0')}">String ${'$'}index in lang ${'$'}lang</string>
""${'"'}.trimIndent()
}
val langFile = if (langIndex == 0) {
File(resourcesFolder, "values/strings.xml")
} else {
File(resourcesFolder, "values-${'$'}lang/strings.xml")
}
langFile.parentFile.mkdirs()
langFile.writeText(template.replace("[ADDITIONAL_STRINGS]", additionalStrings))
}
}
}
tasks.named("generateComposeResClass") {
dependsOn(generateStringFiles)
}
""".trimIndent())
}
)
}
gradle("desktopJar").checks {
check.taskSuccessful(":generateStringFiles")
check.taskSuccessful(":generateComposeResClass")
assertEquals(513, file("src/commonMain/composeResources/values/strings.xml").readLines().size)
}
}
//https://github.com/gmazzo/gradle-buildconfig-plugin/issues/131
@Test
fun testBundledKotlinPoet(): Unit = with(testProject("misc/bundledKotlinPoet")) {
gradle("generateBuildConfig")
}
}

4
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/utils/assertUtils.kt

@ -57,6 +57,10 @@ internal class BuildResultChecks(private val result: BuildResult) {
taskOutcome(task, TaskOutcome.FAILED)
}
fun taskUpToDate(task: String) {
taskOutcome(task, TaskOutcome.UP_TO_DATE)
}
fun taskFromCache(task: String) {
taskOutcome(task, TaskOutcome.FROM_CACHE)
}

1
gradle-plugins/compose/src/test/test-projects/application/mpp/gradle.properties

@ -0,0 +1 @@
android.useAndroidX=true

25
gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/app/build.gradle.kts

@ -0,0 +1,25 @@
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("com.github.gmazzo.buildconfig")
}
group = "app.group"
kotlin {
jvm()
sourceSets {
commonMain {
dependencies {
implementation(compose.runtime)
implementation(compose.material)
implementation(compose.components.resources)
}
}
}
}
buildConfig {
buildConfigField(String::class.java, "str", "")
}

9
gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/app/src/commonMain/kotlin/App.kt

@ -0,0 +1,9 @@
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import app.group.empty_res.generated.resources.Res
@Composable
fun App() {
val res = Res
Text("text")
}

4
gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/build.gradle.kts

@ -0,0 +1,4 @@
plugins {
kotlin("multiplatform").apply(false)
id("org.jetbrains.compose").apply(false)
}

1
gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/gradle.properties

@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx8096M

24
gradle-plugins/compose/src/test/test-projects/misc/bundledKotlinPoet/settings.gradle.kts

@ -0,0 +1,24 @@
rootProject.name = "bundled_kp"
include(":app")
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER")
id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER")
id("com.github.gmazzo.buildconfig").version("5.3.5")
}
}
dependencyResolutionManagement {
repositories {
mavenLocal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
mavenCentral()
gradlePluginPortal()
google()
}
}

27
gradle-plugins/compose/src/test/test-projects/misc/commonResources/build.gradle.kts

@ -1,6 +1,6 @@
plugins {
kotlin("multiplatform")
id("com.android.library")
id("com.android.application")
id("org.jetbrains.compose")
}
@ -21,7 +21,6 @@ kotlin {
dependencies {
implementation(compose.runtime)
implementation(compose.material)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
}
}
@ -29,10 +28,32 @@ kotlin {
}
android {
compileSdk = 31
compileSdk = 34
namespace = "org.jetbrains.compose.resources.test"
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
applicationId = "org.example.project"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
signingConfigs {
create("testkey") {
storeFile = project.file("key/debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("testkey")
}
getByName("debug") {
signingConfig = signingConfigs.getByName("testkey")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11

229
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package app.group.resources_test.generated.resources
import kotlin.ByteArray
@ -6,14 +11,9 @@ import kotlin.String
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.FontResource
import org.jetbrains.compose.resources.LanguageQualifier
import org.jetbrains.compose.resources.RegionQualifier
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.ThemeQualifier
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**
@ -27,115 +27,142 @@ internal object Res {
public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path)
public object drawable {
public val _3_strange_name: DrawableResource = DrawableResource(
"drawable:_3_strange_name",
setOf(
ResourceItem(
setOf(),
"drawable/3-strange-name.xml"
),
)
)
public val _3_strange_name: DrawableResource = get_drawable__3_strange_name()
public val vector: DrawableResource = DrawableResource(
"drawable:vector",
setOf(
ResourceItem(
setOf(LanguageQualifier("au"), RegionQualifier("US"), ),
"drawable-au-rUS/vector.xml"
),
ResourceItem(
setOf(ThemeQualifier.DARK, LanguageQualifier("ge"), ),
"drawable-dark-ge/vector.xml"
),
ResourceItem(
setOf(LanguageQualifier("en"), ),
"drawable-en/vector.xml"
),
ResourceItem(
setOf(),
"drawable/vector.xml"
),
)
)
public val camelCaseName: DrawableResource = get_drawable_camelCaseName()
public val vector_2: DrawableResource = DrawableResource(
"drawable:vector_2",
setOf(
ResourceItem(
setOf(),
"drawable/vector_2.xml"
),
)
)
public val vector: DrawableResource = get_drawable_vector()
public val vector_2: DrawableResource = get_drawable_vector_2()
}
public object string {
public val PascalCase: StringResource = get_string_PascalCase()
public val _1_kebab_case: StringResource = get_string__1_kebab_case()
public val app_name: StringResource = get_string_app_name()
public val camelCase: StringResource = get_string_camelCase()
public val hello: StringResource = get_string_hello()
public val multi_line: StringResource = get_string_multi_line()
public val str_arr: StringResource = get_string_str_arr()
public val str_template: StringResource = get_string_str_template()
}
public object font {
public val emptyfont: FontResource = FontResource(
"font:emptyfont",
setOf(
ResourceItem(
setOf(),
"font/emptyFont.otf"
),
)
)
public val emptyFont: FontResource = get_font_emptyFont()
}
}
public object string {
public val app_name: StringResource = StringResource(
"string:app_name",
"app_name",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
private fun get_drawable__3_strange_name(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:_3_strange_name",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/3-strange-name.xml"),
)
)
private fun get_drawable_camelCaseName(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:camelCaseName",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/camelCaseName.xml"),
)
)
private fun get_drawable_vector(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:vector",
setOf(
public val hello: StringResource = StringResource(
"string:hello",
"hello",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("au"),
org.jetbrains.compose.resources.RegionQualifier("US"), ), "drawable-au-rUS/vector.xml"),
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.ThemeQualifier.DARK,
org.jetbrains.compose.resources.LanguageQualifier("ge"), ), "drawable-dark-ge/vector.xml"),
org.jetbrains.compose.resources.ResourceItem(setOf(org.jetbrains.compose.resources.LanguageQualifier("en"),
), "drawable-en/vector.xml"),
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"),
)
)
public val multi_line: StringResource = StringResource(
"string:multi_line",
"multi_line",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
private fun get_drawable_vector_2(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:vector_2",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector_2.xml"),
)
)
public val str_arr: StringResource = StringResource(
"string:str_arr",
"str_arr",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
private fun get_string_PascalCase(): StringResource =
org.jetbrains.compose.resources.StringResource(
"string:PascalCase", "PascalCase",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
public val str_template: StringResource = StringResource(
"string:str_template",
"str_template",
setOf(
ResourceItem(
setOf(),
"values/strings.xml"
),
)
private fun get_string__1_kebab_case(): StringResource =
org.jetbrains.compose.resources.StringResource(
"string:_1_kebab_case", "_1_kebab_case",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
}
}
)
private fun get_string_app_name(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:app_name", "app_name",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_string_camelCase(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:camelCase", "camelCase",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_string_hello(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:hello", "hello",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_string_multi_line(): StringResource =
org.jetbrains.compose.resources.StringResource(
"string:multi_line", "multi_line",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_string_str_arr(): StringResource = org.jetbrains.compose.resources.StringResource(
"string:str_arr", "str_arr",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_string_str_template(): StringResource =
org.jetbrains.compose.resources.StringResource(
"string:str_template", "str_template",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "values/strings.xml"),
)
)
private fun get_font_emptyFont(): FontResource = org.jetbrains.compose.resources.FontResource(
"font:emptyFont",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "font/emptyFont.otf"),
)
)

3
gradle-plugins/compose/src/test/test-projects/misc/commonResources/gradle.properties

@ -1,2 +1,3 @@
org.gradle.jvmargs=-Xmx8096M
android.useAndroidX=true
android.useAndroidX=true
org.jetbrains.compose.experimental.jscanvas.enabled=true

BIN
gradle-plugins/compose/src/test/test-projects/misc/commonResources/key/debug.keystore

Binary file not shown.

2
gradle-plugins/compose/src/test/test-projects/misc/commonResources/settings.gradle.kts

@ -7,7 +7,7 @@ pluginManagement {
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
plugins {
id("com.android.library").version("AGP_VERSION_PLACEHOLDER")
id("com.android.application").version("AGP_VERSION_PLACEHOLDER")
id("org.jetbrains.kotlin.multiplatform").version("KOTLIN_VERSION_PLACEHOLDER")
id("org.jetbrains.compose").version("COMPOSE_GRADLE_PLUGIN_VERSION_PLACEHOLDER")
}

4
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/androidMain/AndroidManifest.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<application/>
</manifest>

36
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/drawable/camelCaseName.xml

@ -0,0 +1,36 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600">
<path
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z"
android:fillColor="#041619"
android:fillType="nonZero"/>
<path
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z"
android:fillColor="#37BF6E"
android:fillType="nonZero"/>
<path
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z"
android:fillColor="#3870B2"
android:fillType="nonZero"/>
<path
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
<path
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z"
android:strokeWidth="10"
android:fillColor="#00000000"
android:strokeColor="#083042"
android:fillType="nonZero"/>
</vector>

3
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/composeResources/values/strings.xml

@ -10,4 +10,7 @@
<item>item 2</item>
<item>item 3</item>
</string-array>
<string name="PascalCase">PascalCase</string>
<string name="1-kebab-case">1-kebab-case</string>
<string name="camelCase">camelCase</string>
</resources>

2
gradle-plugins/compose/src/test/test-projects/misc/commonResources/src/commonMain/kotlin/App.kt

@ -18,6 +18,6 @@ fun App() {
contentDescription = null
)
Text(stringResource(Res.string.app_name))
val font = FontFamily(Font(Res.font.emptyfont))
val font = FontFamily(Font(Res.font.emptyFont))
}
}

6
gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package app.group.empty_res.generated.resources
import kotlin.ByteArray
@ -6,7 +11,6 @@ import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**

27
gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

@ -1,3 +1,8 @@
@file:OptIn(
org.jetbrains.compose.resources.InternalResourceApi::class,
org.jetbrains.compose.resources.ExperimentalResourceApi::class,
)
package me.app.jvmonlyresources.generated.resources
import kotlin.ByteArray
@ -5,10 +10,8 @@ import kotlin.OptIn
import kotlin.String
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.ResourceItem
import org.jetbrains.compose.resources.readResourceBytes
@OptIn(org.jetbrains.compose.resources.InternalResourceApi::class)
@ExperimentalResourceApi
internal object Res {
/**
@ -22,14 +25,14 @@ internal object Res {
public suspend fun readBytes(path: String): ByteArray = readResourceBytes(path)
public object drawable {
public val vector: DrawableResource = DrawableResource(
"drawable:vector",
setOf(
ResourceItem(
setOf(),
"drawable/vector.xml"
),
)
)
public val vector: DrawableResource = get_drawable_vector()
}
}
}
private fun get_drawable_vector(): DrawableResource =
org.jetbrains.compose.resources.DrawableResource(
"drawable:vector",
setOf(
org.jetbrains.compose.resources.ResourceItem(setOf(), "drawable/vector.xml"),
)
)

2
gradle-plugins/gradle.properties

@ -4,7 +4,7 @@ kotlin.code.style=official
# Default version of Compose Libraries used by Gradle plugin
compose.version=1.6.0-dev1383
# The latest version of Compose Compiler used by Gradle plugin. Used only in tests/CI.
compose.tests.compiler.version=1.5.8-beta01
compose.tests.compiler.version=1.5.8.1-beta02
# The latest version of Kotlin compatible with compose.tests.compiler.version. Used only in tests/CI.
compose.tests.compiler.compatible.kotlin.version=1.9.22
# The latest version of Kotlin compatible with compose.tests.compiler.version for JS target. Used only on CI.

2
gradle-plugins/gradle/libs.versions.toml

@ -1,7 +1,7 @@
[versions]
kotlin = "1.9.0"
gradle-download-plugin = "5.5.0"
kotlin-poet = "1.14.2"
kotlin-poet = "1.16.0"
plugin-android = "7.3.0"
shadow-jar = "8.1.1"
publish-plugin = "1.2.1"

29
html/core/src/jsMain/kotlin/org/jetbrains/compose/web/css/StyleSheet.kt

@ -20,6 +20,9 @@ class CSSRulesHolderState : CSSRulesHolder {
/**
* Represents a collection of the css style rules.
* StyleSheet needs to be mounted.
*
* @param customPrefix Will be used as prefix with current style. Pass `null` to use default value (classname of realization)
*
* @see [Style]
*
* Example:
@ -38,12 +41,22 @@ class CSSRulesHolderState : CSSRulesHolder {
* ```
*/
open class StyleSheet(
customPrefix: String?,
private val rulesHolder: CSSRulesHolder = CSSRulesHolderState(),
val usePrefix: Boolean = true,
) : StyleSheetBuilder, CSSRulesHolder by rulesHolder {
private val boundClasses = mutableMapOf<String, CSSRuleDeclarationList>()
protected val prefix: String = customPrefix ?: "${this::class.simpleName}-"
val usePrefix: Boolean = customPrefix == null
constructor(
rulesHolder: CSSRulesHolder = CSSRulesHolderState(),
usePrefix: Boolean = true
) : this(
if (usePrefix) null else "",
rulesHolder
)
protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(usePrefix, cssRule)
protected fun style(cssRule: CSSBuilder.() -> Unit) = CSSHolder(prefix, cssRule)
/**
* Example:
@ -69,7 +82,7 @@ open class StyleSheet(
* }
* ```
*/
protected fun keyframes(cssKeyframes: CSSKeyframesBuilder.() -> Unit) = CSSKeyframesHolder(usePrefix, cssKeyframes)
protected fun keyframes(cssKeyframes: CSSKeyframesBuilder.() -> Unit) = CSSKeyframesHolder(prefix, cssKeyframes)
companion object {
private var counter = 0
@ -88,13 +101,12 @@ open class StyleSheet(
}
}
protected class CSSHolder(private val usePrefix: Boolean, private val cssBuilder: CSSBuilder.() -> Unit) {
protected class CSSHolder(private val prefix: String, private val cssBuilder: CSSBuilder.() -> Unit) {
operator fun provideDelegate(
sheet: StyleSheet,
property: KProperty<*>
): ReadOnlyProperty<Any?, String> {
val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else ""
val className = "$sheetName${property.name}"
val className = "$prefix${property.name}"
val selector = object : CSSSelector() {
override fun asString() = ".${className}"
}
@ -110,15 +122,14 @@ open class StyleSheet(
* See [keyframes]
*/
protected class CSSKeyframesHolder(
private val usePrefix: Boolean,
private val prefix: String,
private val keyframesBuilder: CSSKeyframesBuilder.() -> Unit
) {
operator fun provideDelegate(
sheet: StyleSheet,
property: KProperty<*>
): ReadOnlyProperty<Any?, CSSNamedKeyframes> {
val sheetName = if (usePrefix) "${sheet::class.simpleName}-" else ""
val keyframesName = "$sheetName${property.name}"
val keyframesName = "$prefix${property.name}"
val rule = buildKeyframes(keyframesName, keyframesBuilder)
sheet.add(rule)

49
html/core/src/jsTest/kotlin/css/AnimationTests.kt

@ -35,6 +35,28 @@ object AnimationsStyleSheet : StyleSheet() {
}
}
class AnimationsStyleSheetWithCustomPrefix(
customPrefix: String
) : StyleSheet(customPrefix) {
val bounce by keyframes {
from {
property("transform", "translateX(50%)")
}
to {
property("transform", "translateX(-50%)")
}
}
val animationClass by style {
animation(bounce) {
duration(2.s)
timingFunction(AnimationTimingFunction.EaseIn)
direction(AnimationDirection.Alternate)
}
}
}
@ExperimentalComposeWebApi
class AnimationTests {
@Test
@ -76,4 +98,31 @@ class AnimationTests {
"Animation class wasn't injected correctly"
)
}
@Test
fun animationClassInjectedWithCustomPrefix() = runTest {
val customPrefix = "CustomPrefix-"
composition {
Style(AnimationsStyleSheetWithCustomPrefix(customPrefix))
}
val el = nextChild() as HTMLStyleElement
val cssRules = (el.sheet as? CSSStyleSheet)?.cssRules
val rules = (0 until (cssRules?.length ?: 0)).map {
cssRules?.item(it)?.cssText?.replace("\n", "") ?: ""
}
// TODO: we need to come up with test that not relying on any kind of formatting
assertEquals(
"@keyframes ${customPrefix}bounce {0% { transform: translateX(50%); }100% { transform: translateX(-50%); }}",
rules[0].replace(" 0%", "0%").replace(" 100%", "100%"),
"Animation keyframes wasn't injected correctly"
)
assertEquals(
".${customPrefix}animationClass { animation: 2s ease-in 0s 1 alternate none running ${customPrefix}bounce; }".trimIndent(),
rules[1],
"Animation class wasn't injected correctly"
)
}
}

30
html/core/src/jsTest/kotlin/css/StyleSheetTests.kt

@ -42,4 +42,34 @@ class StyleSheetTests {
)
}
@Test
fun stylesheetCorrectlyUsingIncomingPrefix() {
val testPrefixParent = "test_prefix_parent-"
val testPrefixChild = "test_prefix_child-"
val styleSheet = object : StyleSheet(customPrefix = testPrefixParent) {
val someClassName by style {
color(Color.red)
}
}
val childStyleSheet = object : StyleSheet(customPrefix = testPrefixChild, styleSheet) {
val someClassName by style {
color(Color.green)
}
}
assertContentEquals(
listOf(".${testPrefixParent}someClassName { color: red;}", ".${testPrefixChild}someClassName { color: green;}"),
styleSheet.serializeRules(),
"styleSheet rules"
)
assertContentEquals(
listOf(".${testPrefixParent}someClassName { color: red;}", ".${testPrefixChild}someClassName { color: green;}"),
childStyleSheet.serializeRules(),
"childStyleSheet rules"
)
}
}
Loading…
Cancel
Save