├── modules
├── preview-processor
│ ├── .gitignore
│ ├── src
│ │ └── main
│ │ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── services
│ │ │ │ ├── com.google.devtools.ksp.processing.SymbolProcessorProvider
│ │ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
│ │ │ └── kotlin
│ │ │ ├── PreviewComponentRegistrar.kt
│ │ │ ├── MakePreviewPublicFirExtensionRegistrar.kt
│ │ │ └── PreviewProcessor.kt
│ └── build.gradle.kts
├── preview-processor-test
│ ├── .gitignore
│ ├── src
│ │ ├── jvmMain
│ │ │ └── kotlin
│ │ │ │ └── util
│ │ │ │ ├── AssertableFile.kt
│ │ │ │ └── Compilation.kt
│ │ └── androidUnitTest
│ │ │ └── kotlin
│ │ │ ├── PreviewProcessorAndroidTest.kt
│ │ │ └── MakePreviewPublicFirExtensionRegistrarAndroidTest.kt
│ └── build.gradle.kts
├── gallery
│ ├── src
│ │ ├── androidMain
│ │ │ ├── res
│ │ │ │ ├── values
│ │ │ │ │ └── strings.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ ├── ic_launcher.png
│ │ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ │ ├── ic_launcher.xml
│ │ │ │ │ └── ic_launcher_round.xml
│ │ │ │ └── drawable-v24
│ │ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrain
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ └── material3
│ │ │ │ └── StoryContent.android.kt
│ │ ├── commonMain
│ │ │ ├── composeResources
│ │ │ │ ├── font
│ │ │ │ │ └── JetBrainsMono-Regular.woff2
│ │ │ │ └── drawable
│ │ │ │ │ ├── info.xml
│ │ │ │ │ ├── check.xml
│ │ │ │ │ ├── story_widget_icon.xml
│ │ │ │ │ ├── arrow_back.xml
│ │ │ │ │ ├── copy.xml
│ │ │ │ │ ├── wrench.xml
│ │ │ │ │ ├── compose-multiplatform.xml
│ │ │ │ │ └── palette.xml
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ ├── platform
│ │ │ │ └── StoryGallery.kt
│ │ │ │ ├── compose
│ │ │ │ ├── CompositionLocal.kt
│ │ │ │ └── AnnotatedString.kt
│ │ │ │ ├── utils
│ │ │ │ └── cast.kt
│ │ │ │ ├── material3
│ │ │ │ ├── Navigation.kt
│ │ │ │ ├── ReponsiveNavigationDrawer.kt
│ │ │ │ ├── StorytaleGalleryApp.kt
│ │ │ │ └── EmbeddedStoryView.kt
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ └── StoryGalleryTheme.kt
│ │ │ │ └── component
│ │ │ │ │ ├── CenterRow.kt
│ │ │ │ │ └── Gap.kt
│ │ │ │ ├── Gallery.kt
│ │ │ │ └── story
│ │ │ │ └── code
│ │ │ │ └── CodeBlock.kt
│ │ ├── jvmMain
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ ├── platform
│ │ │ │ └── StoryGallery.jvm.kt
│ │ │ │ └── material3
│ │ │ │ └── StoryContent.jvm.kt
│ │ ├── mobileMain
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ └── platform
│ │ │ │ └── StoryGallery.mobile.kt
│ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ └── material3
│ │ │ │ └── StoryContent.ios.kt
│ │ ├── jsMain
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── gallery
│ │ │ │ ├── material3
│ │ │ │ └── StoryContent.js.kt
│ │ │ │ └── platform
│ │ │ │ └── StoryGallery.js.kt
│ │ └── wasmJsMain
│ │ │ └── kotlin
│ │ │ └── org
│ │ │ └── jetbrains
│ │ │ └── compose
│ │ │ └── storytale
│ │ │ └── gallery
│ │ │ ├── material3
│ │ │ └── StoryContent.wasmJs.kt
│ │ │ └── platform
│ │ │ └── StoryGallery.wasm.kt
│ └── build.gradle.kts
├── gradle-plugin
│ ├── src
│ │ └── main
│ │ │ ├── resources
│ │ │ └── StorytaleXCode.zip
│ │ │ └── kotlin
│ │ │ └── org
│ │ │ └── jetbrains
│ │ │ └── compose
│ │ │ └── storytale
│ │ │ └── plugin
│ │ │ ├── WasmMultiplatformTasks.kt
│ │ │ ├── NativeSourceGeneratorTask.kt
│ │ │ ├── StorytaleExtension.kt
│ │ │ ├── JsSourceGeneratorTask.kt
│ │ │ ├── NativeCopyResourcesTask.kt
│ │ │ ├── JvmSourceGeneratorTask.kt
│ │ │ ├── StorytaleGradlePlugin.kt
│ │ │ ├── AndroidSourceGeneratorTask.kt
│ │ │ ├── WasmSourceGeneratorTask.kt
│ │ │ ├── JsMultiplatformTasks.kt
│ │ │ ├── Utils.kt
│ │ │ └── JvmMultiplatformTasks.kt
│ ├── settings.gradle.kts
│ └── build.gradle.kts
├── runtime-api
│ ├── src
│ │ ├── androidMain
│ │ │ └── AndroidManifest.xml
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── org
│ │ │ └── jetbrains
│ │ │ └── compose
│ │ │ └── storytale
│ │ │ ├── StoryParameter.kt
│ │ │ ├── PreviewParameter.kt
│ │ │ ├── Story.kt
│ │ │ ├── StoryDelegate.kt
│ │ │ └── StoryParameterDelegate.kt
│ └── build.gradle.kts
├── dokka-plugin
│ ├── src
│ │ ├── main
│ │ │ ├── resources
│ │ │ │ └── META-INF
│ │ │ │ │ └── services
│ │ │ │ │ └── org.jetbrains.dokka.plugability.DokkaPlugin
│ │ │ └── kotlin
│ │ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── dokka
│ │ │ │ └── storytale
│ │ │ │ ├── StorytalePlugin.kt
│ │ │ │ └── StoryHtmlRenderer.kt
│ │ └── test
│ │ │ ├── resources
│ │ │ └── storytale
│ │ │ │ └── module.kt
│ │ │ └── kotlin
│ │ │ └── org
│ │ │ └── jetbrains
│ │ │ └── dokka
│ │ │ └── storytale
│ │ │ └── StorytalePluginTest.kt
│ └── build.gradle.kts
└── compiler-plugin
│ ├── src
│ ├── resources
│ │ └── META-INF
│ │ │ └── services
│ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
│ ├── kotlin
│ │ └── StorytaleComponentRegistrar.kt
│ └── test
│ │ └── kotlin
│ │ ├── MentionAllStoriesGettersInsideMainFunctionLoweringTest.kt
│ │ ├── ReplaceStoryCallWithItsSuccessorWithCodeParameterTest.kt
│ │ ├── util
│ │ └── StorytaleTest.kt
│ │ └── StorytaleComponentRegistrarLearningTest.kt
│ └── build.gradle.kts
├── .fleet
├── settings.json
└── receipt.json
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── examples
├── iosApp
│ ├── Configuration
│ │ └── Config.xcconfig
│ └── iosApp
│ │ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── app-icon-1024.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ ├── iOSApp.swift
│ │ ├── ContentView.swift
│ │ └── Info.plist
├── src
│ ├── androidMain
│ │ ├── res
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── kotlin
│ │ │ ├── Platform.android.kt
│ │ │ └── org
│ │ │ │ └── jetbrains
│ │ │ │ └── storytale
│ │ │ │ └── example
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── commonMain
│ │ ├── kotlin
│ │ │ ├── Platform.kt
│ │ │ ├── Greeting.kt
│ │ │ ├── ComposeLogo.kt
│ │ │ ├── PrimaryButton.kt
│ │ │ └── App.kt
│ │ └── composeResources
│ │ │ └── drawable
│ │ │ └── compose-multiplatform.xml
│ ├── jsMain
│ │ └── kotlin
│ │ │ └── Platform.js.kt
│ ├── wasmJsMain
│ │ └── kotlin
│ │ │ └── Platform.wasmJs.kt
│ ├── desktopMain
│ │ └── kotlin
│ │ │ ├── Platform.jvm.kt
│ │ │ └── main.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── Platform.ios.kt
│ └── commonStories
│ │ └── kotlin
│ │ ├── ComposeLogo.story.kt
│ │ └── PrimaryButton.story.kt
├── .gitignore
├── .fleet
│ └── receipt.json
└── build.gradle.kts
├── gallery-demo
└── src
│ ├── wasmJsMain
│ ├── kotlin
│ │ ├── org
│ │ │ └── jetbrains
│ │ │ │ └── compose
│ │ │ │ └── storytale
│ │ │ │ └── generated
│ │ │ │ └── MainGenerated.kt
│ │ └── storytale
│ │ │ └── gallery
│ │ │ └── demo
│ │ │ └── Main.kt
│ └── resources
│ │ ├── index.html
│ │ └── styles.css
│ ├── desktopMain
│ └── kotlin
│ │ ├── storytale
│ │ └── gallery
│ │ │ └── demo
│ │ │ ├── Main.kt
│ │ │ └── PreviewButton.kt
│ │ └── org
│ │ └── jetbrains
│ │ └── compose
│ │ └── storytale
│ │ └── generated
│ │ └── MainGenerated.kt
│ └── commonMain
│ └── kotlin
│ └── storytale
│ └── gallery
│ └── demo
│ ├── Checkbox.story.kt
│ ├── PreviewCheckbox.kt
│ ├── Simple inputs.story.kt
│ ├── Parameters.story.kt
│ ├── ColorfulMosaic.story.kt
│ └── Buttons.story.kt
├── .github
├── dependabot.yml
└── workflows
│ ├── spotless.yaml
│ ├── smokebuild.yaml
│ └── ci.yaml
├── .gitignore
├── gradle.properties
├── .editorconfig
├── settings.gradle.kts
├── gradlew.bat
└── README.md
/modules/preview-processor/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/modules/preview-processor-test/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.fleet/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "run.destination.stop.already.running": "Always"
3 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/examples/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=org.jetbrains.storytale.example
3 | APP_NAME=example
--------------------------------------------------------------------------------
/examples/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | example
3 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Storytale
3 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/examples/src/commonMain/kotlin/Platform.kt:
--------------------------------------------------------------------------------
1 | interface Platform {
2 | val name: String
3 | }
4 |
5 | expect fun getPlatform(): Platform
6 |
--------------------------------------------------------------------------------
/modules/preview-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:
--------------------------------------------------------------------------------
1 | PreviewProcessor$Provider
2 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/resources/StorytaleXCode.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gradle-plugin/src/main/resources/StorytaleXCode.zip
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/modules/runtime-api/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/examples/src/jsMain/kotlin/Platform.js.kt:
--------------------------------------------------------------------------------
1 | class JsPlatform : Platform {
2 | override val name: String = "Web with Kotlin/Js"
3 | }
4 |
5 | actual fun getPlatform(): Platform = JsPlatform()
6 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/examples/src/commonMain/kotlin/Greeting.kt:
--------------------------------------------------------------------------------
1 | class Greeting {
2 | private val platform = getPlatform()
3 |
4 | fun greet(): String {
5 | return "Hello, ${platform.name}!"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/src/wasmJsMain/kotlin/Platform.wasmJs.kt:
--------------------------------------------------------------------------------
1 | class WasmPlatform : Platform {
2 | override val name: String = "Web with Kotlin/Wasm"
3 | }
4 |
5 | actual fun getPlatform(): Platform = WasmPlatform()
6 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/font/JetBrainsMono-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin/Storytale/HEAD/modules/gallery/src/commonMain/composeResources/font/JetBrainsMono-Regular.woff2
--------------------------------------------------------------------------------
/examples/src/desktopMain/kotlin/Platform.jvm.kt:
--------------------------------------------------------------------------------
1 | class JVMPlatform : Platform {
2 | override val name: String = "Java ${System.getProperty("java.version")}"
3 | }
4 |
5 | actual fun getPlatform(): Platform = JVMPlatform()
6 |
--------------------------------------------------------------------------------
/gallery-demo/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/generated/MainGenerated.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.generated
2 |
3 | @Suppress("ktlint:standard:function-naming")
4 | fun MainViewController() {}
5 |
--------------------------------------------------------------------------------
/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/Main.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import org.jetbrains.compose.storytale.generated.MainViewController
4 |
5 | fun main() {
6 | MainViewController()
7 | }
8 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/src/androidMain/kotlin/Platform.android.kt:
--------------------------------------------------------------------------------
1 | import android.os.Build
2 |
3 | class AndroidPlatform : Platform {
4 | override val name: String = "Android ${Build.VERSION.SDK_INT}"
5 | }
6 |
7 | actual fun getPlatform(): Platform = AndroidPlatform()
8 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/src/main/resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 | #
4 |
5 | org.jetbrains.dokka.storytale.StorytalePlugin
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.platform
2 |
3 | import androidx.navigation.NavHostController
4 |
5 | expect suspend fun bindNavigation(navController: NavHostController)
6 |
--------------------------------------------------------------------------------
/examples/src/iosMain/kotlin/Platform.ios.kt:
--------------------------------------------------------------------------------
1 | import platform.UIKit.UIDevice
2 |
3 | class IOSPlatform : Platform {
4 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
5 | }
6 |
7 | actual fun getPlatform(): Platform = IOSPlatform()
8 |
--------------------------------------------------------------------------------
/modules/gallery/src/jvmMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.jvm.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.platform
2 |
3 | import androidx.navigation.NavHostController
4 |
5 | actual suspend fun bindNavigation(navController: NavHostController) {
6 | }
7 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/compose/CompositionLocal.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.compose
2 |
3 | inline fun noCompositionLocalProvided(): T {
4 | error("CompositionLocal ${T::class.simpleName} not present")
5 | }
6 |
--------------------------------------------------------------------------------
/modules/gallery/src/mobileMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.mobile.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.platform
2 |
3 | import androidx.navigation.NavHostController
4 |
5 | actual suspend fun bindNavigation(navController: NavHostController) {
6 | }
7 |
--------------------------------------------------------------------------------
/examples/src/desktopMain/kotlin/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.Window
2 | import androidx.compose.ui.window.application
3 |
4 | fun main() = application {
5 | Window(
6 | onCloseRequest = ::exitApplication,
7 | title = "example",
8 | ) {
9 | App()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | versionCatalogs {
7 | create("libs") {
8 | from(files("../../gradle/libs.versions.toml"))
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/utils/cast.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.utils
2 |
3 | @Throws(ClassCastException::class)
4 | internal inline fun Any?.cast(): T = this as T
5 |
6 | internal inline fun Any?.castOrNull(): T? = this as? T
7 |
--------------------------------------------------------------------------------
/modules/preview-processor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.jvm)
3 | }
4 |
5 | dependencies {
6 | implementation(libs.kotlin.poet)
7 | implementation(libs.ksp.api)
8 | implementation(kotlin("compiler-embeddable"))
9 | implementation("org.jetbrains.compose.storytale:gradle-plugin")
10 | }
11 |
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 08 17:33:08 CST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/examples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/Navigation.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | @SerialName("story")
8 | data class StoryScreen(val storyName: String)
9 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/src/test/resources/storytale/module.kt:
--------------------------------------------------------------------------------
1 | package storytale
2 |
3 | /**
4 | * Test function with story annotation
5 | * @story https://storytale.io/stories/12345
6 | */
7 | fun testWithStory(): String = "Story function"
8 |
9 | /**
10 | * Regular function without story
11 | */
12 | fun testWithoutStory(): String = "Regular function"
13 |
--------------------------------------------------------------------------------
/modules/gallery/src/iosMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.ios.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 |
6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) {
7 | this.setClipEntry(ClipEntry.withPlainText(code))
8 | }
9 |
--------------------------------------------------------------------------------
/modules/gallery/src/jsMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.js.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 |
6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) {
7 | this.setClipEntry(ClipEntry.withPlainText(code))
8 | }
9 |
--------------------------------------------------------------------------------
/modules/gallery/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 |
6 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) {
7 | this.setClipEntry(ClipEntry.withPlainText(code))
8 | }
9 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .kotlin
3 | .gradle
4 | **/build/
5 | xcuserdata
6 | !src/**/build/
7 | local.properties
8 | .idea
9 | .DS_Store
10 | captures
11 | .externalNativeBuild
12 | .cxx
13 | *.xcodeproj/*
14 | !*.xcodeproj/project.pbxproj
15 | !*.xcodeproj/xcshareddata/
16 | !*.xcodeproj/project.xcworkspace/
17 | !*.xcworkspace/contents.xcworkspacedata
18 | **/xcshareddata/WorkspaceSettings.xcsettings
19 |
--------------------------------------------------------------------------------
/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryParameter.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale
2 |
3 | import androidx.compose.runtime.MutableState
4 | import kotlin.reflect.KClass
5 |
6 | class StoryParameter(
7 | val name: String,
8 | val type: KClass<*>,
9 | val values: List<*>?,
10 | val label: String?,
11 | val state: MutableState,
12 | )
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | **/build/
4 | xcuserdata
5 | !src/**/build/
6 | local.properties
7 | .idea
8 | .DS_Store
9 | captures
10 | .kotlin
11 | .externalNativeBuild
12 | .cxx
13 | /example/build
14 | *.xcodeproj/*
15 | !*.xcodeproj/project.pbxproj
16 | !*.xcodeproj/xcshareddata/
17 | !*.xcodeproj/project.xcworkspace/
18 | !*.xcworkspace/contents.xcworkspacedata
19 | **/xcshareddata/WorkspaceSettings.xcsettings
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
3 | #Gradle
4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
5 |
6 | #Android
7 | android.nonTransitiveRClass=true
8 | android.useAndroidX=true
9 |
10 | #Compose
11 | org.jetbrains.compose.experimental.jscanvas.enabled=true
12 |
13 | kotlin.jvm.target.validation.mode=ignore
14 |
15 | #Publication
16 | storytale.deploy.version=0.0.4-SNAPSHOT
17 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/compose/AnnotatedString.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.compose
2 |
3 | import androidx.compose.ui.text.AnnotatedString.Builder
4 | import androidx.compose.ui.text.SpanStyle
5 | import androidx.compose.ui.text.withStyle
6 |
7 | fun Builder.text(text: String, style: SpanStyle = SpanStyle()) = withStyle(style = style) {
8 | append(text)
9 | }
10 |
--------------------------------------------------------------------------------
/gallery-demo/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Storytale Gallery Demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/modules/gallery/src/jvmMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StoryContent.jvm.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.ui.platform.ClipEntry
4 | import androidx.compose.ui.platform.Clipboard
5 | import java.awt.datatransfer.StringSelection
6 |
7 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) {
8 | setClipEntry(ClipEntry(StringSelection(code)))
9 | }
10 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/kotlin/org/jetbrain/compose/storytale/gallery/material3/StoryContent.android.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import android.content.ClipData
4 | import androidx.compose.ui.platform.ClipEntry
5 | import androidx.compose.ui.platform.Clipboard
6 |
7 | internal actual suspend fun Clipboard.copyCodeToClipboard(code: String) {
8 | val clipData = ClipData.newPlainText("Copied Code", code)
9 | setClipEntry(ClipEntry(clipData))
10 | }
11 |
--------------------------------------------------------------------------------
/modules/gallery/src/jsMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.js.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.platform
2 |
3 | import androidx.navigation.ExperimentalBrowserHistoryApi
4 | import androidx.navigation.NavHostController
5 | import androidx.navigation.bindToNavigation
6 | import kotlinx.browser.window
7 |
8 | @OptIn(ExperimentalBrowserHistoryApi::class)
9 | actual suspend fun bindNavigation(navController: NavHostController) {
10 | window.bindToNavigation(navController)
11 | }
12 |
--------------------------------------------------------------------------------
/modules/gallery/src/wasmJsMain/kotlin/org/jetbrains/compose/storytale/gallery/platform/StoryGallery.wasm.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.platform
2 |
3 | import androidx.navigation.ExperimentalBrowserHistoryApi
4 | import androidx.navigation.NavHostController
5 | import androidx.navigation.bindToNavigation
6 | import kotlinx.browser.window
7 |
8 | actual suspend fun bindNavigation(navController: NavHostController) {
9 | @OptIn(ExperimentalBrowserHistoryApi::class)
10 | window.bindToNavigation(navController)
11 | }
12 |
--------------------------------------------------------------------------------
/examples/src/commonStories/kotlin/ComposeLogo.story.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.foundation.layout.size
2 | import androidx.compose.ui.Modifier
3 | import androidx.compose.ui.unit.dp
4 | import org.jetbrains.compose.storytale.story
5 |
6 | val `ComposeLogo Default State` by story {
7 | ComposeLogo()
8 | }
9 |
10 | val `ComposeLogo Small` by story {
11 | ComposeLogo(Modifier.size(300.dp))
12 | }
13 |
14 | val `ComposeLogo Medium` by story {
15 | ComposeLogo(Modifier.size(600.dp))
16 | }
17 |
18 | val `ComposeLogo Large` by story {
19 | ComposeLogo(Modifier.size(900.dp))
20 | }
21 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/src/commonMain/kotlin/ComposeLogo.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.foundation.Image
2 | import androidx.compose.runtime.Composable
3 | import androidx.compose.ui.Modifier
4 | import org.jetbrains.compose.resources.painterResource
5 | import org.jetbrains.compose.storytale.example.Res
6 | import org.jetbrains.compose.storytale.example.compose_multiplatform
7 |
8 | @Composable
9 | fun ComposeLogo(
10 | modifier: Modifier = Modifier,
11 | ) {
12 | Image(
13 | painter = painterResource(Res.drawable.compose_multiplatform),
14 | contentDescription = "Compose Multiplatform Logo",
15 | modifier = modifier,
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/spotless.yaml:
--------------------------------------------------------------------------------
1 | name: Spotless Check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | spotless:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up JDK
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: '17'
20 | distribution: 'temurin'
21 | cache: gradle
22 |
23 | - name: Grant execute permission for gradlew
24 | run: chmod +x ./gradlew
25 |
26 | - name: Run Spotless Check
27 | run: ./gradlew spotlessCheck
28 |
--------------------------------------------------------------------------------
/examples/src/androidMain/kotlin/org/jetbrains/storytale/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.storytale.example
2 |
3 | import App
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.tooling.preview.Preview
9 |
10 | class MainActivity : ComponentActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 |
14 | setContent {
15 | App()
16 | }
17 | }
18 | }
19 |
20 | @Preview
21 | @Composable
22 | private fun AppAndroidPreview() {
23 | App()
24 | }
25 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/info.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
14 |
17 |
--------------------------------------------------------------------------------
/modules/preview-processor/src/main/kotlin/PreviewComponentRegistrar.kt:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
2 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
3 | import org.jetbrains.kotlin.config.CompilerConfiguration
4 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter
5 |
6 | @OptIn(ExperimentalCompilerApi::class)
7 | class PreviewComponentRegistrar : CompilerPluginRegistrar() {
8 | override val supportsK2: Boolean get() = true
9 |
10 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
11 | FirExtensionRegistrarAdapter.registerExtension(MakePreviewPublicFirExtensionRegistrar())
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gallery-demo/src/desktopMain/kotlin/org/jetbrains/compose/storytale/generated/MainGenerated.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.generated
2 |
3 | import androidx.compose.ui.unit.dp
4 | import androidx.compose.ui.window.WindowState
5 | import androidx.compose.ui.window.singleWindowApplication
6 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp
7 |
8 | // To let the Storytale compiler plugin add the initializations for stories
9 | @Suppress("ktlint:standard:function-naming")
10 | fun MainViewController() {
11 | singleWindowApplication(
12 | state = WindowState(width = 800.dp, height = 800.dp),
13 | alwaysOnTop = true,
14 | ) {
15 | StorytaleGalleryApp()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.fleet/receipt.json:
--------------------------------------------------------------------------------
1 | // Project generated by Kotlin Multiplatform Wizard
2 | {
3 | "spec": {
4 | "template_id": "kmt",
5 | "targets": {
6 | "android": {
7 | "ui": [
8 | "compose"
9 | ]
10 | },
11 | "ios": {
12 | "ui": [
13 | "compose"
14 | ]
15 | },
16 | "desktop": {
17 | "ui": [
18 | "compose"
19 | ]
20 | },
21 | "web": {
22 | "ui": [
23 | "compose"
24 | ]
25 | }
26 | }
27 | },
28 | "timestamp": "2024-05-08T16:38:45.656209060Z"
29 | }
--------------------------------------------------------------------------------
/examples/.fleet/receipt.json:
--------------------------------------------------------------------------------
1 | // Project generated by Kotlin Multiplatform Wizard
2 | {
3 | "spec": {
4 | "template_id": "kmt",
5 | "targets": {
6 | "android": {
7 | "ui": [
8 | "compose"
9 | ]
10 | },
11 | "ios": {
12 | "ui": [
13 | "compose"
14 | ]
15 | },
16 | "desktop": {
17 | "ui": [
18 | "compose"
19 | ]
20 | },
21 | "web": {
22 | "ui": [
23 | "compose"
24 | ]
25 | }
26 | }
27 | },
28 | "timestamp": "2024-05-26T06:06:11.223920641Z"
29 | }
--------------------------------------------------------------------------------
/modules/preview-processor/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2010-2023 JetBrains s.r.o.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | PreviewComponentRegistrar
18 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/check.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Checkbox.story.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.material3.TriStateCheckbox
4 | import androidx.compose.ui.state.ToggleableState
5 | import androidx.compose.ui.state.ToggleableState.Indeterminate
6 | import androidx.compose.ui.state.ToggleableState.Off
7 | import androidx.compose.ui.state.ToggleableState.On
8 | import org.jetbrains.compose.storytale.story
9 |
10 | val `Check box` by story {
11 | var state by parameter(ToggleableState.entries)
12 |
13 | TriStateCheckbox(
14 | state = state,
15 | onClick = {
16 | state = when (state) {
17 | On -> Indeterminate
18 | Off -> On
19 | Indeterminate -> Off
20 | }
21 | },
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2010-2023 JetBrains s.r.o.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar
18 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/theme/StoryGalleryTheme.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.ui.theme
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.staticCompositionLocalOf
6 | import androidx.compose.ui.platform.LocalDensity
7 | import androidx.compose.ui.unit.Density
8 | import org.jetbrains.compose.storytale.gallery.compose.noCompositionLocalProvided
9 |
10 | val LocalCustomDensity = staticCompositionLocalOf { noCompositionLocalProvided() }
11 |
12 | @Composable
13 | inline fun UseCustomDensity(crossinline content: @Composable () -> Unit) {
14 | CompositionLocalProvider(LocalDensity provides LocalCustomDensity.current) {
15 | content()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/story_widget_icon.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/component/CenterRow.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.ui.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.RowScope
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Alignment
8 | import androidx.compose.ui.Modifier
9 |
10 | @Composable
11 | inline fun CenterRow(
12 | modifier: Modifier = Modifier,
13 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
14 | content: @Composable RowScope.() -> Unit,
15 | ) = Row(
16 | verticalAlignment = Alignment.CenterVertically,
17 | horizontalArrangement = horizontalArrangement,
18 | modifier = modifier,
19 | ) {
20 | content()
21 | }
22 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/kotlin/StorytaleComponentRegistrar.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.plugin.storytale.compiler
2 |
3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
4 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
5 | import org.jetbrains.kotlin.config.CompilerConfiguration
6 |
7 | class StorytaleComponentRegistrar : CompilerPluginRegistrar() {
8 | override val supportsK2: Boolean get() = true
9 |
10 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
11 | Companion.registerExtensions(this)
12 | }
13 |
14 | companion object {
15 | fun registerExtensions(extensionStorage: ExtensionStorage) = with(extensionStorage) {
16 | IrGenerationExtension.registerExtension(StorytaleLoweringExtension())
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/PreviewParameter.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.staticCompositionLocalOf
5 |
6 | @Composable
7 | inline fun previewParameter(defaultValue: T) = LocalStory.current.parameter(defaultValue)
8 |
9 | @Composable
10 | inline fun previewParameter(
11 | values: List,
12 | defaultValueIndex: Int = 0,
13 | label: String? = null,
14 | ) = LocalStory.current.parameter(values, defaultValueIndex, label)
15 |
16 | @Composable
17 | inline fun > previewParameter(defaultValue: T, label: String? = null) = LocalStory.current.parameter(defaultValue, label)
18 |
19 | @PublishedApi
20 | internal val LocalStory = staticCompositionLocalOf { Story(-1, "DefaultPreviewStory", "", "", {}) }
21 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 4
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.{kt,kts}]
11 | ij_kotlin_imports_layout = *
12 | ij_kotlin_allow_trailing_comma = true
13 | ij_kotlin_allow_trailing_comma_on_call_site = true
14 | ij_kotlin_line_break_after_multiline_when_entry = false
15 | ij_kotlin_name_count_to_use_star_import = 2147483647
16 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
17 | ij_kotlin_packages_to_use_import_on_demand = unset
18 | ktlint_code_style = intellij_idea
19 | ktlint_function_naming_ignore_when_annotated_with = Composable
20 | ktlint_standard_function-expression-body = disabled
21 | ktlint_standard_filename = disabled
22 | ktlint_compose_modifier-missing-check = disabled
23 |
24 | [*.md]
25 | trim_trailing_whitespace = false
26 |
27 | [*.xml]
28 | indent_size = 4
--------------------------------------------------------------------------------
/modules/preview-processor-test/src/jvmMain/kotlin/util/AssertableFile.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import java.io.File
4 | import org.assertj.core.util.Files
5 | import org.intellij.lang.annotations.Language
6 |
7 | data class AssertableFile(
8 | val path: String,
9 | val content: String,
10 | ) {
11 | override fun toString(): String {
12 | return buildString {
13 | append('\"')
14 | append(path)
15 | appendLine("\" has content \"\"\"")
16 | appendLine(content)
17 | appendLine("\"\"\"")
18 | }
19 | }
20 | }
21 |
22 | fun File.assertable(): AssertableFile {
23 | val it = this
24 | return path hasContent Files.contentOf(it, Charsets.UTF_8).trim()
25 | }
26 |
27 | @Suppress("NOTHING_TO_INLINE")
28 | inline infix fun String.hasContent(@Language("kotlin") content: String): AssertableFile {
29 | return AssertableFile(this, content)
30 | }
31 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/Gallery.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.navigation.compose.rememberNavController
6 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp
7 | import org.jetbrains.compose.storytale.gallery.platform.bindNavigation
8 |
9 | @Suppress("unused") // The call of this function is added implicitly in the generated code
10 | @Composable
11 | fun Gallery() {
12 | StorytaleGalleryApp()
13 | }
14 |
15 | @Composable
16 | fun Gallery(isEmbedded: Boolean) {
17 | val navHostController = rememberNavController()
18 |
19 | StorytaleGalleryApp(isEmbedded = isEmbedded, navHostController = navHostController)
20 |
21 | LaunchedEffect(Unit) {
22 | bindNavigation(navHostController)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/arrow_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/PreviewCheckbox.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.material3.TriStateCheckbox
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.state.ToggleableState
6 | import androidx.compose.ui.state.ToggleableState.Indeterminate
7 | import androidx.compose.ui.state.ToggleableState.Off
8 | import androidx.compose.ui.state.ToggleableState.On
9 | import org.jetbrains.compose.storytale.previewParameter
10 | import org.jetbrains.compose.ui.tooling.preview.Preview
11 |
12 | @Preview
13 | @Composable
14 | @Suppress("ktlint")
15 | internal fun PreviewCheckbox() {
16 | var state by previewParameter(ToggleableState.entries)
17 |
18 | TriStateCheckbox(
19 | state = state,
20 | onClick = {
21 | state = when (state) {
22 | On -> Indeterminate
23 | Off -> On
24 | Indeterminate -> Off
25 | }
26 | },
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/examples/src/commonMain/kotlin/PrimaryButton.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.material.Button
2 | import androidx.compose.material.Text
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.Modifier
5 | import androidx.compose.ui.unit.sp
6 |
7 | @Composable
8 | fun PrimaryButton(
9 | text: String,
10 | modifier: Modifier = Modifier,
11 | onClick: () -> Unit = {},
12 | enabled: Boolean = true,
13 | size: PrimaryButtonSize = PrimaryButtonSize.Medium,
14 | ) {
15 | Button(
16 | modifier = modifier,
17 | onClick = onClick,
18 | enabled = enabled,
19 | ) {
20 | Text(
21 | text = text,
22 | fontSize = when (size) {
23 | PrimaryButtonSize.Small -> 12.sp
24 | PrimaryButtonSize.Medium -> 14.sp
25 | PrimaryButtonSize.Large -> 20.sp
26 | },
27 | )
28 | }
29 | }
30 |
31 | enum class PrimaryButtonSize {
32 | Small,
33 | Medium,
34 | Large,
35 | }
36 |
--------------------------------------------------------------------------------
/examples/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/src/commonStories/kotlin/PrimaryButton.story.kt:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.storytale.story
2 |
3 | val PrimaryButton by story {
4 | val size by parameter(PrimaryButtonSize.Medium)
5 | val enabled by parameter(true)
6 | val text by parameter("Click Me")
7 |
8 | PrimaryButton(text = text, enabled = enabled, size = size)
9 | }
10 |
11 | val `PrimaryButton Default State` by story {
12 | PrimaryButton("Click Me")
13 | }
14 |
15 | val `PrimaryButton Disabled` by story {
16 | PrimaryButton(
17 | text = "Click Me",
18 | enabled = false,
19 | )
20 | }
21 |
22 | val `PrimaryButton Food` by story {
23 | val foodEmojiList = listOf(
24 | "Apple 🍎",
25 | "Banana 🍌",
26 | "Cherry 🍒",
27 | "Grapes 🍇",
28 | "Strawberry 🍓",
29 | "Watermelon 🍉",
30 | "Pineapple 🍍",
31 | "Pizza 🍕",
32 | "Burger 🍔",
33 | "Fries 🍟",
34 | "Ice Cream 🍦",
35 | "Cake 🍰",
36 | "Coffee ☕",
37 | "Beer 🍺",
38 | )
39 |
40 | val food by parameter(foodEmojiList)
41 | PrimaryButton(text = food)
42 | }
43 |
--------------------------------------------------------------------------------
/gallery-demo/src/wasmJsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 |
18 | html, body {
19 | width: 100%;
20 | height: 100%;
21 | margin: 0;
22 | padding: 0;
23 | overflow: hidden;
24 | }
25 |
26 | h1 {
27 | padding: 0 16px;
28 | font-size: 2em;
29 | }
30 |
31 | #composeApplication {
32 | width: 100%;
33 | height: 100%;
34 | }
35 |
36 | body:has(textarea) {
37 | background-color: aliceblue;
38 | }
39 |
40 | body:has(textarea:focus) {
41 | background-color: #eaffe3;
42 | }
--------------------------------------------------------------------------------
/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/Story.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale
2 |
3 | import androidx.compose.runtime.Composable
4 | import kotlin.enums.enumEntries
5 |
6 | data class Story(
7 | val id: Int,
8 | val name: String,
9 | val group: String,
10 | val code: String,
11 | val content: @Composable Story.() -> Unit,
12 | ) {
13 | @PublishedApi
14 | internal val nameToParameterMapping = linkedMapOf>() // using linkedMap to keep the order
15 | val parameters inline get() = nameToParameterMapping.values.toList()
16 |
17 | inline fun parameter(defaultValue: T) = StoryParameterDelegate(this, T::class, defaultValue)
18 |
19 | inline fun parameter(values: List, defaultValueIndex: Int = 0, label: String? = null) = StoryListParameterDelegate(this, T::class, values, defaultValueIndex, label)
20 |
21 | inline fun > parameter(defaultValue: T, label: String? = null): StoryListParameterDelegate {
22 | val enumEntries = enumEntries()
23 | return StoryListParameterDelegate(this, T::class, enumEntries, enumEntries.indexOf(defaultValue), label)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/smokebuild.yaml:
--------------------------------------------------------------------------------
1 | name: Smoke build
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | push:
7 | branches:
8 | - "main"
9 |
10 | jobs:
11 | build:
12 | name: Publish to MavenLocal and Build Examples
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Set up JDK
16 | uses: actions/setup-java@v2
17 | with:
18 | distribution: 'adopt'
19 | java-version: '17'
20 |
21 | - name: Checkout code
22 | uses: actions/checkout@v3
23 |
24 | - name: Test Dokka Plugin
25 | run: ./gradlew :modules:dokka-plugin:test
26 |
27 | - name: Publish to Maven Local
28 | run: ./gradlew publishToMavenLocal
29 |
30 | - name: Build gallery-demo
31 | run: |
32 | ./gradlew :gallery-demo:wasmJsBrowserDevelopmentExecutableDistribution :gallery-demo:packageReleaseUberJarForCurrentOS
33 |
34 | - name: Build Stories for Wasm target
35 | run: |
36 | cd examples
37 | ../gradlew wasmJsBrowserStoriesProductionExecutableDistribution
38 |
39 | - name: Build Stories for Desktop target
40 | run: |
41 | cd examples
42 | ../gradlew desktopStorytaleGenerate
43 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Simple inputs.story.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("ktlint:standard:property-naming")
2 |
3 | package storytale.gallery.demo
4 |
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.ButtonDefaults
7 | import androidx.compose.material3.Checkbox
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Switch
10 | import androidx.compose.material3.Text
11 | import org.jetbrains.compose.storytale.story
12 |
13 | val Button by story {
14 | val Label by parameter("Click Me")
15 | val Enabled by parameter(true)
16 | val bgColorAlpha by parameter(1f)
17 |
18 | Button(
19 | enabled = Enabled,
20 | onClick = {},
21 | colors = ButtonDefaults.buttonColors().copy(
22 | containerColor = MaterialTheme.colorScheme.primary.copy(alpha = bgColorAlpha),
23 | ),
24 | ) {
25 | Text(Label)
26 | }
27 | }
28 |
29 | val Checkbox by story {
30 | var checked by parameter(false)
31 | Checkbox(checked, onCheckedChange = { checked = it })
32 | }
33 |
34 | val Switch by story {
35 | var checked by parameter(false)
36 | Switch(checked, onCheckedChange = { checked = it })
37 | }
38 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/copy.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
23 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/WasmMultiplatformTasks.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import org.gradle.api.Project
4 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
5 |
6 | fun Project.processWasmCompilation(extension: StorytaleExtension, target: KotlinJsIrTarget) {
7 | project.logger.info("Configuring storytale using Kotlin/Wasm")
8 | createWasmStorytaleGenerateSourceTask(extension, target)
9 |
10 | val storytaleCompilation = createWasmAndJsStorytaleCompilation(extension, target)
11 |
12 | createWasmAndJsStorytaleExecTask(storytaleCompilation)
13 | }
14 |
15 | private fun Project.createWasmStorytaleGenerateSourceTask(extension: StorytaleExtension, target: KotlinJsIrTarget) {
16 | val storytaleBuildDir = extension.getBuildDirectory(target)
17 | tasks.register("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}", WasmSourceGeneratorTask::class.java) {
18 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
19 | description = "Generate Wasm source files for '${target.name}'"
20 | title = target.name
21 | outputSourcesDir = file("$storytaleBuildDir/sources")
22 | outputResourcesDir = file("$storytaleBuildDir/resources")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | `maven-publish`
7 | alias(libs.plugins.kotlin.jvm)
8 | alias(libs.plugins.dokka)
9 | }
10 |
11 | group = "org.jetbrains.compose.storytale"
12 |
13 | repositories {
14 | mavenCentral()
15 | }
16 |
17 | dependencies {
18 | compileOnly(libs.dokka.core)
19 | implementation(libs.dokka.base)
20 | implementation(libs.kotlinx.html)
21 |
22 | testImplementation(libs.jsoup)
23 | testImplementation(libs.dokka.test.api)
24 | testImplementation(libs.dokka.base.test.utils)
25 | testImplementation(libs.dokka.base)
26 | testImplementation(libs.junit.api)
27 | testImplementation(libs.kotlin.test)
28 | }
29 |
30 | tasks.withType {
31 | useJUnitPlatform()
32 | }
33 |
34 | kotlin {
35 | jvmToolchain(11)
36 | }
37 |
38 | val emptyJavadocJar by tasks.registering(Jar::class) {
39 | archiveClassifier.set("javadoc")
40 | }
41 |
42 | publishing {
43 | publications {
44 | create("maven") {
45 | artifactId = "storytale-dokka-plugin"
46 | from(components["kotlin"])
47 | }
48 | withType {
49 | artifact(emptyJavadocJar)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/modules/runtime-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.androidLibrary)
6 | alias(libs.plugins.kotlinMultiplatform)
7 | alias(libs.plugins.jetbrainsCompose)
8 | alias(libs.plugins.compose.compiler)
9 | `maven-publish`
10 | }
11 |
12 | kotlin {
13 | wasmJs {
14 | browser()
15 | }
16 | js {
17 | browser()
18 | }
19 | iosX64()
20 | iosArm64()
21 | iosSimulatorArm64()
22 | jvm {
23 | compilerOptions {
24 | jvmTarget.set(JvmTarget.JVM_11)
25 | }
26 | }
27 | androidTarget {
28 | publishLibraryVariants("release")
29 |
30 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
31 | compilerOptions {
32 | jvmTarget.set(JvmTarget.JVM_11)
33 | }
34 | }
35 |
36 | sourceSets {
37 | commonMain.dependencies {
38 | implementation(compose.runtime)
39 | }
40 | }
41 | }
42 |
43 | group = "org.jetbrains.compose.storytale"
44 |
45 | publishing {}
46 |
47 | android {
48 | namespace = "org.jetbrains.compose.storytale.runtime"
49 | compileSdk = libs.versions.android.compileSdk.get().toInt()
50 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
51 | }
52 |
--------------------------------------------------------------------------------
/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryDelegate.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import kotlin.reflect.KProperty
6 |
7 | val storiesStorage = mutableListOf()
8 |
9 | inline fun story(
10 | code: String = "",
11 | group: String = "",
12 | crossinline content: @Composable Story.() -> Unit,
13 | ) = StoryDelegate({ content() }, group, code)
14 |
15 | class StoryDelegate(
16 | private val content: @Composable Story.() -> Unit,
17 | private val group: String = "",
18 | private val code: String = "",
19 | ) {
20 | private lateinit var instance: Story
21 |
22 | operator fun getValue(thisRef: Any?, property: KProperty<*>): Story {
23 | return instance
24 | }
25 |
26 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): StoryDelegate {
27 | val wrappedContent: @Composable Story.() -> Unit = {
28 | val story = this
29 | val delegate = this@StoryDelegate
30 |
31 | CompositionLocalProvider(LocalStory provides story) {
32 | delegate.content(story)
33 | }
34 | }
35 |
36 | instance = Story(storiesStorage.size, property.name, group, code, wrappedContent)
37 | .also(storiesStorage::add)
38 | return this
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/src/commonMain/kotlin/App.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.animation.AnimatedVisibility
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 |
17 | @Composable
18 | fun App(modifier: Modifier = Modifier) {
19 | var showContent by remember { mutableStateOf(false) }
20 |
21 | MaterialTheme {
22 | Column(
23 | modifier = modifier.fillMaxWidth(),
24 | horizontalAlignment = Alignment.CenterHorizontally,
25 | ) {
26 | PrimaryButton(text = "Click Me", onClick = { showContent = !showContent })
27 | AnimatedVisibility(showContent) {
28 | val greeting = remember { Greeting().greet() }
29 | Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
30 | ComposeLogo(Modifier.size(600.dp))
31 | Text("Compose: $greeting")
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Parameters.story.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.foundation.layout.size
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.Text
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.unit.dp
8 | import org.jetbrains.compose.storytale.story
9 |
10 | val foodEmojiList = listOf(
11 | "Apple 🍎",
12 | "Banana 🍌",
13 | "Cherry 🍒",
14 | "Grapes 🍇",
15 | "Strawberry 🍓",
16 | "Watermelon 🍉",
17 | "Pineapple 🍍",
18 | "Pizza 🍕",
19 | "Burger 🍔",
20 | "Fries 🍟",
21 | "Ice Cream 🍦",
22 | "Cake 🍰",
23 | "Coffee ☕",
24 | "Beer 🍺",
25 | )
26 |
27 | val `List Parameters` by story {
28 | val food by parameter(foodEmojiList)
29 |
30 | Button(onClick = {}) {
31 | Text(food)
32 | }
33 | }
34 |
35 | enum class PrimaryButtonSize {
36 | XS,
37 | Small,
38 | Medium,
39 | Large,
40 | XL,
41 | XXL,
42 | }
43 |
44 | val `Enum Parameters` by story {
45 | val size by parameter(PrimaryButtonSize.Medium, label = null)
46 |
47 | Button(
48 | onClick = {},
49 | modifier = Modifier.size(
50 | when (size) {
51 | PrimaryButtonSize.XS -> 60.dp
52 | PrimaryButtonSize.Small -> 90.dp
53 | PrimaryButtonSize.Medium -> 120.dp
54 | PrimaryButtonSize.Large -> 150.dp
55 | PrimaryButtonSize.XL -> 170.dp
56 | PrimaryButtonSize.XXL -> 200.dp
57 | },
58 | ),
59 | ) {
60 | Text(size.toString())
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/ColorfulMosaic.story.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.aspectRatio
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.lazy.grid.GridCells
9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
10 | import androidx.compose.foundation.lazy.grid.items
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.draw.clip
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.unit.dp
16 | import org.jetbrains.compose.storytale.story
17 |
18 | val ColorfulMosaic by story {
19 | val colors = listOf(
20 | Color(0xFFE57373), Color(0xFFF06292), Color(0xFFBA68C8), Color(0xFF9575CD),
21 | Color(0xFF7986CB), Color(0xFF64B5F6), Color(0xFF4FC3F7), Color(0xFF4DD0E1),
22 | Color(0xFF4DB6AC), Color(0xFF81C784), Color(0xFFAED581), Color(0xFFFFD54F),
23 | Color(0xFFFFB74D), Color(0xFFFF8A65), Color(0xFFA1887F), Color(0xFF90A4AE),
24 | )
25 |
26 | LazyVerticalGrid(
27 | columns = GridCells.Adaptive(120.dp),
28 | modifier = Modifier.fillMaxSize(),
29 | ) {
30 | items(colors) { color ->
31 | Box(
32 | modifier = Modifier
33 | .padding(8.dp)
34 | .aspectRatio(1f)
35 | .clip(RoundedCornerShape(8.dp))
36 | .background(color),
37 | )
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/test/kotlin/MentionAllStoriesGettersInsideMainFunctionLoweringTest.kt:
--------------------------------------------------------------------------------
1 | import com.tschuchort.compiletesting.KotlinCompilation
2 | import kotlin.test.Test
3 | import org.assertj.core.api.Assertions.assertThat
4 | import org.jetbrains.compose.storytale.storiesStorage
5 | import util.invokeMainController
6 | import util.storytaleTest
7 |
8 | class MentionAllStoriesGettersInsideMainFunctionLoweringTest {
9 | @Test
10 | fun `all stories are registered after compilation and main invocation`() {
11 | val result = storytaleTest {
12 | "Group1.story.kt" hasContent """
13 | package storytale.gallery.demo
14 | import org.jetbrains.compose.storytale.story
15 |
16 | val Story1 by story { }
17 | val Story2 by story { }
18 | """
19 | "Group2.story.kt" hasContent """
20 | package storytale.gallery.demo
21 | import org.jetbrains.compose.storytale.story
22 |
23 | val Story3 by story { }
24 | """
25 | "Main.kt" hasContent """
26 | package org.jetbrains.compose.storytale.generated
27 | fun MainViewController() { }
28 | """
29 | }
30 |
31 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
32 |
33 | result.invokeMainController()
34 |
35 | // CRAZY!!!
36 | assertThat(storiesStorage).hasSize(3)
37 | assertThat(storiesStorage.map { it.name })
38 | .containsExactlyInAnyOrder("Story1", "Story2", "Story3")
39 | assertThat(storiesStorage.map { it.group }.toSet())
40 | .containsExactlyInAnyOrder("Group1", "Group2")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/wrench.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/gallery-demo/src/wasmJsMain/kotlin/storytale/gallery/demo/Main.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.runtime.LaunchedEffect
4 | import androidx.compose.ui.window.ComposeViewport
5 | import androidx.navigation.ExperimentalBrowserHistoryApi
6 | import androidx.navigation.bindToNavigation
7 | import androidx.navigation.compose.rememberNavController
8 | import kotlinx.browser.window
9 | import org.jetbrains.compose.resources.ExperimentalResourceApi
10 | import org.jetbrains.compose.resources.preloadFont
11 | import org.jetbrains.compose.storytale.gallery.material3.StorytaleGalleryApp
12 | import org.jetbrains.compose.storytale.gallery.material3.openFullScreenStory
13 | import org.jetbrains.compose.storytale.gallery.story.code.JetBrainsMonoRegularRes
14 | import org.jetbrains.compose.storytale.generated.MainViewController
15 |
16 | @OptIn(ExperimentalResourceApi::class, ExperimentalBrowserHistoryApi::class)
17 | fun main() {
18 | MainViewController() // Storytale compiler will initialize the stories
19 |
20 | openFullScreenStory = { story, _ ->
21 | window.open(window.location.origin + "/#story/" + story.storyName)
22 | }
23 |
24 | val useEmbedded = window.location.search.contains("embedded=true")
25 |
26 | ComposeViewport(viewportContainerId = "composeApplication") {
27 | val hasResourcePreloadCompleted = preloadFont(JetBrainsMonoRegularRes).value != null
28 | val navHostController = rememberNavController()
29 |
30 | if (hasResourcePreloadCompleted) {
31 | StorytaleGalleryApp(isEmbedded = useEmbedded, navHostController)
32 |
33 | LaunchedEffect(Unit) {
34 | window.bindToNavigation(navHostController)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/NativeSourceGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import com.squareup.kotlinpoet.ClassName
4 | import com.squareup.kotlinpoet.FileSpec
5 | import java.io.File
6 | import org.gradle.api.DefaultTask
7 | import org.gradle.api.tasks.CacheableTask
8 | import org.gradle.api.tasks.Input
9 | import org.gradle.api.tasks.OutputDirectory
10 | import org.gradle.api.tasks.TaskAction
11 |
12 | @CacheableTask
13 | open class NativeSourceGeneratorTask : DefaultTask() {
14 | @Input
15 | lateinit var title: String
16 |
17 | @OutputDirectory
18 | lateinit var outputResourcesDir: File
19 |
20 | @OutputDirectory
21 | lateinit var outputSourcesDir: File
22 |
23 | @TaskAction
24 | fun generate() {
25 | cleanup(outputSourcesDir)
26 | cleanup(outputResourcesDir)
27 |
28 | generateSources()
29 | }
30 |
31 | private fun generateSources() {
32 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply {
33 | addImport("androidx.compose.ui.window", "ComposeUIViewController")
34 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery")
35 |
36 | val uIViewControllerType = ClassName("platform.UIKit", "UIViewController")
37 | function("MainViewController") {
38 | returns(uIViewControllerType)
39 | addStatement(
40 | """
41 | |return ComposeUIViewController {
42 | | Gallery()
43 | |}
44 | """.trimMargin(),
45 | )
46 | }
47 | }.build()
48 |
49 | file.writeTo(outputSourcesDir)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | `java-gradle-plugin`
6 | `maven-publish`
7 | alias(libs.plugins.buildTimeConfig)
8 | }
9 |
10 | gradlePlugin {
11 | plugins {
12 | create("storytale") {
13 | id = "org.jetbrains.compose.storytale"
14 | implementationClass = "org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin"
15 | }
16 | }
17 | }
18 |
19 | dependencies {
20 | implementation(libs.kotlin.gradle.plugin)
21 | implementation(libs.android.gradle.plugin)
22 | implementation(libs.kotlin.compiler.embeddable)
23 | implementation(libs.compose.gradle.plugin)
24 | implementation(libs.compose.hot.reload.plugin)
25 | implementation(libs.kotlin.poet)
26 | }
27 |
28 | group = "org.jetbrains.compose.storytale"
29 |
30 | val emptyJavadocJar by tasks.registering(Jar::class) {
31 | archiveClassifier.set("javadoc")
32 | }
33 |
34 | publishing {
35 | publications {
36 | create("maven") {
37 | artifactId = "gradle-plugin"
38 | from(components["kotlin"])
39 | }
40 | withType {
41 | artifact(emptyJavadocJar)
42 | }
43 | }
44 | }
45 |
46 | tasks.withType().configureEach {
47 | friendPaths.setFrom(libraries)
48 | }
49 |
50 | buildTimeConfig {
51 | config {
52 | packageName.set("org.jetbrains.compose.storytale.plugin")
53 | objectName.set("BuildTimeConfig")
54 | destination.set(project.layout.buildDirectory.get().asFile)
55 |
56 | configProperties {
57 | val projectVersion: String by string(version as String)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/test/kotlin/ReplaceStoryCallWithItsSuccessorWithCodeParameterTest.kt:
--------------------------------------------------------------------------------
1 | import com.tschuchort.compiletesting.KotlinCompilation
2 | import org.assertj.core.api.Assertions.assertThat
3 | import org.jetbrains.compose.storytale.storiesStorage
4 | import org.junit.Test
5 | import util.invokeMainController
6 | import util.storytaleTest
7 |
8 | class ReplaceStoryCallWithItsSuccessorWithCodeParameterTest {
9 | @Test
10 | fun `replace story call extracts code parameter`() {
11 | val result = storytaleTest {
12 | "Group1.story.kt" hasContent """
13 | package storytale.gallery.demo
14 | import org.jetbrains.compose.storytale.story
15 | import androidx.compose.foundation.text.BasicText
16 |
17 | val Story1 by story {
18 | val text by parameter("Hello World")
19 | BasicText(text)
20 | }
21 | """
22 | "Main.kt" hasContent """
23 | package org.jetbrains.compose.storytale.generated
24 | fun MainViewController() { }
25 | """
26 | }
27 |
28 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
29 |
30 | result.invokeMainController()
31 |
32 | assertThat(storiesStorage).hasSize(1)
33 | with(storiesStorage.first()) {
34 | assertThat(name).isEqualTo("Story1")
35 | assertThat(group).isEqualTo("Group1")
36 | assertThat(code).isEqualTo(
37 | """
38 | |val text by parameter("Hello World")
39 | |BasicText(text)
40 | """
41 | .trimMargin()
42 | .trim('\n'),
43 | )
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | `maven-publish`
6 | kotlin("jvm")
7 | alias(libs.plugins.jetbrainsCompose)
8 | alias(libs.plugins.compose.compiler)
9 | }
10 | dependencies {
11 | compileOnly(compose.runtime)
12 | implementation(kotlin("compiler-embeddable"))
13 | testImplementation(compose.foundation)
14 | testImplementation(compose.material3)
15 | testImplementation(compose.runtime)
16 | testImplementation(compose.ui)
17 | testImplementation(kotlin("compose-compiler-plugin-embeddable"))
18 | testImplementation(kotlin("test"))
19 | testImplementation(libs.assertj.core)
20 | testImplementation(libs.junit)
21 | testImplementation(libs.kotlinCompileTesting.core)
22 | testImplementation(project(":modules:runtime-api"))
23 | }
24 |
25 | sourceSets {
26 | val main by getting {
27 | kotlin.srcDirs("src/kotlin")
28 | resources.srcDir("src/resources")
29 | }
30 | val test by getting {
31 | kotlin.srcDirs("src/test/kotlin")
32 | resources.srcDir("src/test/resources")
33 | }
34 | }
35 |
36 | group = "org.jetbrains.compose.storytale"
37 |
38 | val emptyJavadocJar by tasks.registering(Jar::class) {
39 | archiveClassifier.set("javadoc")
40 | }
41 |
42 | publishing {
43 | publications {
44 | create("maven") {
45 | artifactId = "compiler-plugin"
46 | from(components["kotlin"])
47 | }
48 | withType {
49 | artifact(emptyJavadocJar)
50 | }
51 | }
52 | }
53 |
54 | tasks.withType>().configureEach {
55 | compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi")
56 | }
57 |
--------------------------------------------------------------------------------
/examples/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/modules/gallery/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/StorytaleExtension.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import org.gradle.api.Project
4 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
5 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
6 |
7 | open class StorytaleExtension(internal val project: Project) {
8 | var buildDir: String = "storytale"
9 |
10 | val multiplatformExtension = run {
11 | val multiplatformClass =
12 | tryGetClass(
13 | className = "org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension",
14 | )
15 | multiplatformClass?.let { project.extensions.findByType(it) } ?: error("UNEXPECTED")
16 | }
17 |
18 | val targets = multiplatformExtension.targets
19 |
20 | val mainStoriesSourceSet by lazy {
21 | multiplatformExtension
22 | .sourceSets
23 | .create("common${StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX}")
24 | .apply { setupCommonStoriesSourceSetDependencies(this) }
25 | }
26 |
27 | internal fun setupCommonStoriesSourceSetDependencies(sourceSet: KotlinSourceSet) {
28 | with(sourceSet) {
29 | dependencies {
30 | implementation("org.jetbrains.compose.storytale:gallery:${StorytaleGradlePlugin.VERSION}")
31 | implementation("org.jetbrains.compose.storytale:runtime-api:${StorytaleGradlePlugin.VERSION}")
32 | }
33 | }
34 | }
35 |
36 | private fun Any.tryGetClass(className: String): Class? {
37 | val classLoader = javaClass.classLoader
38 | return try {
39 | @Suppress("UNCHECKED_CAST")
40 | Class.forName(className, false, classLoader) as Class
41 | } catch (e: ClassNotFoundException) {
42 | null
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Publish Application on GitHub Pages
2 | # Don't add something here until the plugin is available on Maven, the CI is quite long :(
3 | on: [workflow_dispatch]
4 | jobs:
5 | # Build job
6 | build:
7 | name: Build Kotlin/Wasm Stories
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Set up JDK
11 | uses: actions/setup-java@v2
12 | with:
13 | distribution: 'adopt'
14 | java-version: '17'
15 |
16 | - name: Checkout code
17 | uses: actions/checkout@v3
18 |
19 | - name: Publish local Storytale plugin
20 | run: ./gradlew publishToMavenLocal
21 |
22 | - name: Run Gradle Tasks
23 | run: |
24 | cd examples
25 | ../gradlew wasmJsBrowserStoriesProductionExecutableDistribution
26 |
27 | - name: Fix permissions
28 | run: |
29 | chmod -v -R +rX "examples/build/dist/wasmJs/StoriesProductionExecutable/" | while read line; do
30 | echo "::warning title=Invalid file permissions automatically fixed::$line"
31 | done
32 |
33 | - name: Upload Pages artifact
34 | uses: actions/upload-pages-artifact@v3
35 | with:
36 | path: examples/build/dist/wasmJs/StoriesProductionExecutable
37 |
38 | deploy:
39 | # Add a dependency to the build job
40 | needs: build
41 |
42 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
43 | permissions:
44 | pages: write # to deploy to Pages
45 | id-token: write # to verify the deployment originates from an appropriate source
46 |
47 | # Deploy to the github-pages environment
48 | environment:
49 | name: github-pages
50 | url: ${{ steps.deployment.outputs.page_url }}
51 |
52 | # Specify runner + deployment step
53 | runs-on: ubuntu-latest
54 | steps:
55 | - name: Deploy to GitHub Pages
56 | id: deployment
57 | uses: actions/deploy-pages@v4
58 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JsSourceGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import com.squareup.kotlinpoet.FileSpec
4 | import java.io.File
5 | import org.gradle.api.DefaultTask
6 | import org.gradle.api.tasks.CacheableTask
7 | import org.gradle.api.tasks.Input
8 | import org.gradle.api.tasks.OutputDirectory
9 | import org.gradle.api.tasks.TaskAction
10 | import org.jetbrains.kotlin.incremental.createDirectory
11 |
12 | @CacheableTask
13 | open class JsSourceGeneratorTask : DefaultTask() {
14 |
15 | @Input
16 | lateinit var title: String
17 |
18 | @OutputDirectory
19 | lateinit var outputResourcesDir: File
20 |
21 | @OutputDirectory
22 | lateinit var outputSourcesDir: File
23 |
24 | @TaskAction
25 | fun generate() {
26 | cleanup(outputSourcesDir)
27 | cleanup(outputResourcesDir)
28 |
29 | generateSources()
30 | generateResources()
31 | }
32 |
33 | private fun generateSources() {
34 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply {
35 | addImport("org.jetbrains.skiko.wasm", "onWasmReady")
36 | addMainFileImports()
37 | addMainViewControllerFun()
38 | function("main") {
39 | addStatement("onWasmReady { MainViewController() }")
40 | }
41 | }.build()
42 |
43 | file.writeTo(outputSourcesDir)
44 | }
45 |
46 | private fun generateResources() {
47 | if (!outputResourcesDir.exists()) {
48 | outputResourcesDir.createDirectory()
49 | }
50 |
51 | val stylesFile = File(outputResourcesDir, "styles.css")
52 | stylesFile.writeText(webStylesCssContent)
53 |
54 | val index = File(outputResourcesDir, "index.html")
55 | index.writeText(webIndexHtmlContent(SCRIPT_FILE_NAME))
56 | }
57 |
58 | companion object {
59 | internal const val SCRIPT_FILE_NAME = "composeApp.js"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/modules/preview-processor-test/src/androidUnitTest/kotlin/PreviewProcessorAndroidTest.kt:
--------------------------------------------------------------------------------
1 | import com.tschuchort.compiletesting.KotlinCompilation
2 | import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor
3 | import org.assertj.core.api.Assertions.assertThat
4 | import org.junit.Test
5 | import util.assertableGeneratedKspSources
6 | import util.hasContent
7 | import util.storytaleTest
8 |
9 | class PreviewProcessorAndroidTest {
10 |
11 | @Test
12 | fun `generates story for Androidx Android Preview function`() {
13 | val compilation = storytaleTest {
14 | "AndroidButton.kt" hasContent """
15 | package storytale.gallery.demo
16 |
17 | @androidx.compose.runtime.Composable
18 | fun AndroidButton() { }
19 |
20 | @androidx.compose.ui.tooling.preview.Preview
21 | @androidx.compose.runtime.Composable
22 | fun PreviewAndroidButton() {
23 | AndroidButton()
24 | }
25 | """
26 | }
27 | val result = compilation
28 | .compile()
29 |
30 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
31 |
32 | assertThat(result.sourcesGeneratedBySymbolProcessor.toList()).hasSize(1)
33 |
34 | assertThat(result.assertableGeneratedKspSources(compilation))
35 | .containsExactlyInAnyOrder(
36 | "kotlin/org/jetbrains/compose/storytale/generated/Previews.story.kt" hasContent """
37 | |package org.jetbrains.compose.storytale.generated
38 | |
39 | |import org.jetbrains.compose.storytale.Story
40 | |import org.jetbrains.compose.storytale.story
41 | |import storytale.gallery.demo.PreviewAndroidButton
42 | |
43 | |public val AndroidButton: Story by story {
44 | | PreviewAndroidButton()
45 | |}
46 | """.trimMargin(),
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "Storytale"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 | repositories {
6 | google {
7 | mavenContent {
8 | includeGroupAndSubgroups("androidx")
9 | includeGroupAndSubgroups("com.android")
10 | includeGroupAndSubgroups("com.google")
11 | }
12 | }
13 | mavenCentral()
14 | gradlePluginPortal()
15 | mavenLocal()
16 | }
17 | }
18 |
19 | dependencyResolutionManagement {
20 | repositories {
21 | google {
22 | mavenContent {
23 | includeGroupAndSubgroups("androidx")
24 | includeGroupAndSubgroups("com.android")
25 | includeGroupAndSubgroups("com.google")
26 | }
27 | }
28 | mavenCentral()
29 | mavenLocal()
30 | exclusiveContent {
31 | forRepository {
32 | maven {
33 | url = uri("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/")
34 | }
35 | }
36 | filter {
37 | includeGroupByRegex("org\\.jetbrains\\.intellij\\.deps.*")
38 | }
39 | }
40 | exclusiveContent {
41 | forRepository {
42 | maven {
43 | url = uri("https://www.jetbrains.com/intellij-repository/releases/")
44 | }
45 | }
46 | filter {
47 | includeGroup("com.jetbrains.intellij.platform")
48 | }
49 | }
50 | }
51 | }
52 |
53 | plugins {
54 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
55 | }
56 |
57 | include(":examples")
58 | include(":gallery-demo")
59 | include(":modules:gallery")
60 | includeBuild("modules/gradle-plugin")
61 | include(":modules:compiler-plugin")
62 | include(":modules:dokka-plugin")
63 | include(":modules:runtime-api")
64 | include(":modules:preview-processor")
65 | include(":modules:preview-processor-test")
66 |
--------------------------------------------------------------------------------
/gallery-demo/src/desktopMain/kotlin/storytale/gallery/demo/PreviewButton.kt:
--------------------------------------------------------------------------------
1 | package storytale.gallery.demo
2 |
3 | import androidx.compose.foundation.layout.Spacer
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.AddCircle
7 | import androidx.compose.material3.ExtendedFloatingActionButton
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.SegmentedButton
11 | import androidx.compose.material3.SegmentedButtonDefaults
12 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableIntStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.dp
19 | import org.jetbrains.compose.storytale.previewParameter
20 |
21 | @org.jetbrains.compose.ui.tooling.preview.Preview
22 | @Composable
23 | @Suppress("ktlint")
24 | private fun PreviewExtendedFAB() {
25 | val bgColor by previewParameter(MaterialTheme.colorScheme.primary)
26 |
27 | ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) {
28 | Icon(imageVector = Icons.Default.AddCircle, contentDescription = null)
29 | Spacer(Modifier.padding(4.dp))
30 | Text("Extended")
31 | }
32 | }
33 |
34 | @androidx.compose.desktop.ui.tooling.preview.Preview
35 | @Composable
36 | @Suppress("ktlint")
37 | private fun PreviewSegmentedButton() {
38 | val selectedIndex = remember { mutableIntStateOf(0) }
39 |
40 | SingleChoiceSegmentedButtonRow {
41 | repeat(3) { index ->
42 | SegmentedButton(
43 | selected = index == selectedIndex.value,
44 | onClick = { selectedIndex.value = index },
45 | shape = SegmentedButtonDefaults.itemShape(index, 3),
46 | ) {
47 | Text("Button $index", modifier = Modifier.padding(4.dp))
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/NativeCopyResourcesTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import org.gradle.api.DefaultTask
4 | import org.gradle.api.file.DirectoryProperty
5 | import org.gradle.api.file.FileCollection
6 | import org.gradle.api.provider.Property
7 | import org.gradle.api.tasks.Input
8 | import org.gradle.api.tasks.InputFiles
9 | import org.gradle.api.tasks.OutputDirectory
10 | import org.gradle.api.tasks.PathSensitive
11 | import org.gradle.api.tasks.PathSensitivity
12 | import org.gradle.api.tasks.TaskAction
13 |
14 | internal abstract class NativeCopyResourcesTask : DefaultTask() {
15 | @get:Input
16 | abstract val xcodeTargetPlatform: Property
17 |
18 | @get:Input
19 | abstract val xcodeTargetArchs: Property
20 |
21 | @get:PathSensitive(PathSensitivity.ABSOLUTE)
22 | @get:InputFiles
23 | abstract val resourceFiles: Property
24 |
25 | @get:OutputDirectory
26 | abstract val outputDir: DirectoryProperty
27 |
28 | @TaskAction
29 | fun run() {
30 | val outputDir = outputDir.get().asFile
31 | outputDir.deleteRecursively()
32 | outputDir.mkdirs()
33 | logger.info("Clean ${outputDir.path}")
34 |
35 | resourceFiles.get().forEach { dir ->
36 | if (dir.exists() && dir.isDirectory) {
37 | logger.info("Copy '${dir.path}' to '${outputDir.path}'")
38 | dir.walkTopDown().filter { !it.isDirectory && !it.isHidden }.forEach { file ->
39 | val targetFile = outputDir.resolve(file.relativeTo(dir))
40 | if (targetFile.exists()) {
41 | logger.info("Skip [already exists] '${file.path}'")
42 | } else {
43 | logger.info(" -> '${file.path}'")
44 | file.copyTo(targetFile)
45 | }
46 | }
47 | } else {
48 | logger.info("File '${dir.path}' is not a dir or doesn't exist")
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JvmSourceGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import com.squareup.kotlinpoet.FileSpec
4 | import java.io.File
5 | import org.gradle.api.DefaultTask
6 | import org.gradle.api.tasks.CacheableTask
7 | import org.gradle.api.tasks.Input
8 | import org.gradle.api.tasks.OutputDirectory
9 | import org.gradle.api.tasks.TaskAction
10 |
11 | @CacheableTask
12 | open class JvmSourceGeneratorTask : DefaultTask() {
13 | @Input
14 | lateinit var title: String
15 |
16 | @OutputDirectory
17 | lateinit var outputResourcesDir: File
18 |
19 | @OutputDirectory
20 | lateinit var outputSourcesDir: File
21 |
22 | @TaskAction
23 | fun generate() {
24 | cleanup(outputSourcesDir)
25 | cleanup(outputResourcesDir)
26 |
27 | generateSources()
28 | }
29 |
30 | private fun generateSources() {
31 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply {
32 | addImport("androidx.compose.ui.window", "Window")
33 | addImport("androidx.compose.ui.window", "application")
34 | addImport("androidx.compose.ui.unit", "DpSize")
35 | addImport("androidx.compose.ui.unit", "dp")
36 | addImport("androidx.compose.ui.window", "rememberWindowState")
37 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery")
38 |
39 | function("MainViewController") {
40 | addStatement(
41 | """
42 | |application {
43 | | Window(
44 | | onCloseRequest = ::exitApplication,
45 | | title = "example",
46 | | state = rememberWindowState(size = DpSize(1440.dp, 920.dp))
47 | | ) {
48 | | Gallery()
49 | | }
50 | |}
51 | """.trimMargin(),
52 | )
53 | }
54 |
55 | function("main") {
56 | addStatement("MainViewController()")
57 | }
58 | }.build()
59 |
60 | file.writeTo(outputSourcesDir)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/composeResources/drawable/palette.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/ReponsiveNavigationDrawer.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.foundation.layout.ColumnScope
4 | import androidx.compose.foundation.layout.width
5 | import androidx.compose.material3.DismissibleDrawerSheet
6 | import androidx.compose.material3.DismissibleNavigationDrawer
7 | import androidx.compose.material3.DrawerState
8 | import androidx.compose.material3.PermanentDrawerSheet
9 | import androidx.compose.material3.PermanentNavigationDrawer
10 | import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.movableContentOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 | import androidx.window.core.layout.WindowWidthSizeClass
17 |
18 | @Composable
19 | fun ResponsiveNavigationDrawer(
20 | drawerState: DrawerState,
21 | drawerContent: @Composable ColumnScope.() -> Unit,
22 | content: @Composable () -> Unit,
23 | ) {
24 | val currentWindowWidthClass = currentWindowAdaptiveInfo().windowSizeClass.windowWidthSizeClass
25 | val isWideWindow = currentWindowWidthClass == WindowWidthSizeClass.EXPANDED
26 | val isSmallWindow = currentWindowWidthClass == WindowWidthSizeClass.COMPACT
27 |
28 | val drawerModifier = Modifier.width(280.dp)
29 | if (!isWideWindow) {
30 | DismissibleNavigationDrawer(
31 | drawerContent = remember {
32 | movableContentOf {
33 | DismissibleDrawerSheet(
34 | drawerState = drawerState,
35 | content = drawerContent,
36 | modifier = drawerModifier,
37 | )
38 | }
39 | },
40 | content = content,
41 | drawerState = drawerState,
42 | gesturesEnabled = isSmallWindow,
43 | )
44 | } else {
45 | PermanentNavigationDrawer(
46 | drawerContent = {
47 | PermanentDrawerSheet(content = drawerContent, modifier = drawerModifier)
48 | },
49 | content = content,
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/src/main/kotlin/org/jetbrains/dokka/storytale/StorytalePlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package org.jetbrains.dokka.storytale
6 |
7 | import org.jetbrains.dokka.CoreExtensions
8 | import org.jetbrains.dokka.DokkaConfiguration
9 | import org.jetbrains.dokka.base.DokkaBase
10 | import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider
11 | import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder
12 | import org.jetbrains.dokka.model.doc.CustomTagWrapper
13 | import org.jetbrains.dokka.model.doc.Text
14 | import org.jetbrains.dokka.plugability.DokkaPlugin
15 | import org.jetbrains.dokka.plugability.DokkaPluginApiPreview
16 | import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement
17 |
18 | public class StorytalePlugin : DokkaPlugin() {
19 | private val dokkaBase by lazy { plugin() }
20 |
21 | public val storytaleRenderer by extending {
22 | CoreExtensions.renderer providing { StoryHtmlRenderer(it) } override dokkaBase.htmlRenderer
23 | }
24 |
25 | public val storyTagContentProvider by extending {
26 | plugin().customTagContentProvider with StoryTagContentProvider order {
27 | before(plugin().sinceKotlinTagContentProvider)
28 | }
29 | }
30 |
31 | @OptIn(DokkaPluginApiPreview::class)
32 | override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement
33 | }
34 |
35 | private const val STORY_TAG = "story"
36 |
37 | private object StoryTagContentProvider : CustomTagContentProvider {
38 | override fun isApplicable(customTag: CustomTagWrapper): Boolean = customTag.name == STORY_TAG
39 |
40 | override fun DocumentableContentBuilder.contentForDescription(
41 | sourceSet: DokkaConfiguration.DokkaSourceSet,
42 | customTag: CustomTagWrapper,
43 | ) {
44 | val url = customTag.extractUrl() ?: return
45 |
46 | codeBlock(language = STORY_IFRAME_CODE) { text(url) }
47 | }
48 |
49 | private fun CustomTagWrapper.extractUrl(): String? = (((root.children.firstOrNull() as? org.jetbrains.dokka.model.doc.P)?.children as? ArrayList)?.singleOrNull() as? Text)?.body
50 | }
51 |
--------------------------------------------------------------------------------
/modules/runtime-api/src/commonMain/kotlin/org/jetbrains/compose/storytale/StoryParameterDelegate.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.compose.runtime.mutableStateOf
5 | import kotlin.reflect.KClass
6 | import kotlin.reflect.KProperty
7 |
8 | class StoryParameterDelegate(
9 | private val story: Story,
10 | private val type: KClass<*>,
11 | private val defaultValue: T,
12 | private val label: String? = null,
13 | ) {
14 | @Suppress("UNCHECKED_CAST")
15 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = story.nameToParameterMapping.getValue(property.name).state.value as T
16 |
17 | @Suppress("UNCHECKED_CAST")
18 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
19 | (story.nameToParameterMapping.getValue(property.name).state as MutableState).value = value
20 | }
21 |
22 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = also {
23 | story.nameToParameterMapping.getOrPut(property.name) {
24 | StoryParameter(property.name, type, values = null, label, mutableStateOf(defaultValue))
25 | }
26 | }
27 | }
28 |
29 | class StoryListParameterDelegate(
30 | private val story: Story,
31 | private val type: KClass<*>,
32 | private val list: List,
33 | private val defaultValueIndex: Int,
34 | private val label: String? = null,
35 | ) {
36 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = list[story.nameToParameterMapping.getValue(property.name).state.value as Int]
37 |
38 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
39 | val index = list.indexOf(value)
40 | check(index != -1) {
41 | "Cannot find element with value: $value in ${list.joinToString()}."
42 | }
43 | @Suppress("UNCHECKED_CAST")
44 | (story.nameToParameterMapping.getValue(property.name).state as MutableState).value =
45 | index
46 | }
47 |
48 | operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) = also {
49 | story.nameToParameterMapping.getOrPut(property.name) {
50 | require(list.isNotEmpty()) { "List cannot be empty" }
51 | StoryParameter(property.name, type, list, label, mutableStateOf(defaultValueIndex))
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/modules/preview-processor/src/main/kotlin/MakePreviewPublicFirExtensionRegistrar.kt:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.descriptors.Visibilities
2 | import org.jetbrains.kotlin.fir.FirSession
3 | import org.jetbrains.kotlin.fir.declarations.FirDeclaration
4 | import org.jetbrains.kotlin.fir.declarations.FirDeclarationStatus
5 | import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction
6 | import org.jetbrains.kotlin.fir.declarations.hasAnnotation
7 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar
8 | import org.jetbrains.kotlin.fir.extensions.FirStatusTransformerExtension
9 | import org.jetbrains.kotlin.fir.extensions.transform
10 | import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
11 | import org.jetbrains.kotlin.name.ClassId
12 | import org.jetbrains.kotlin.name.FqName
13 |
14 | class MakePreviewPublicFirExtensionRegistrar : FirExtensionRegistrar() {
15 | companion object {
16 | private val PREVIEW_ANNOTATION_FQ_NAME = FqName("org.jetbrains.compose.ui.tooling.preview.Preview")
17 | private val ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.ui.tooling.preview.Preview")
18 | private val DESKTOP_PREVIEW_ANNOTATION_FQ_NAME = FqName("androidx.compose.desktop.ui.tooling.preview.Preview")
19 |
20 | private val PREVIEW_CLASS_IDS = setOf(
21 | ClassId.topLevel(PREVIEW_ANNOTATION_FQ_NAME),
22 | ClassId.topLevel(ANDROIDX_PREVIEW_ANNOTATION_FQ_NAME),
23 | ClassId.topLevel(DESKTOP_PREVIEW_ANNOTATION_FQ_NAME),
24 | )
25 | }
26 |
27 | override fun ExtensionRegistrarContext.configurePlugin() {
28 | +FirStatusTransformerExtension.Factory { session -> Extension(session) }
29 | }
30 |
31 | class Extension(session: FirSession) : FirStatusTransformerExtension(session) {
32 | override fun needTransformStatus(declaration: FirDeclaration): Boolean {
33 | if (declaration !is FirSimpleFunction) return false
34 | return PREVIEW_CLASS_IDS.any { declaration.hasAnnotation(it, session) }
35 | }
36 |
37 | override fun transformStatus(
38 | status: FirDeclarationStatus,
39 | function: FirSimpleFunction,
40 | containingClass: FirClassLikeSymbol<*>?,
41 | isLocal: Boolean,
42 | ): FirDeclarationStatus {
43 | return status.transform(visibility = Visibilities.Public)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/test/kotlin/util/StorytaleTest.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar
4 | import com.tschuchort.compiletesting.JvmCompilationResult
5 | import com.tschuchort.compiletesting.KotlinCompilation
6 | import com.tschuchort.compiletesting.SourceFile
7 | import kotlin.contracts.ExperimentalContracts
8 | import kotlin.contracts.InvocationKind
9 | import kotlin.contracts.contract
10 | import org.assertj.core.api.Assertions
11 | import org.intellij.lang.annotations.Language
12 | import org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar
13 | import org.jetbrains.compose.storytale.storiesStorage
14 |
15 | @OptIn(ExperimentalContracts::class)
16 | fun storytaleTest(
17 | compilationBuilder: KotlinCompilation.() -> Unit = {},
18 | testSourceBuilder: StorytaleTestSourceScope.() -> Unit,
19 | ): JvmCompilationResult {
20 | contract {
21 | callsInPlace(compilationBuilder, InvocationKind.EXACTLY_ONCE)
22 | callsInPlace(testSourceBuilder, InvocationKind.EXACTLY_ONCE)
23 | }
24 | storiesStorage.clear()
25 |
26 | val testSourceScope = object : StorytaleTestSourceScope {
27 | val sources = mutableListOf()
28 | override fun String.hasContent(content: String) {
29 | sources.add(SourceFile.Companion.kotlin(this, content))
30 | }
31 | }
32 |
33 | val compilationSetup = KotlinCompilation().apply {
34 | compilerPluginRegistrars =
35 | listOf(StorytaleComponentRegistrar(), ComposePluginRegistrar())
36 | inheritClassPath = true
37 | messageOutputStream = System.out // see diagnostics in real time
38 | jvmTarget = "17"
39 | verbose = false
40 | }
41 |
42 | compilationSetup.compilationBuilder()
43 | testSourceScope.testSourceBuilder()
44 |
45 | compilationSetup.sources = testSourceScope.sources
46 |
47 | return compilationSetup.compile()
48 | }
49 |
50 | interface StorytaleTestSourceScope {
51 | infix fun String.hasContent(@Language("kotlin") content: String)
52 | }
53 |
54 | fun JvmCompilationResult.invokeMainController() {
55 | val clazz = classLoader.loadClass("org.jetbrains.compose.storytale.generated.MainKt")
56 |
57 | Assertions.assertThat(clazz).isNotNull
58 | Assertions.assertThat(clazz).hasDeclaredMethods("MainViewController")
59 |
60 | clazz.getMethod("MainViewController").invoke(null)
61 | }
62 |
--------------------------------------------------------------------------------
/modules/preview-processor-test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.gradle.kotlin.dsl.compileOnly
2 | import org.gradle.kotlin.dsl.kotlin
3 | import org.gradle.kotlin.dsl.project
4 | import org.gradle.kotlin.dsl.withType
5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
6 |
7 | plugins {
8 | alias(libs.plugins.kotlinMultiplatform)
9 | alias(libs.plugins.androidLibrary)
10 | alias(libs.plugins.jetbrainsCompose)
11 | alias(libs.plugins.compose.compiler)
12 | }
13 |
14 | kotlin {
15 | jvm()
16 | androidTarget()
17 |
18 | sourceSets {
19 | val commonMain by getting {
20 | dependencies {
21 | compileOnly(compose.runtime)
22 | }
23 | }
24 | val jvmMain by getting {
25 | dependencies {
26 | implementation(compose.components.uiToolingPreview)
27 | implementation(project(":modules:preview-processor"))
28 | implementation(kotlin("test"))
29 |
30 | implementation(compose.runtime)
31 | implementation(kotlin("compiler-embeddable"))
32 | implementation(kotlin("compose-compiler-plugin-embeddable"))
33 | implementation(kotlin("test"))
34 | implementation(libs.assertj.core)
35 | implementation(libs.junit)
36 | implementation(libs.kotlinCompileTesting.ksp)
37 | implementation(project(":modules:runtime-api"))
38 | }
39 | }
40 | val androidUnitTest by getting {
41 | dependsOn(jvmMain)
42 |
43 | dependencies {
44 | implementation("androidx.compose.ui:ui-tooling-preview-android:1.7.0")
45 | }
46 | }
47 | val jvmTest by getting {
48 | dependencies {
49 | implementation("androidx.compose.ui:ui-tooling-preview-desktop:1.7.0")
50 | }
51 | }
52 | }
53 | }
54 |
55 | android {
56 | compileSdk = 35
57 | namespace = "org.jetbrains.compose.storytale.preview.processor.test"
58 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
59 | defaultConfig {
60 | minSdk = 24
61 | }
62 | compileOptions {
63 | sourceCompatibility = JavaVersion.VERSION_17
64 | targetCompatibility = JavaVersion.VERSION_17
65 | }
66 | }
67 |
68 | tasks.withType>().configureEach {
69 | compilerOptions.optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi")
70 | }
71 |
--------------------------------------------------------------------------------
/modules/preview-processor-test/src/jvmMain/kotlin/util/Compilation.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import PreviewComponentRegistrar
4 | import PreviewProcessor
5 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar
6 | import com.tschuchort.compiletesting.JvmCompilationResult
7 | import com.tschuchort.compiletesting.KotlinCompilation
8 | import com.tschuchort.compiletesting.SourceFile
9 | import com.tschuchort.compiletesting.kspSourcesDir
10 | import com.tschuchort.compiletesting.sourcesGeneratedBySymbolProcessor
11 | import com.tschuchort.compiletesting.symbolProcessorProviders
12 | import com.tschuchort.compiletesting.useKsp2
13 | import kotlin.contracts.ExperimentalContracts
14 | import kotlin.contracts.InvocationKind
15 | import kotlin.contracts.contract
16 | import org.intellij.lang.annotations.Language
17 | import org.jetbrains.kotlin.utils.fileUtils.descendantRelativeTo
18 |
19 | @OptIn(ExperimentalContracts::class)
20 | fun storytaleTest(
21 | compilationBuilder: KotlinCompilation.() -> Unit = {},
22 | testSourceBuilder: StorytaleTestSourceScope.() -> Unit,
23 | ): KotlinCompilation {
24 | contract {
25 | callsInPlace(compilationBuilder, InvocationKind.EXACTLY_ONCE)
26 | }
27 |
28 | val testSourceScope = object : StorytaleTestSourceScope {
29 | val sources = mutableListOf()
30 | override fun String.hasContent(content: String) {
31 | sources.add(SourceFile.Companion.kotlin(this, content))
32 | }
33 | }
34 |
35 | return KotlinCompilation().apply {
36 | compilerPluginRegistrars = listOf(
37 | ComposePluginRegistrar(),
38 | PreviewComponentRegistrar(),
39 | )
40 | useKsp2()
41 | symbolProcessorProviders.add(PreviewProcessor.Provider())
42 |
43 | // magic
44 | inheritClassPath = true
45 | messageOutputStream = System.out // see diagnostics in real time
46 | jvmTarget = "17"
47 | verbose = false
48 |
49 | compilationBuilder()
50 | testSourceScope.testSourceBuilder()
51 | sources = testSourceScope.sources
52 | }
53 | }
54 |
55 | interface StorytaleTestSourceScope {
56 | infix fun String.hasContent(@Language("kotlin") content: String)
57 | }
58 |
59 | fun JvmCompilationResult.assertableGeneratedKspSources(
60 | compilation: KotlinCompilation,
61 | ): List {
62 | return sourcesGeneratedBySymbolProcessor.toList().map {
63 | it.descendantRelativeTo(compilation.kspSourcesDir).path hasContent
64 | org.assertj.core.util.Files.contentOf(it, Charsets.UTF_8).trim()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/StorytaleGalleryApp.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.lightColorScheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.CompositionLocalProvider
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.mutableStateSetOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.platform.LocalDensity
14 | import androidx.compose.ui.unit.Density
15 | import androidx.navigation.NavHostController
16 | import androidx.navigation.compose.rememberNavController
17 | import org.jetbrains.compose.storytale.gallery.ui.theme.LocalCustomDensity
18 |
19 | @Composable
20 | fun StorytaleGalleryApp(
21 | isEmbedded: Boolean = false,
22 | navHostController: NavHostController = rememberNavController(),
23 | ) {
24 | val isSystemDarkTheme = isSystemInDarkTheme()
25 |
26 | val appState = remember {
27 | StorytaleGalleryAppState(isSystemDarkTheme)
28 | }
29 |
30 | CompositionLocalProvider(
31 | LocalCustomDensity provides Density(LocalDensity.current.density * 0.8f),
32 | ) {
33 | MaterialTheme(colorScheme = if (appState.isDarkTheme()) darkThemeColors else lightThemeColors) {
34 | if (isEmbedded) {
35 | EmbeddedStoryView(appState, navHostController)
36 | } else {
37 | FullStorytaleGallery(appState, navHostController)
38 | }
39 | }
40 | }
41 |
42 | LaunchedEffect(isSystemDarkTheme) {
43 | appState.switchTheme(isSystemDarkTheme)
44 | }
45 | }
46 |
47 | internal val darkThemeColors = darkColorScheme()
48 | internal val lightThemeColors = lightColorScheme()
49 |
50 | @Composable
51 | internal fun isDarkMaterialTheme(): Boolean {
52 | return MaterialTheme.colorScheme == darkThemeColors
53 | }
54 |
55 | class StorytaleGalleryAppState(
56 | initialIsDarkTheme: Boolean,
57 | ) {
58 | private val localIsDarkTheme = mutableStateOf(false)
59 |
60 | init {
61 | localIsDarkTheme.value = initialIsDarkTheme
62 | }
63 |
64 | val expandedGroups = mutableStateSetOf()
65 |
66 | fun switchTheme(dark: Boolean) {
67 | localIsDarkTheme.value = dark
68 | }
69 |
70 | fun isDarkTheme(): Boolean = localIsDarkTheme.value
71 | }
72 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/ui/component/Gap.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.ui.component
2 |
3 | import androidx.compose.foundation.layout.ColumnScope
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.unit.Dp
12 | import androidx.compose.ui.unit.isSpecified
13 |
14 | /**
15 | * A spacer that adds a vertical gap between elements in a column layout.
16 | *
17 | * @param height The height of the gap.
18 | * @param modifier The optional modifier to be applied to the spacer.
19 | */
20 | @Composable
21 | fun ColumnScope.Gap(height: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.height(height))
22 |
23 | /**
24 | * A spacer that adds a horizontal gap between elements in a row layout.
25 | *
26 | * @param width The width of the gap.
27 | * @param modifier The optional modifier to be applied to the spacer.
28 | */
29 | @Composable
30 | fun RowScope.Gap(width: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.width(width))
31 |
32 | /**
33 | * A spacer that fills the available space in a column layout.
34 | *
35 | * @param modifier The optional modifier to be applied to the spacer.
36 | */
37 | @Composable
38 | fun ColumnScope.FillGap(modifier: Modifier = Modifier) = Spacer(modifier = modifier.weight(1f))
39 |
40 | /**
41 | * A spacer that fills the available space in a row layout.
42 | *
43 | * @param modifier The optional modifier to be applied to the spacer.
44 | */
45 | @Composable
46 | fun RowScope.FillGap(modifier: Modifier = Modifier) = Spacer(modifier = modifier.weight(1f))
47 |
48 | /**
49 | * A spacer that adds a square gap of the specified size.
50 | *
51 | * @param size The size of the gap.
52 | * @param modifier The optional modifier to be applied to the spacer.
53 | */
54 | @Composable
55 | fun Gap(size: Dp, modifier: Modifier = Modifier) = Spacer(modifier = modifier.size(size))
56 |
57 | /**
58 | * A spacer that adds a gap with the specified width and height.
59 | *
60 | * @param modifier The optional modifier to be applied to the spacer.
61 | * @param width The width of the gap. If not specified, the width will be unspecified.
62 | * @param height The height of the gap. If not specified, the height will be unspecified.
63 | */
64 | @Composable
65 | fun Gap(
66 | modifier: Modifier = Modifier,
67 | width: Dp = Dp.Unspecified,
68 | height: Dp = Dp.Unspecified,
69 | ) = Spacer(
70 | modifier = modifier
71 | .run { if (width.isSpecified) width(width) else this }
72 | .run { if (height.isSpecified) height(height) else this },
73 | )
74 |
--------------------------------------------------------------------------------
/modules/compiler-plugin/src/test/kotlin/StorytaleComponentRegistrarLearningTest.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.compiler.plugins.kotlin.ComposePluginRegistrar
2 | import com.tschuchort.compiletesting.KotlinCompilation
3 | import com.tschuchort.compiletesting.SourceFile
4 | import org.assertj.core.api.Assertions.assertThat
5 | import org.jetbrains.compose.plugin.storytale.compiler.StorytaleComponentRegistrar
6 | import org.jetbrains.compose.storytale.storiesStorage
7 | import org.junit.Test
8 |
9 | /**
10 | * This test is a learning test for new comers. Please don't over engineer it
11 | */
12 | class StorytaleComponentRegistrarLearningTest {
13 |
14 | @Test
15 | fun `all stories are registered after compilation and main invocation`() {
16 | // reset to initialize state
17 | storiesStorage.clear()
18 |
19 | val group1Kt = SourceFile.kotlin(
20 | "Group1.story.kt",
21 | """
22 | package storytale.gallery.demo
23 | import org.jetbrains.compose.storytale.story
24 |
25 | val Story1 by story { }
26 | val Story2 by story { }
27 | """,
28 | )
29 | val group2Kt = SourceFile.kotlin(
30 | "Group2.story.kt",
31 | """
32 | package storytale.gallery.demo
33 | import org.jetbrains.compose.storytale.story
34 |
35 | val Story3 by story { }
36 | """,
37 | )
38 | val mainKt = SourceFile.kotlin(
39 | "Main.kt",
40 | """
41 | package org.jetbrains.compose.storytale.generated
42 | fun MainViewController() { }
43 | """,
44 | )
45 |
46 | val result = KotlinCompilation().apply {
47 | sources = listOf(group1Kt, group2Kt, mainKt)
48 |
49 | compilerPluginRegistrars =
50 | listOf(StorytaleComponentRegistrar(), ComposePluginRegistrar())
51 | // magic
52 | inheritClassPath = true
53 | messageOutputStream = System.out // see diagnostics in real time
54 | jvmTarget = "17"
55 | verbose = false
56 | }
57 | .compile()
58 |
59 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
60 |
61 | val clazz = result.classLoader.loadClass("org.jetbrains.compose.storytale.generated.MainKt")
62 |
63 | assertThat(clazz).isNotNull
64 | assertThat(clazz).hasDeclaredMethods("MainViewController")
65 |
66 | clazz.getMethod("MainViewController").invoke(null)
67 |
68 | // CRAZY!!!
69 | assertThat(storiesStorage).hasSize(3)
70 | assertThat(storiesStorage.map { it.name })
71 | .containsExactlyInAnyOrder("Story1", "Story2", "Story3")
72 | assertThat(storiesStorage.map { it.group }.toSet())
73 | .containsExactlyInAnyOrder("Group1", "Group2")
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/src/test/kotlin/org/jetbrains/dokka/storytale/StorytalePluginTest.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.dokka.storytale
2 |
3 | import kotlin.test.assertEquals
4 | import kotlin.test.assertTrue
5 | import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
6 | import org.jsoup.Jsoup
7 | import org.junit.jupiter.api.Test
8 | import utils.TestOutputWriterPlugin
9 |
10 | class StorytalePluginTest : BaseAbstractTest() {
11 | private val configuration = dokkaConfiguration {
12 | sourceSets {
13 | sourceSet {
14 | sourceRoots = listOf("src/main/kotlin")
15 | }
16 | }
17 | }
18 |
19 | @Test
20 | fun `test iframe generation with story tag`() {
21 | val writerPlugin = TestOutputWriterPlugin()
22 | val source = """
23 | |/src/main/kotlin/test/Test.kt
24 | |package example
25 | |
26 | |/**
27 | | * Function with story tag
28 | | * @story https://storytale.io/stories/12345
29 | | */
30 | |fun testWithStory(): String = "Has story"
31 | """.trimIndent()
32 |
33 | testInline(
34 | source,
35 | configuration,
36 | pluginOverrides = listOf(writerPlugin, StorytalePlugin()),
37 | ) {
38 | renderingStage = { _, _ ->
39 | val html = writerPlugin.writer.contents.getValue("root/example/test-with-story.html")
40 | val doc = Jsoup.parse(html)
41 | val iframes = doc.select("iframe")
42 |
43 | assertTrue(iframes.isNotEmpty(), "Should have an iframe")
44 | assertEquals(
45 | "https://storytale.io/stories/12345",
46 | iframes.first()?.attr("src"),
47 | "Iframe should have correct URL",
48 | )
49 | }
50 | }
51 | }
52 |
53 | @Test
54 | fun `test no iframe without story tag`() {
55 | val writerPlugin = TestOutputWriterPlugin()
56 | val source = """
57 | |/src/main/kotlin/test/Test.kt
58 | |package example
59 | |
60 | |/**
61 | | * Regular function without story tag
62 | | */
63 | |fun testWithoutStory(): String = "No story"
64 | """.trimIndent()
65 |
66 | testInline(
67 | source,
68 | configuration,
69 | pluginOverrides = listOf(writerPlugin, StorytalePlugin()),
70 | ) {
71 | renderingStage = { _, _ ->
72 | val html = writerPlugin.writer.contents.getValue("root/example/test-without-story.html")
73 | val doc = Jsoup.parse(html)
74 | val iframes = doc.select("iframe")
75 |
76 | assertTrue(iframes.isEmpty(), "Should not have any iframes")
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/modules/dokka-plugin/src/main/kotlin/org/jetbrains/dokka/storytale/StoryHtmlRenderer.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.dokka.storytale
2 |
3 | import java.util.Locale
4 | import kotlinx.html.FlowContent
5 | import kotlinx.html.code
6 | import kotlinx.html.div
7 | import kotlinx.html.iframe
8 | import kotlinx.html.pre
9 | import kotlinx.html.span
10 | import kotlinx.html.style
11 | import org.jetbrains.dokka.base.renderers.html.HtmlRenderer
12 | import org.jetbrains.dokka.pages.ContentCodeBlock
13 | import org.jetbrains.dokka.pages.ContentPage
14 | import org.jetbrains.dokka.pages.ContentStyle
15 | import org.jetbrains.dokka.pages.ContentText
16 | import org.jetbrains.dokka.pages.TextStyle
17 | import org.jetbrains.dokka.plugability.DokkaContext
18 |
19 | const val STORY_IFRAME_CODE = "@story/iframe"
20 |
21 | open class StoryHtmlRenderer(context: DokkaContext) : HtmlRenderer(context) {
22 | override fun FlowContent.buildCodeBlock(code: ContentCodeBlock, pageContext: ContentPage) {
23 | if (code.language == STORY_IFRAME_CODE) {
24 | div(classes = "storytale-embedded-container") {
25 | iframe(classes = "storytale-embedded") {
26 | style = "display: flex; margin: 15px auto;"
27 | src = (code.children.single() as ContentText).text
28 | attributes["allow"] = "clipboard-read; clipboard-write"
29 | }
30 | }
31 | } else {
32 | div("sample-container") {
33 | val codeLang = "lang-" + code.language.ifEmpty { "kotlin" }
34 | val stylesWithBlock = code.style + TextStyle.Block + codeLang
35 | pre {
36 | code(stylesWithBlock.joinToString(" ") { it.toString().lowercase(Locale.getDefault()) }) {
37 | attributes["theme"] = "idea"
38 | code.children.forEach { buildContentNode(it, pageContext) }
39 | }
40 | }
41 | fun FlowContent.copiedPopup(
42 | notificationContent: String,
43 | additionalClasses: String = "",
44 | ) = div("copy-popup-wrapper $additionalClasses") {
45 | span("copy-popup-icon")
46 | span {
47 | text(notificationContent)
48 | }
49 | }
50 |
51 | fun FlowContent.copyButton() = span(classes = "top-right-position") {
52 | span("copy-icon")
53 | copiedPopup("Content copied to clipboard", "popup-to-left")
54 | }
55 | /*
56 | Disable copy button on samples as:
57 | - it is useless
58 | - it overflows with playground's run button
59 | */
60 | if (!code.style.contains(ContentStyle.RunnableSample)) copyButton()
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/StorytaleGradlePlugin.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import org.gradle.api.Project
4 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
5 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
6 | import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
7 | import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
8 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
9 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
10 | import org.jetbrains.kotlin.gradle.targets.js.KotlinWasmTargetType
11 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
12 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
13 |
14 | class StorytaleGradlePlugin : KotlinCompilerPluginSupportPlugin {
15 |
16 | override fun apply(project: Project) {
17 | project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
18 | project.plugins.withId("org.jetbrains.compose") {
19 | val extension =
20 | project.extensions.create(STORYTALE_EXTENSION_NAME, StorytaleExtension::class.java, project)
21 | project.processConfigurations(extension)
22 | }
23 | }
24 | }
25 |
26 | override fun getCompilerPluginId() = COMPILER_PLUGIN_ID
27 |
28 | override fun isApplicable(kotlinCompilation: KotlinCompilation<*>) = true
29 |
30 | override fun getPluginArtifact() = SubpluginArtifact("org.jetbrains.compose.storytale", "compiler-plugin", VERSION)
31 |
32 | override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>) = kotlinCompilation.target.project.provider { emptyList() }
33 |
34 | private fun Project.processConfigurations(extension: StorytaleExtension) {
35 | extension.targets.all {
36 | when (this) {
37 | is KotlinJsIrTarget ->
38 | when (wasmTargetType) {
39 | KotlinWasmTargetType.JS -> processWasmCompilation(extension, this)
40 | null -> processJsCompilation(extension, this)
41 | else -> {}
42 | }
43 | is KotlinAndroidTarget -> processAndroidCompilation(extension, this)
44 | is KotlinJvmTarget -> processJvmCompilation(extension, this)
45 | is KotlinNativeTarget -> processNativeCompilation(extension, this)
46 | }
47 | }
48 | }
49 |
50 | companion object {
51 | const val COMPILER_PLUGIN_ID = "org.jetbrains.compose.storytale.compiler-plugin"
52 | const val STORYTALE_TASK_GROUP = "storytale"
53 | const val STORYTALE_EXTENSION_NAME = "storytale"
54 | const val STORYTALE_PACKAGE = "org.jetbrains.compose.storytale.generated"
55 | const val STORYTALE_GENERATE_SUFFIX = "StorytaleGenerate"
56 | const val STORYTALE_SOURCESET_SUFFIX = "Stories"
57 | const val STORYTALE_EXEC_SUFFIX = "Stories"
58 | const val STORYTALE_EXEC_PREFIX = "stories"
59 | const val STORYTALE_NATIVE_APP_NAME = "StorytaleGalleryApp"
60 | const val STORYTALE_NATIVE_PROJECT_NAME = "StorytaleXCode"
61 | const val STORYTALE_NATIVE_PROJECT_PATH = "Compose.StorytaleXCode"
62 | const val STORYTALE_DEVICE_NAME = "Storytale iOS Device"
63 | const val DERIVED_DATA_DIRECTORY_NAME = "dd"
64 | const val LINK_BUILD_VERSION = "Debug"
65 |
66 | val VERSION = BuildTimeConfig.projectVersion
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/story/code/CodeBlock.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.story.code
2 |
3 | import androidx.compose.foundation.horizontalScroll
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.rememberScrollState
8 | import androidx.compose.foundation.text.selection.SelectionContainer
9 | import androidx.compose.foundation.verticalScroll
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.derivedStateOf
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.text.SpanStyle
19 | import androidx.compose.ui.text.buildAnnotatedString
20 | import androidx.compose.ui.text.font.FontFamily
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.unit.sp
24 | import dev.snipme.highlights.Highlights
25 | import dev.snipme.highlights.model.BoldHighlight
26 | import dev.snipme.highlights.model.ColorHighlight
27 | import dev.snipme.highlights.model.SyntaxLanguage
28 | import dev.snipme.highlights.model.SyntaxTheme
29 | import dev.snipme.highlights.model.SyntaxThemes
30 | import org.jetbrains.compose.resources.Font
31 | import org.jetbrains.compose.storytale.gallery.compose.text
32 | import org.jetbrains.compose.storytale.gallery.generated.resources.JetBrainsMono_Regular
33 | import org.jetbrains.compose.storytale.gallery.generated.resources.Res
34 |
35 | @Composable
36 | fun CodeBlock(
37 | code: String,
38 | modifier: Modifier = Modifier,
39 | theme: SyntaxTheme = SyntaxThemes.pastel(),
40 | ) = Row(modifier = modifier) {
41 | val codeVerticalScrollState = rememberScrollState()
42 | val codeHighlights by remember(theme, code) {
43 | derivedStateOf {
44 | Highlights.Builder()
45 | .code(code)
46 | .theme(theme)
47 | .language(SyntaxLanguage.KOTLIN)
48 | .build()
49 | }
50 | }
51 | SelectionContainer(Modifier.fillMaxSize()) {
52 | Text(
53 | text = buildAnnotatedString {
54 | text(codeHighlights.getCode())
55 | codeHighlights.getHighlights()
56 | .filterIsInstance()
57 | .forEach {
58 | addStyle(
59 | SpanStyle(color = Color(it.rgb).copy(alpha = 1f)),
60 | start = it.location.start,
61 | end = it.location.end,
62 | )
63 | }
64 | codeHighlights.getHighlights()
65 | .filterIsInstance()
66 | .forEach {
67 | addStyle(
68 | SpanStyle(fontWeight = FontWeight.Bold),
69 | start = it.location.start,
70 | end = it.location.end,
71 | )
72 | }
73 | },
74 | color = MaterialTheme.colorScheme.onSurface,
75 | fontSize = 14.sp,
76 | lineHeight = 18.sp,
77 | fontFamily = FontFamily(Font(Res.font.JetBrainsMono_Regular)),
78 | modifier = Modifier
79 | .fillMaxSize()
80 | .horizontalScroll(rememberScrollState())
81 | .verticalScroll(codeVerticalScrollState)
82 | .padding(12.dp),
83 | )
84 | }
85 | }
86 |
87 | val JetBrainsMonoRegularRes = Res.font.JetBrainsMono_Regular
88 |
--------------------------------------------------------------------------------
/modules/gallery/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.androidLibrary)
6 | alias(libs.plugins.kotlinMultiplatform)
7 | alias(libs.plugins.jetbrainsCompose)
8 | alias(libs.plugins.compose.compiler)
9 | alias(libs.plugins.serialization)
10 | `maven-publish`
11 | }
12 |
13 | kotlin {
14 | js {
15 | browser()
16 | }
17 | wasmJs {
18 | browser()
19 | }
20 | iosX64()
21 | iosArm64()
22 | iosSimulatorArm64()
23 | jvm {
24 | compilerOptions {
25 | jvmTarget.set(JvmTarget.JVM_11)
26 | }
27 | }
28 | androidTarget {
29 | publishLibraryVariants("release")
30 |
31 | compilerOptions {
32 | jvmTarget.set(JvmTarget.JVM_11)
33 | }
34 | }
35 |
36 | applyDefaultHierarchyTemplate()
37 |
38 | sourceSets {
39 | val commonMain by getting {
40 | dependencies {
41 | implementation(compose.runtime)
42 | implementation(compose.foundation)
43 | implementation(compose.material3)
44 | implementation(libs.material3.adaptive)
45 | implementation(libs.material3.icons.core)
46 | implementation(compose.ui)
47 | implementation(compose.components.resources)
48 | implementation(compose.components.uiToolingPreview)
49 | implementation(libs.navigation.compose)
50 | implementation(libs.compose.highlights)
51 | implementation(libs.kotlinx.serialization.json)
52 | implementation(projects.modules.runtimeApi)
53 | implementation(libs.navigation.compose)
54 | }
55 | }
56 |
57 | val mobileMain by creating {
58 | dependsOn(commonMain)
59 | }
60 |
61 | val androidMain by getting {
62 | dependsOn(mobileMain)
63 | }
64 |
65 | val iosMain by getting {
66 | dependsOn(mobileMain)
67 | }
68 |
69 | val desktopMain by creating {
70 | dependsOn(commonMain)
71 | }
72 |
73 | val jsMain by getting {
74 | dependsOn(desktopMain)
75 | }
76 |
77 | val wasmJsMain by getting {
78 | dependsOn(desktopMain)
79 | }
80 |
81 | val jvmMain by getting {
82 | dependsOn(desktopMain)
83 | dependencies {
84 | implementation(compose.desktop.currentOs)
85 | }
86 | }
87 | }
88 |
89 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
90 | compilerOptions {
91 | freeCompilerArgs = listOf(
92 | "-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi",
93 | "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
94 | "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
95 | "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
96 | "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
97 | "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
98 | "-opt-in=androidx.compose.material.ExperimentalMaterialApi",
99 | "-opt-in=kotlinx.coroutines.FlowPreview",
100 | "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
101 | "-opt-in=com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi",
102 | "-Xexpect-actual-classes",
103 | )
104 | }
105 | }
106 |
107 | group = "org.jetbrains.compose.storytale"
108 |
109 | publishing {}
110 |
111 | android {
112 | namespace = "org.jetbrains.compose.storytale.gallery"
113 | compileSdk = libs.versions.android.compileSdk.get().toInt()
114 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
115 | defaultConfig {
116 | minSdk = 24
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/AndroidSourceGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import com.squareup.kotlinpoet.ClassName
4 | import com.squareup.kotlinpoet.FileSpec
5 | import com.squareup.kotlinpoet.KModifier
6 | import java.io.File
7 | import java.nio.file.Files
8 | import org.gradle.api.DefaultTask
9 | import org.gradle.api.tasks.CacheableTask
10 | import org.gradle.api.tasks.Input
11 | import org.gradle.api.tasks.OutputDirectory
12 | import org.gradle.api.tasks.TaskAction
13 |
14 | @CacheableTask
15 | open class AndroidSourceGeneratorTask : DefaultTask() {
16 | @Input
17 | lateinit var title: String
18 |
19 | @Input
20 | lateinit var appPackageName: String
21 |
22 | @OutputDirectory
23 | lateinit var outputResourcesDir: File
24 |
25 | @OutputDirectory
26 | lateinit var outputSourcesDir: File
27 |
28 | @TaskAction
29 | fun generate() {
30 | cleanup(outputSourcesDir)
31 | cleanup(outputResourcesDir)
32 |
33 | generateSources()
34 | generateAndroidManifest()
35 | }
36 |
37 | private fun generateSources() {
38 | FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "MainViewController").apply {
39 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery")
40 |
41 | function("MainViewController") {
42 | addAnnotation(ClassName("androidx.compose.runtime", "Composable"))
43 | addStatement("Gallery()")
44 | }
45 | }
46 | .build()
47 | .writeTo(outputSourcesDir)
48 |
49 | FileSpec.builder(appPackageName, "StorytaleAppActivity").apply {
50 | addImport("androidx.activity", "enableEdgeToEdge")
51 | addImport("androidx.activity.compose", "setContent")
52 | addImport(StorytaleGradlePlugin.STORYTALE_PACKAGE, "MainViewController")
53 |
54 | klass("StorytaleAppActivity") {
55 | superclass(ClassName("androidx.activity", "ComponentActivity"))
56 | function("onCreate") {
57 | addModifiers(KModifier.OVERRIDE)
58 | addParameter("savedInstanceState", ClassName("android.os", "Bundle").copy(nullable = true))
59 | addStatement(
60 | """
61 | | super.onCreate(savedInstanceState)
62 | | setContent { MainViewController() }
63 | """.trimMargin(),
64 | )
65 | }
66 | }
67 | }
68 | .build()
69 | .writeTo(outputSourcesDir)
70 | }
71 |
72 | private fun generateAndroidManifest() {
73 | val androidManifestFile = File(outputResourcesDir, "AndroidManifest.xml")
74 | Files.createDirectories(androidManifestFile.parentFile.toPath())
75 | androidManifestFile.writeText(
76 | """
77 |
78 |
82 |
86 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | """.trimIndent(),
101 | )
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/gallery-demo/src/commonMain/kotlin/storytale/gallery/demo/Buttons.story.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("ktlint:standard:property-naming")
2 |
3 | package storytale.gallery.demo
4 |
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.filled.Add
11 | import androidx.compose.material.icons.filled.AddCircle
12 | import androidx.compose.material3.Button
13 | import androidx.compose.material3.ElevatedButton
14 | import androidx.compose.material3.ExtendedFloatingActionButton
15 | import androidx.compose.material3.FilledTonalButton
16 | import androidx.compose.material3.FloatingActionButton
17 | import androidx.compose.material3.Icon
18 | import androidx.compose.material3.LargeFloatingActionButton
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.OutlinedButton
21 | import androidx.compose.material3.SegmentedButton
22 | import androidx.compose.material3.SegmentedButtonDefaults
23 | import androidx.compose.material3.SingleChoiceSegmentedButtonRow
24 | import androidx.compose.material3.SmallFloatingActionButton
25 | import androidx.compose.material3.Text
26 | import androidx.compose.material3.TextButton
27 | import androidx.compose.runtime.CompositionLocalProvider
28 | import androidx.compose.runtime.mutableIntStateOf
29 | import androidx.compose.runtime.remember
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.platform.LocalDensity
33 | import androidx.compose.ui.unit.dp
34 | import org.jetbrains.compose.storytale.story
35 |
36 | val `Floating Action Buttons` by story {
37 | val Density by parameter(LocalDensity.current)
38 | val `Container color` by parameter(MaterialTheme.colorScheme.primary)
39 | val bgColor = `Container color`
40 |
41 | CompositionLocalProvider(LocalDensity provides Density) {
42 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
43 | SmallFloatingActionButton(onClick = {}, containerColor = bgColor) {
44 | Icon(imageVector = Icons.Default.Add, contentDescription = null)
45 | }
46 | FloatingActionButton(onClick = {}, containerColor = bgColor) {
47 | Icon(imageVector = Icons.Default.Add, contentDescription = null)
48 | }
49 | ExtendedFloatingActionButton(onClick = {}, containerColor = bgColor) {
50 | Icon(imageVector = Icons.Default.AddCircle, contentDescription = null)
51 | Spacer(Modifier.padding(4.dp))
52 | Text("Extended")
53 | }
54 | LargeFloatingActionButton(onClick = {}, containerColor = bgColor) {
55 | Text("Large")
56 | }
57 | }
58 | }
59 | }
60 |
61 | val `Segmented buttons` by story {
62 | val selectedIndex = remember { mutableIntStateOf(0) }
63 |
64 | SingleChoiceSegmentedButtonRow {
65 | repeat(3) { index ->
66 | SegmentedButton(
67 | selected = index == selectedIndex.value,
68 | onClick = { selectedIndex.value = index },
69 | shape = SegmentedButtonDefaults.itemShape(index, 3),
70 | ) {
71 | Text("Button $index", modifier = Modifier.padding(4.dp))
72 | }
73 | }
74 | }
75 | }
76 |
77 | val `Common buttons` by story {
78 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
79 | ElevatedButton(onClick = {}) {
80 | Text("Elevated Button")
81 | }
82 |
83 | Button(onClick = {}) {
84 | Text("Filled", softWrap = false)
85 | }
86 |
87 | FilledTonalButton(onClick = {}) {
88 | Text("Tonal", softWrap = false)
89 | }
90 |
91 | OutlinedButton(onClick = {}) {
92 | Text("Outlined", softWrap = false)
93 | }
94 |
95 | TextButton(onClick = {}) {
96 | Text("Text", softWrap = false)
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/modules/preview-processor/src/main/kotlin/PreviewProcessor.kt:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.processing.CodeGenerator
2 | import com.google.devtools.ksp.processing.Dependencies
3 | import com.google.devtools.ksp.processing.KSPLogger
4 | import com.google.devtools.ksp.processing.Resolver
5 | import com.google.devtools.ksp.processing.SymbolProcessor
6 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
7 | import com.google.devtools.ksp.processing.SymbolProcessorProvider
8 | import com.google.devtools.ksp.symbol.KSAnnotated
9 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration
10 | import com.google.devtools.ksp.validate
11 | import com.squareup.kotlinpoet.ClassName
12 | import com.squareup.kotlinpoet.FileSpec
13 | import com.squareup.kotlinpoet.PropertySpec
14 | import com.squareup.kotlinpoet.buildCodeBlock
15 | import org.jetbrains.compose.storytale.plugin.StorytaleGradlePlugin
16 |
17 | class PreviewProcessor(
18 | private val logger: KSPLogger,
19 | private val codeGenerator: CodeGenerator,
20 | ) : SymbolProcessor {
21 | override fun process(resolver: Resolver): List {
22 | val (jetbrains, discarded1) =
23 | resolver.getSymbolsWithAnnotation("org.jetbrains.compose.ui.tooling.preview.Preview")
24 | .partition { it.validate() }
25 | val (androidxDesktop, discarded2) = resolver.getSymbolsWithAnnotation("androidx.compose.desktop.ui.tooling.preview.Preview")
26 | .partition { it.validate() }
27 |
28 | val (androidxAndroid, discarded3) = resolver.getSymbolsWithAnnotation("androidx.compose.ui.tooling.preview.Preview")
29 | .partition { it.validate() }
30 |
31 | val validPreviewFunctions = (jetbrains + androidxDesktop + androidxAndroid)
32 | .filter { it is KSFunctionDeclaration && it.validate() }
33 | .map { it as KSFunctionDeclaration }
34 | .sortedBy { (it.qualifiedName ?: it.simpleName).asString() }
35 |
36 | generatePreviewFile(validPreviewFunctions)
37 |
38 | return discarded1 + discarded2 + discarded3
39 | }
40 |
41 | private fun generatePreviewFile(previewFunctions: List) {
42 | if (previewFunctions.isEmpty()) return
43 |
44 | val packageName = StorytaleGradlePlugin.STORYTALE_PACKAGE
45 | val fileSpecBuilder = FileSpec.builder(
46 | packageName,
47 | "Previews.story",
48 | ).apply {
49 | indent(" ")
50 | addImport("org.jetbrains.compose.storytale", "story")
51 |
52 | previewFunctions.forEach { function ->
53 | addImport(function.packageName.asString(), function.simpleName.asString())
54 |
55 | val functionName = function.simpleName.asString()
56 | val storyName = functionName.removePrefix("Preview").removeSuffix("Preview")
57 |
58 | addProperty(
59 | PropertySpec
60 | .builder(
61 | storyName.ifEmpty { functionName },
62 | ClassName("org.jetbrains.compose.storytale", "Story"),
63 | )
64 | .delegate(
65 | buildCodeBlock {
66 | beginControlFlow("story")
67 | addStatement("%N()", functionName)
68 | endControlFlow()
69 | },
70 | )
71 | .build(),
72 | )
73 | }
74 | }
75 |
76 | val containingFiles = previewFunctions
77 | .mapNotNull { it.containingFile }
78 | .distinct()
79 |
80 | val dependencies = if (containingFiles.isNotEmpty()) {
81 | Dependencies(true, containingFiles.first())
82 | } else {
83 | Dependencies.ALL_FILES
84 | }
85 |
86 | val file = codeGenerator.createNewFile(
87 | dependencies,
88 | packageName,
89 | "Previews.story",
90 | )
91 | fileSpecBuilder.build().toJavaFileObject().openInputStream().copyTo(file)
92 | }
93 |
94 | class Provider : SymbolProcessorProvider {
95 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
96 | return PreviewProcessor(
97 | environment.logger,
98 | environment.codeGenerator,
99 | )
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/examples/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
24 |
30 |
36 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.11.0"
3 | android-targetSdk = "36"
4 | android-compileSdk = "36"
5 | android-minSdk = "26"
6 | androidx-activityCompose = "1.9.3"
7 | assertj = "3.27.2"
8 | kotlinCompileTesting = "0.7.0"
9 | junitVersion = "4.13.2"
10 | storytale = "0.0.3+dev8"
11 |
12 | compose-plugin = "1.8.1"
13 | compose-hot-reload = "1.0.0-beta06"
14 | compose-navigation = "2.9.0-alpha16"
15 | kotlinx-serialization-json = "1.7.3"
16 | kotlin = "2.1.21"
17 | kotlin-poet = "1.18.1"
18 | build-time-config = "2.3.0"
19 | code-highlights = "1.0.0"
20 | spotless = "7.0.2"
21 | dokka = "1.9.10"
22 | kotlinx-html = "0.7.3"
23 | junit = "5.10.1"
24 | jsoup = "1.16.1"
25 | ktlint = "1.5.0"
26 | composeRules = "0.4.22"
27 | material3-icons = "1.7.3"
28 | material3-adaptive = "1.1.0-beta01"
29 | ksp = "2.1.20-2.0.1"
30 |
31 | [libraries]
32 | assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }
33 | kotlinCompileTesting-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" }
34 | kotlinCompileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kotlinCompileTesting" }
35 | junit = { module = "junit:junit", version.ref = "junitVersion" }
36 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
37 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
38 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
39 | compose-gradle-plugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-plugin" }
40 | compose-hot-reload-plugin = { module = "org.jetbrains.compose.hot-reload:hot-reload-gradle-plugin", version.ref = "compose-hot-reload" }
41 | kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
42 | kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlin-poet" }
43 | android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
44 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
45 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
46 | compose-highlights = { module = "dev.snipme:highlights", version.ref = "code-highlights" }
47 | dokka-core = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka" }
48 | dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka" }
49 | dokka-test-api = { module = "org.jetbrains.dokka:dokka-test-api", version.ref = "dokka" }
50 | dokka-base-test-utils = { module = "org.jetbrains.dokka:dokka-base-test-utils", version.ref = "dokka" }
51 | kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version.ref = "kotlinx-html" }
52 | junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
53 | jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
54 | ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }
55 | composeRules = { module = "io.nlopez.compose.rules:ktlint", version.ref = "composeRules" }
56 | material3-adaptive = { module = "org.jetbrains.compose.material3.adaptive:adaptive", version.ref = "material3-adaptive" }
57 | material3-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "material3-icons" }
58 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
59 |
60 | [plugins]
61 | androidApplication = { id = "com.android.application", version.ref = "agp" }
62 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
63 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
64 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
65 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
66 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
67 | compose-hot-reload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hot-reload" }
68 | buildTimeConfig = { id = "dev.limebeck.build-time-config", version.ref = "build-time-config" }
69 | storytale = { id = "org.jetbrains.compose.storytale", version.ref = "storytale" }
70 | kotlinDsl = { id = "kotlin-dsl" }
71 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
72 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
73 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
74 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
75 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/WasmSourceGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import com.squareup.kotlinpoet.AnnotationSpec
4 | import com.squareup.kotlinpoet.ClassName
5 | import com.squareup.kotlinpoet.FileSpec
6 | import java.io.File
7 | import org.gradle.api.DefaultTask
8 | import org.gradle.api.tasks.CacheableTask
9 | import org.gradle.api.tasks.Input
10 | import org.gradle.api.tasks.OutputDirectory
11 | import org.gradle.api.tasks.TaskAction
12 | import org.jetbrains.kotlin.incremental.createDirectory
13 |
14 | @CacheableTask
15 | open class WasmSourceGeneratorTask : DefaultTask() {
16 | @Input
17 | lateinit var title: String
18 |
19 | @OutputDirectory
20 | lateinit var outputResourcesDir: File
21 |
22 | @OutputDirectory
23 | lateinit var outputSourcesDir: File
24 |
25 | @TaskAction
26 | fun generate() {
27 | cleanup(outputSourcesDir)
28 | cleanup(outputResourcesDir)
29 |
30 | generateSources()
31 | generateResources()
32 | }
33 |
34 | private fun generateSources() {
35 | val file = FileSpec.builder(StorytaleGradlePlugin.STORYTALE_PACKAGE, "Main").apply {
36 | addMainFileImports()
37 | addMainViewControllerFun()
38 | function("main") {
39 | addStatement("MainViewController()")
40 | }
41 | }.build()
42 |
43 | file.writeTo(outputSourcesDir)
44 | }
45 |
46 | private fun generateResources() {
47 | if (!outputResourcesDir.exists()) {
48 | outputResourcesDir.createDirectory()
49 | }
50 |
51 | val styles = File(outputResourcesDir, "styles.css")
52 | styles.writeText(webStylesCssContent)
53 |
54 | val index = File(outputResourcesDir, "index.html")
55 | index.writeText(
56 | webIndexHtmlContent(
57 | jsFileName = JsSourceGeneratorTask.SCRIPT_FILE_NAME,
58 | addSkikoJs = false,
59 | ),
60 | )
61 | }
62 | }
63 |
64 | internal fun FileSpec.Builder.addMainFileImports() {
65 | addImport("androidx.compose.ui.window", "ComposeViewport")
66 | addImport("kotlinx.browser", "document")
67 | addImport("kotlinx.browser", "window")
68 | addImport("org.jetbrains.compose.storytale.gallery", "Gallery")
69 | addImport("org.jetbrains.compose.storytale.gallery.story.code", "JetBrainsMonoRegularRes")
70 | addImport("org.jetbrains.compose.resources", "preloadFont")
71 | }
72 |
73 | internal fun FileSpec.Builder.addMainViewControllerFun() {
74 | val optInExperimentalComposeUi = AnnotationSpec.builder(ClassName("kotlin", "OptIn")).addMember(
75 | "androidx.compose.ui.ExperimentalComposeUiApi::class, org.jetbrains.compose.resources.ExperimentalResourceApi::class",
76 | ).build()
77 |
78 | function("MainViewController") {
79 | addAnnotation(optInExperimentalComposeUi)
80 | addStatement(
81 | """
82 | |val useEmbedded = window.location.search.contains("embedded=true")
83 | |
84 | |ComposeViewport(document.body!!) {
85 | | val hasResourcePreloadCompleted = preloadFont(JetBrainsMonoRegularRes).value != null
86 | |
87 | | if (hasResourcePreloadCompleted) {
88 | | Gallery(isEmbedded = useEmbedded)
89 | | }
90 | |
91 | |}
92 | |
93 | """.trimMargin(),
94 | )
95 | }
96 | }
97 |
98 | internal val webStylesCssContent = """
99 | |html, body {
100 | | width: 100%;
101 | | height: 100%;
102 | | margin: 0;
103 | | padding: 0;
104 | | overflow: hidden;
105 | |}
106 | """.trimMargin()
107 |
108 | internal fun webIndexHtmlContent(jsFileName: String, addSkikoJs: Boolean = true): String {
109 | return """
110 | |
111 | |
112 | |
113 | |
114 | |
115 | | Gallery
116 | |
117 | | ${if (addSkikoJs) """""" else ""}
118 | |
119 | |
120 | |
121 | |
122 | |
123 | """.trimMargin()
124 | }
125 |
--------------------------------------------------------------------------------
/examples/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
5 |
6 | plugins {
7 | alias(libs.plugins.compose.compiler)
8 | alias(libs.plugins.storytale)
9 | alias(libs.plugins.kotlinMultiplatform)
10 | alias(libs.plugins.androidApplication)
11 | alias(libs.plugins.jetbrainsCompose)
12 | alias(libs.plugins.compose.hot.reload)
13 | }
14 |
15 | configurations.all {
16 | resolutionStrategy.dependencySubstitution {
17 | substitute(module("org.jetbrains.compose.storytale:compiler-plugin"))
18 | .using(project(":modules:compiler-plugin"))
19 | substitute(module("org.jetbrains.compose.storytale:runtime-api"))
20 | .using(project(":modules:runtime-api"))
21 | substitute(module("org.jetbrains.compose.storytale:gallery"))
22 | .using(project(":modules:gallery"))
23 | }
24 | }
25 |
26 | kotlin {
27 | wasmJs {
28 | moduleName = "composeApp"
29 | browser {
30 | commonWebpackConfig {
31 | outputFileName = "composeApp.js"
32 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
33 | static = (static ?: mutableListOf()).apply {
34 | // Serve sources to debug inside browser
35 | add(project.projectDir.path)
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
42 | androidTarget {
43 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
44 | compilerOptions {
45 | jvmTarget.set(JvmTarget.JVM_11)
46 | }
47 | }
48 |
49 | jvm("desktop")
50 |
51 | listOf(
52 | iosX64(),
53 | iosArm64(),
54 | iosSimulatorArm64(),
55 | ).forEach { iosTarget ->
56 | iosTarget.binaries.framework {
57 | baseName = "ComposeApp"
58 | isStatic = true
59 | binaryOption("bundleId", "Storytale Examples App")
60 | }
61 | }
62 |
63 | applyDefaultHierarchyTemplate()
64 |
65 | sourceSets {
66 | val desktopMain by getting
67 |
68 | androidMain.dependencies {
69 | implementation(compose.preview)
70 | implementation(libs.androidx.activity.compose)
71 | }
72 | commonMain.dependencies {
73 | implementation(compose.runtime)
74 | implementation(compose.foundation)
75 | implementation(compose.material)
76 | implementation(compose.ui)
77 | implementation(compose.components.resources)
78 | implementation(compose.components.uiToolingPreview)
79 | }
80 | desktopMain.dependencies {
81 | implementation(compose.desktop.currentOs)
82 | }
83 | }
84 | }
85 |
86 | android {
87 | namespace = "org.jetbrains.compose.storytale.example"
88 | compileSdk = libs.versions.android.compileSdk.get().toInt()
89 |
90 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
91 | sourceSets["main"].res.srcDirs("src/androidMain/res")
92 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
93 |
94 | defaultConfig {
95 | applicationId = "org.jetbrains.compose.storytale.example"
96 | minSdk = libs.versions.android.minSdk.get().toInt()
97 | targetSdk = libs.versions.android.targetSdk.get().toInt()
98 | versionCode = 1
99 | versionName = "1.0"
100 | }
101 | packaging {
102 | resources {
103 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
104 | }
105 | }
106 | buildTypes {
107 | getByName("release") {
108 | isMinifyEnabled = false
109 | }
110 | }
111 | compileOptions {
112 | sourceCompatibility = JavaVersion.VERSION_11
113 | targetCompatibility = JavaVersion.VERSION_11
114 | }
115 | buildFeatures {
116 | compose = true
117 | }
118 | dependencies {
119 | debugImplementation(compose.uiTooling)
120 | }
121 | }
122 |
123 | compose.resources {
124 | publicResClass = true
125 | packageOfResClass = "org.jetbrains.compose.storytale.example"
126 | }
127 |
128 | compose.desktop {
129 | application {
130 | mainClass = "MainKt"
131 |
132 | nativeDistributions {
133 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
134 | packageName = "org.jetbrains.compose.storytale"
135 | packageVersion = "1.0.0"
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/modules/gallery/src/commonMain/kotlin/org/jetbrains/compose/storytale/gallery/material3/EmbeddedStoryView.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.gallery.material3
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.material3.CenterAlignedTopAppBar
13 | import androidx.compose.material3.HorizontalDivider
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalUriHandler
22 | import androidx.compose.ui.platform.UriHandler
23 | import androidx.compose.ui.text.SpanStyle
24 | import androidx.compose.ui.text.buildAnnotatedString
25 | import androidx.compose.ui.text.style.TextDecoration
26 | import androidx.compose.ui.text.withStyle
27 | import androidx.compose.ui.unit.dp
28 | import androidx.compose.ui.unit.sp
29 | import androidx.navigation.NavHostController
30 | import androidx.navigation.compose.NavHost
31 | import androidx.navigation.compose.composable
32 | import androidx.navigation.toRoute
33 | import org.jetbrains.compose.storytale.storiesStorage
34 |
35 | @Composable
36 | fun EmbeddedStoryView(
37 | appState: StorytaleGalleryAppState,
38 | navHostController: NavHostController,
39 | ) {
40 | NavHost(
41 | navController = navHostController,
42 | startDestination = StoryScreen(""),
43 | enterTransition = { EnterTransition.None },
44 | exitTransition = { ExitTransition.None },
45 | popEnterTransition = { EnterTransition.None },
46 | popExitTransition = { ExitTransition.None },
47 | ) {
48 | composable {
49 | val args = it.toRoute()
50 | val storyName = args.storyName
51 | Column(
52 | modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface),
53 | horizontalAlignment = Alignment.CenterHorizontally,
54 | ) {
55 | CenterAlignedTopAppBar(
56 | title = {
57 | Text(
58 | text = storyName,
59 | style = MaterialTheme.typography.titleMedium,
60 | modifier = Modifier.padding(12.dp),
61 | )
62 | },
63 | actions = {
64 | ThemeSwitcherIconButton(appState)
65 |
66 | val uriHandler = LocalUriHandler.current
67 |
68 | IconButton(onClick = {
69 | openFullScreenStory(args, uriHandler)
70 | }) {
71 | Icon(imageVector = OpenInFull, contentDescription = null)
72 | }
73 | },
74 | )
75 | StoryContent(
76 | activeStory = storiesStorage.firstOrNull {
77 | it.name == storyName
78 | },
79 | useEmbeddedView = true,
80 | modifier = Modifier.weight(1f),
81 | )
82 | HorizontalDivider()
83 | Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 12.dp)) {
84 | val uriHandler = LocalUriHandler.current
85 | Text(
86 | text = buildAnnotatedString {
87 | append("Powered by ")
88 | withStyle(
89 | style = SpanStyle(
90 | textDecoration = TextDecoration.Underline,
91 | color = MaterialTheme.colorScheme.primary,
92 | ),
93 | ) {
94 | append("Storytale")
95 | }
96 | },
97 | modifier = Modifier.align(Alignment.CenterEnd).clickable(onClick = {
98 | uriHandler.openUri("https://github.com/Kotlin/Storytale")
99 | }),
100 | style = MaterialTheme.typography.bodySmall,
101 | fontSize = 9.sp,
102 | color = MaterialTheme.colorScheme.onSurfaceVariant,
103 | )
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
110 | /**
111 | * EXPERIMENTAL!
112 | * A lambda to be provided by the Storytale gallery consumers to open a full screen gallery.
113 | */
114 | var openFullScreenStory: (StoryScreen, UriHandler) -> Unit = { story, uriHandler ->
115 | println("openFullScreenStory($story) is not implemented. It should be provided by the Gallery consumer.")
116 | }
117 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JsMultiplatformTasks.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
2 |
3 | package org.jetbrains.compose.storytale.plugin
4 |
5 | import org.gradle.api.Project
6 | import org.gradle.api.file.DuplicatesStrategy
7 | import org.gradle.api.tasks.AbstractCopyTask
8 | import org.gradle.kotlin.dsl.task
9 | import org.gradle.kotlin.dsl.withType
10 | import org.jetbrains.compose.web.tasks.UnpackSkikoWasmRuntimeTask
11 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
12 | import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.ResolveResourcesFromDependenciesTask
13 | import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
14 | import org.jetbrains.kotlin.gradle.targets.js.ir.JsIrBinary
15 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinBrowserJsIr
16 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrCompilation
17 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
18 | import org.jetbrains.kotlin.gradle.targets.js.ir.WebpackConfigurator
19 | import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile
20 |
21 | fun Project.processJsCompilation(extension: StorytaleExtension, target: KotlinJsIrTarget) {
22 | project.logger.info("Configuring storytale for Kotlin/JS")
23 | createJsStorytaleGenerateSourceTask(extension, target)
24 |
25 | val storytaleCompilation = createWasmAndJsStorytaleCompilation(extension, target)
26 |
27 | createWasmAndJsStorytaleExecTask(storytaleCompilation)
28 | }
29 |
30 | fun Project.createWasmAndJsStorytaleCompilation(
31 | extension: StorytaleExtension,
32 | target: KotlinJsIrTarget,
33 | ): KotlinJsIrCompilation {
34 | val mainCompilation = target.compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME).get()
35 | val storytaleBuildDir = extension.getBuildDirectory(target)
36 | val storytaleCompilation =
37 | target.compilations.create(StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX) as KotlinJsIrCompilation
38 |
39 | storytaleCompilation.associateWith(mainCompilation)
40 | setupResourceResolvingForTarget(storytaleBuildDir, storytaleCompilation)
41 |
42 | (mainCompilation.target as KotlinJsTargetDsl).apply {
43 | // force to create executable: required for IR, do nothing on Legacy
44 | binaries.executable(storytaleCompilation)
45 | }
46 |
47 | project.afterEvaluate {
48 | storytaleCompilation.apply {
49 | val sourceSet = kotlinSourceSets.single()
50 |
51 | sourceSet.dependsOn(extension.mainStoriesSourceSet)
52 |
53 | sourceSet.kotlin.setSrcDirs(files("$storytaleBuildDir/sources"))
54 |
55 | val generateTaskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}"
56 |
57 | val unpackSkikoTask = extension.project.tasks.withType().single()
58 | val resolveDependencyResourcesTask = extension.project.tasks
59 | .getByName(storytaleCompilation.resolveDependencyResourcesTaskName) as ResolveResourcesFromDependenciesTask
60 |
61 | sourceSet.resources.srcDirs(
62 | "$storytaleBuildDir/resources",
63 | unpackSkikoTask.outputDir,
64 | resolveDependencyResourcesTask.outputDirectory,
65 | mainCompilation.defaultSourceSet.resources,
66 | )
67 |
68 | extension.project.tasks.named(processResourcesTaskName).configure {
69 | dependsOn(unpackSkikoTask)
70 | dependsOn(generateTaskName)
71 | dependsOn(resolveDependencyResourcesTask)
72 |
73 | (this as? AbstractCopyTask)?.duplicatesStrategy = DuplicatesStrategy.INCLUDE
74 | }
75 |
76 | compileTaskProvider.configure {
77 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
78 | description = "Compile web storytale source files for '${target.name}'"
79 |
80 | dependsOn(generateTaskName)
81 | configureOptions()
82 | }
83 |
84 | binaries.withType(JsIrBinary::class.java)
85 | .configureEach { linkTask.configure { configureOptions() } }
86 | }
87 | }
88 |
89 | return storytaleCompilation
90 | }
91 |
92 | private fun Kotlin2JsCompile.configureOptions() {
93 | compilerOptions.sourceMap.set(true)
94 | }
95 |
96 | private fun Project.createJsStorytaleGenerateSourceTask(extension: StorytaleExtension, target: KotlinJsIrTarget) {
97 | val storytaleBuildDir = extension.getBuildDirectory(target)
98 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}") {
99 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
100 | description = "Generate JS source files for '${target.name}'"
101 | title = target.name
102 | outputResourcesDir = file("$storytaleBuildDir/resources")
103 | outputSourcesDir = file("$storytaleBuildDir/sources")
104 | }
105 | }
106 |
107 | fun Project.createWasmAndJsStorytaleExecTask(compilation: KotlinJsIrCompilation) {
108 | afterEvaluate {
109 | val browser = compilation.target.browser as KotlinBrowserJsIr
110 |
111 | browser.subTargetConfigurators.withType().configureEach {
112 | setupBuild(compilation)
113 | setupRun(compilation)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/Utils.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(org.jetbrains.kotlin.gradle.InternalKotlinGradlePluginApi::class)
2 | @file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
3 |
4 | package org.jetbrains.compose.storytale.plugin
5 |
6 | import com.squareup.kotlinpoet.FileSpec
7 | import com.squareup.kotlinpoet.FunSpec
8 | import com.squareup.kotlinpoet.MemberSpecHolder
9 | import com.squareup.kotlinpoet.TypeSpec
10 | import java.io.ByteArrayOutputStream
11 | import java.io.File
12 | import java.io.IOException
13 | import java.io.InputStream
14 | import java.nio.file.Files
15 | import java.nio.file.StandardCopyOption
16 | import java.util.zip.ZipInputStream
17 | import org.gradle.api.Action
18 | import org.gradle.api.DefaultTask
19 | import org.gradle.api.Project
20 | import org.gradle.api.file.DirectoryProperty
21 | import org.gradle.api.file.FileCollection
22 | import org.gradle.api.provider.Property
23 | import org.gradle.api.tasks.Input
24 | import org.gradle.api.tasks.OutputDirectory
25 | import org.gradle.api.tasks.TaskAction
26 | import org.gradle.configurationcache.extensions.capitalized
27 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
28 | import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
29 | import org.jetbrains.kotlin.gradle.plugin.mpp.internal
30 | import org.jetbrains.kotlin.gradle.plugin.mpp.publishing.configureResourcesPublicationAttributes
31 | import org.jetbrains.kotlin.gradle.plugin.mpp.resources.resolve.ResolveResourcesFromDependenciesTask
32 | import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget
33 |
34 | fun cleanup(file: File) {
35 | if (file.exists()) {
36 | val listing = file.listFiles()
37 | if (listing != null) {
38 | for (sub in listing) {
39 | cleanup(sub)
40 | }
41 | }
42 | file.delete()
43 | }
44 | }
45 |
46 | inline fun MemberSpecHolder.Builder<*>.function(
47 | name: String,
48 | builderAction: FunSpec.Builder.() -> Unit,
49 | ): FunSpec {
50 | return FunSpec.builder(name).apply(builderAction).build().also {
51 | addFunction(it)
52 | }
53 | }
54 |
55 | inline fun FileSpec.Builder.klass(
56 | name: String,
57 | builderAction: TypeSpec.Builder.() -> Unit,
58 | ): TypeSpec {
59 | return TypeSpec.classBuilder(name).apply(builderAction).build().also {
60 | addType(it)
61 | }
62 | }
63 |
64 | fun StorytaleExtension.getBuildDirectory(target: KotlinTarget) = with(project) {
65 | file(buildDir.resolve(this@getBuildDirectory.buildDir).resolve(name).resolve(target.name))
66 | }
67 |
68 | abstract class UnzipResourceTask : DefaultTask() {
69 | @get:Input
70 | abstract val resourcePath: Property
71 |
72 | @get:OutputDirectory
73 | abstract val outputDir: DirectoryProperty
74 |
75 | @TaskAction
76 | fun unzip() {
77 | val resourcePath = resourcePath.get()
78 | javaClass.classLoader.getResourceAsStream(resourcePath)?.use { zipStream ->
79 | unzipStream(zipStream)
80 | } ?: throw IOException("Resource not found: $resourcePath")
81 | }
82 |
83 | private fun unzipStream(zipStream: InputStream) {
84 | val outputDir = outputDir.get()
85 |
86 | ZipInputStream(zipStream).use { zis ->
87 | while (true) {
88 | val entry = zis.nextEntry ?: break
89 | val newFile = File(outputDir.asFile, entry.name)
90 | if (entry.isDirectory) {
91 | newFile.mkdirs()
92 | } else {
93 | newFile.parentFile.mkdirs()
94 | Files.copy(zis, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
95 | }
96 | zis.closeEntry()
97 | }
98 | }
99 | }
100 | }
101 |
102 | val KotlinCompilation<*>.resolveDependencyResourcesTaskName: String
103 | get() = "${name.lowercase()}${target.name.capitalized()}ResolveDependencyResources"
104 |
105 | fun setupResourceResolvingForTarget(storytaleBuildDir: File, compilation: KotlinCompilation<*>) {
106 | compilation.project.tasks.register(
107 | compilation.resolveDependencyResourcesTaskName,
108 | ResolveResourcesFromDependenciesTask::class.java,
109 | Action {
110 | filterResourcesByExtension.set(true)
111 | archivesFromDependencies.setFrom(getArchivesFromResources(compilation))
112 | outputDirectory.set(storytaleBuildDir.resolve("resolved-dependency-resources"))
113 | },
114 | )
115 | }
116 |
117 | fun getArchivesFromResources(compilation: KotlinCompilation<*>): FileCollection {
118 | val dependenciesConfiguration = if (compilation.target is KotlinJsIrTarget) {
119 | compilation.internal.configurations.runtimeDependencyConfiguration
120 | ?: return compilation.project.files()
121 | } else {
122 | compilation.internal.configurations.compileDependencyConfiguration
123 | }
124 |
125 | return dependenciesConfiguration.incoming.artifactView {
126 | withVariantReselection()
127 | attributes {
128 | configureResourcesPublicationAttributes(compilation.target)
129 | }
130 | isLenient = true
131 | }.files
132 | }
133 |
134 | fun Project.execute(vararg args: String): String = ByteArrayOutputStream().apply {
135 | exec {
136 | commandLine(*args)
137 | standardOutput = this@apply
138 | }
139 | }.toString()
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://github.com/JetBrains#jetbrains-on-github)
4 |
5 | # Storytale
6 |
7 | Storytale is a Gradle Plugin designed to help developers to show their composables and develop them isolated by generating a gallery of the project components.
8 | Check the `examples` and their generated web gallery [here](https://kotlin.github.io/Storytale)
9 |
10 | Since Storytale is still in the early stages of development, the api is marked as unstable, but this section will also show you how to use `Storytale` to write code for your components, so let's get started! 🌟
11 |
12 |
13 |
14 | ## ⚙️ Getting Started
15 |
16 | ### 1. Setup
17 |
18 | #### Import Dependencies
19 |
20 |
21 | using Version Catalog
22 |
23 | > **libs.versions.toml**
24 |
25 | ```toml
26 | [versions]
27 | storytale = "0.0.4-alpha01+dev19"
28 |
29 | [plugins]
30 | storytale = { id = "org.jetbrains.compose.storytale", version.ref = "storytale" }
31 | ```
32 |
33 | For the latest version check out the [Releases page](https://github.com/Kotlin/Storytale/releases)
34 |
35 | > **build.gradle.kts** `root level`
36 | ```kotlin
37 | plugins {
38 | alias(libs.plugins.storytale) apply false
39 | }
40 | ```
41 |
42 |
43 | > **build.gradle.kts** `app level`
44 | ```kotlin
45 | plugins {
46 | alias(libs.plugins.storytale)
47 | }
48 | ```
49 |
50 |
51 | ```kotlin
52 | repositories {
53 | mavenCentral()
54 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
55 | }
56 | ```
57 |
58 | > [!NOTE]
59 | > Storytale **has not** yet released its first version on `mavenCentral`. Currently, we publish dev builds to maven("https://maven.pkg.jetbrains.space/public/p/compose/dev"), so it's required to add this repository as shown above.
60 |
61 | ### 2. Create Sourcesets for Storytale on the target platform (for multi-platform projects)
62 |
63 | Storytale can be used for Compose Multiplatform projects. To start using the Storytale API, you need to define a sourceset for the component you want to test (for example, it might only be used for `Android/iOS` platforms, or it could be common for all platforms).
64 |
65 | In your app's 'src' folder, go to New -> Directory:
66 |
67 |
68 |
69 | ### 3. Usage
70 |
71 | Now, your project structure will look like this:
72 |
73 | ```
74 | └── src/
75 | ├── androidMain
76 | ├── commonMain
77 | ├── xxxxxStories/
78 | │ └── kotlin
79 | └── desktopMain
80 | ```
81 |
82 | Let's try to write a simple function in `commonMain`:
83 |
84 | `commonMain/PrimaryButton.kt`
85 | ```kotlin
86 | @Composable
87 | fun PrimaryButton(onClick: () -> Unit, enabled: Boolean = true) {
88 | Button(onClick = onClick, enabled = enabled) {
89 | Text("Click me!")
90 | }
91 | }
92 | ```
93 |
94 | `commonStories/kotlin/PrimaryButton.story.kt`
95 | ```kotlin
96 | import org.jetbrains.compose.storytale.story
97 |
98 | val `PrimaryButton default state` by story {
99 | val enabled by parameter(true)
100 | PrimaryButton(onClick = {}, enabled = enabled)
101 | }
102 | ```
103 |
104 | Next, let's run the `desktopStoriesRun` command, you can find it in the `project/Storytale` section on the right side of the Gradle panel.
105 |
106 |
107 |
108 | If you can't find all Gradle tasks containing `Storytale` after syncing, check if this option is enabled:
109 |
110 | `settings->Experimental`
111 |
112 |
113 |
114 |
115 | ## Building and Contributing
116 |
117 | Once the sync is successful, run `./gradlew publishToMavenLocal`.
118 |
119 | At this point, if you see these Storytale `Gradle tasks`, it means you’ve successfully set up the project and can start contributing! :)
120 |
121 |
122 |
123 | Before running `XXXXStoriesRun`, you need to run `./gradlew publishToMavenLocal` to deploy the latest changes if you've modified any part of the code (except for examples module)
124 |
125 | #### About project structure
126 |
127 | ```
128 | .
129 | └── modules/
130 | ├── compiler-plugin
131 | ├── gallery
132 | ├── gradle-plugin
133 | └── runtime
134 | ```
135 |
136 | ##### compiler-plugin
137 |
138 | Includes the entry point of the Storytale compiler plugin and its related features.
139 |
140 | ##### gallery
141 |
142 | The gallery represents the final, fully functional multi-platform application that is produced by Storytale.
143 |
144 | ##### gradle-plugin
145 |
146 | All aspects related to building Storytale, including various Gradle tasks, generating Storytale apps for different platforms, and so on.
147 |
148 | #### runtime
149 |
150 | The runtime module is designed to provide developers with essential APIs during the coding process
151 |
152 | ## Feedback and questions
153 |
154 | Share your feedback or questions in our [#storytale](https://slack-chats.kotlinlang.org/c/storytale) Slack channel.
155 | [Get a Slack invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up).
156 |
--------------------------------------------------------------------------------
/modules/gradle-plugin/src/main/kotlin/org/jetbrains/compose/storytale/plugin/JvmMultiplatformTasks.kt:
--------------------------------------------------------------------------------
1 | package org.jetbrains.compose.storytale.plugin
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.api.file.FileCollection
5 | import org.gradle.api.plugins.JavaPluginExtension
6 | import org.gradle.api.provider.Provider
7 | import org.gradle.api.tasks.JavaExec
8 | import org.gradle.jvm.toolchain.JavaLauncher
9 | import org.gradle.jvm.toolchain.JavaToolchainService
10 | import org.gradle.kotlin.dsl.register
11 | import org.gradle.kotlin.dsl.task
12 | import org.jetbrains.compose.reload.gradle.ComposeHotRun
13 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
14 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
15 | import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
16 |
17 | fun Project.processJvmCompilation(extension: StorytaleExtension, target: KotlinJvmTarget) {
18 | project.logger.info("Configuring storytale for Kotlin/JVM")
19 | createJvmStorytaleGenerateSourceTask(extension, target)
20 |
21 | val storytaleCompilation = createJvmStorytaleCompileTask(extension, target)
22 |
23 | val runtimeClasspath = storytaleCompilation.output.allOutputs + storytaleCompilation.runtimeDependencyFiles
24 | createJvmStorytaleExecTask(extension, storytaleCompilation, target, runtimeClasspath)
25 | createJvmStorytaleHotReloadExecTask(compilation = storytaleCompilation, target = target)
26 | }
27 |
28 | private fun Project.createJvmStorytaleGenerateSourceTask(
29 | extension: StorytaleExtension,
30 | target: KotlinJvmTarget,
31 | ) {
32 | val storytaleBuildDir = extension.getBuildDirectory(target)
33 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}") {
34 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
35 | description = "Generate JVM source files for '${target.name}'"
36 | title = target.name
37 | outputResourcesDir = file("$storytaleBuildDir/resources")
38 | outputSourcesDir = file("$storytaleBuildDir/sources")
39 | }
40 | }
41 |
42 | fun Project.createJvmStorytaleCompileTask(
43 | extension: StorytaleExtension,
44 | target: KotlinJvmTarget,
45 | ): KotlinJvmCompilation {
46 | val mainCompilation = target.compilations.named(KotlinCompilation.MAIN_COMPILATION_NAME).get()
47 | val storytaleBuildDir = extension.getBuildDirectory(target)
48 | val storytaleCompilation =
49 | target.compilations.create(StorytaleGradlePlugin.STORYTALE_SOURCESET_SUFFIX) as KotlinJvmCompilation
50 |
51 | storytaleCompilation.associateWith(mainCompilation)
52 |
53 | storytaleCompilation.apply {
54 | val sourceSet = kotlinSourceSets.single()
55 |
56 | sourceSet.dependsOn(extension.mainStoriesSourceSet)
57 |
58 | sourceSet.kotlin.setSrcDirs(files("$storytaleBuildDir/sources"))
59 |
60 | val generateTaskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_GENERATE_SUFFIX}"
61 |
62 | sourceSet.resources.srcDirs("$storytaleBuildDir/resources", mainCompilation.defaultSourceSet.resources)
63 |
64 | extension.project.tasks.named(processResourcesTaskName).configure {
65 | dependsOn(generateTaskName)
66 | }
67 |
68 | compileTaskProvider.configure {
69 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
70 | description = "Compile JVM storytale source files for '${target.name}'"
71 |
72 | dependsOn(generateTaskName)
73 | }
74 | }
75 | return storytaleCompilation
76 | }
77 |
78 | private fun Project.createJvmStorytaleExecTask(
79 | extension: StorytaleExtension,
80 | compilation: KotlinJvmCompilation,
81 | target: KotlinJvmTarget,
82 | runtimeClasspath: FileCollection,
83 | ) {
84 | task("${target.name}${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}Run") {
85 | dependsOn(compilation.compileTaskProvider)
86 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
87 | description = "Execute storytale for '${target.name}'"
88 |
89 | val storiesBuildDir = extension.getBuildDirectory(target)
90 | mainClass.set("org.jetbrains.compose.storytale.generated.MainKt")
91 |
92 | classpath(
93 | file("$storiesBuildDir/classes"),
94 | file("$storiesBuildDir/resources"),
95 | runtimeClasspath,
96 | )
97 |
98 | javaLauncher.set(javaLauncherProvider())
99 | }
100 | }
101 |
102 | private fun Project.createJvmStorytaleHotReloadExecTask(
103 | compilation: KotlinJvmCompilation,
104 | target: KotlinJvmTarget,
105 | ) {
106 | val taskName = "${target.name}${StorytaleGradlePlugin.STORYTALE_EXEC_SUFFIX}HotRun"
107 | project.plugins.withId("org.jetbrains.compose.hot-reload") {
108 | logger.info("Compose Hot Reload plugin found, configuring Storytale Hot Reload for ${target.name}")
109 | tasks.register(taskName) {
110 | this.compilation.set(compilation)
111 | group = StorytaleGradlePlugin.STORYTALE_TASK_GROUP
112 | description = "Execute storytale for '${target.name}' with hot reload"
113 | mainClass.set("org.jetbrains.compose.storytale.generated.MainKt")
114 | }
115 | }
116 | }
117 |
118 | private fun Project.javaLauncherProvider(): Provider = provider {
119 | val toolchainService = extensions.findByType(JavaToolchainService::class.java) ?: return@provider null
120 | val javaExtension = extensions.findByType(JavaPluginExtension::class.java) ?: return@provider null
121 | toolchainService.launcherFor(javaExtension.toolchain).orNull
122 | }
123 |
--------------------------------------------------------------------------------
/modules/preview-processor-test/src/androidUnitTest/kotlin/MakePreviewPublicFirExtensionRegistrarAndroidTest.kt:
--------------------------------------------------------------------------------
1 | package com.storytale
2 |
3 | import com.tschuchort.compiletesting.KotlinCompilation
4 | import java.lang.reflect.Modifier
5 | import kotlin.test.Ignore
6 | import org.assertj.core.api.Assertions.assertThat
7 | import org.junit.Test
8 | import util.storytaleTest
9 |
10 | class MakePreviewPublicFirExtensionRegistrarAndroidTest {
11 |
12 | @Test
13 | fun `ensure that private androidx Preview functions are made public`() {
14 | val compilation = storytaleTest {
15 | "PrivateAndroidPreview.kt" hasContent """
16 | package test
17 |
18 | import androidx.compose.runtime.Composable
19 |
20 | @Composable
21 | private fun PrivateAndroidComponent() {}
22 |
23 | @androidx.compose.ui.tooling.preview.Preview
24 | @Composable
25 | private fun PrivateAndroidPreviewFunction() {
26 | PrivateAndroidComponent()
27 | }
28 | """
29 | }
30 | val result = compilation.compile()
31 |
32 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
33 |
34 | val classLoader = result.classLoader
35 | val previewClass = classLoader.loadClass("test.PrivateAndroidPreviewKt")
36 |
37 | val previewFunction = previewClass.declaredMethods.find { it.name == "PrivateAndroidPreviewFunction" }
38 | assertThat(previewFunction).isNotNull
39 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue()
40 |
41 | val privateComponent = previewClass.declaredMethods.find { it.name == "PrivateAndroidComponent" }
42 | assertThat(privateComponent).isNotNull
43 | assertThat(Modifier.isPrivate(privateComponent!!.modifiers)).isTrue()
44 | }
45 |
46 | @Test
47 | fun `ensure that internal androidx Preview functions are made public`() {
48 | val compilation = storytaleTest {
49 | "InternalAndroidPreview.kt" hasContent """
50 | package test
51 |
52 | import androidx.compose.runtime.Composable
53 |
54 | @Composable
55 | internal fun InternalAndroidComponent() {}
56 |
57 | @androidx.compose.ui.tooling.preview.Preview
58 | @Composable
59 | internal fun InternalAndroidPreviewFunction() {
60 | InternalAndroidComponent()
61 | }
62 | """
63 | }
64 | val result = compilation.compile()
65 |
66 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
67 |
68 | val classLoader = result.classLoader
69 | val previewClass = classLoader.loadClass("test.InternalAndroidPreviewKt")
70 |
71 | val previewFunction = previewClass.declaredMethods.find { it.name == "InternalAndroidPreviewFunction" }
72 | assertThat(previewFunction).isNotNull
73 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue()
74 |
75 | val internalComponent = previewClass.declaredMethods.find { it.name == "InternalAndroidComponent" }
76 | assertThat(internalComponent).isNotNull
77 | assertThat(Modifier.isPublic(internalComponent!!.modifiers)).isTrue()
78 | }
79 |
80 | @Ignore("PreviewProcessor hasn't support protected Preview functions in classes yet")
81 | @Test
82 | fun `ensure that protected androidx Preview functions are made public in classes`() {
83 | val compilation = storytaleTest {
84 | "ProtectedAndroidPreview.kt" hasContent """
85 | package test
86 |
87 | import androidx.compose.runtime.Composable
88 |
89 | open class PreviewAndroidContainer {
90 | @Composable
91 | protected fun ProtectedAndroidComponent() {}
92 |
93 | @androidx.compose.ui.tooling.preview.Preview
94 | @Composable
95 | protected fun ProtectedAndroidPreviewFunction() {
96 | ProtectedAndroidComponent()
97 | }
98 | }
99 | """
100 | }
101 | val result = compilation.compile()
102 |
103 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
104 |
105 | val classLoader = result.classLoader
106 | val containerClass = classLoader.loadClass("test.PreviewAndroidContainer")
107 |
108 | val previewFunction = containerClass.declaredMethods.find { it.name == "ProtectedAndroidPreviewFunction" }
109 | assertThat(previewFunction).isNotNull
110 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue()
111 |
112 | val protectedComponent = containerClass.declaredMethods.find { it.name == "ProtectedAndroidComponent" }
113 | assertThat(protectedComponent).isNotNull
114 | assertThat(Modifier.isPublic(protectedComponent!!.modifiers)).isTrue()
115 | }
116 |
117 | @Test
118 | fun `ensure that public androidx Preview functions remain public`() {
119 | val compilation = storytaleTest {
120 | "PublicAndroidPreview.kt" hasContent """
121 | package test
122 |
123 | import androidx.compose.runtime.Composable
124 |
125 | @Composable
126 | fun PublicAndroidComponent() {}
127 |
128 | @androidx.compose.ui.tooling.preview.Preview
129 | @Composable
130 | fun PublicAndroidPreviewFunction() {
131 | PublicAndroidComponent()
132 | }
133 | """
134 | }
135 | val result = compilation.compile()
136 |
137 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
138 |
139 | val classLoader = result.classLoader
140 | val previewClass = classLoader.loadClass("test.PublicAndroidPreviewKt")
141 |
142 | val previewFunction = previewClass.declaredMethods.find { it.name == "PublicAndroidPreviewFunction" }
143 | assertThat(previewFunction).isNotNull
144 | assertThat(Modifier.isPublic(previewFunction!!.modifiers)).isTrue()
145 | }
146 | }
147 |
--------------------------------------------------------------------------------