├── .gitignore ├── LICENSE.txt ├── README.md ├── assets └── decomposer.gif ├── build.gradle.kts ├── compiler-plugin-gradle ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ └── kotlin │ └── com │ └── decomposer │ └── gradle │ ├── DecomposerGradlePlugin.kt │ └── DecomposerPluginExtension.kt ├── compiler-plugin ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── com │ │ └── decomposer │ │ └── compiler │ │ ├── BaseDecomposerTransformer.kt │ │ ├── CommandLineProcessor.kt │ │ ├── DecomposerComponentRegistrar.kt │ │ ├── FqNames.kt │ │ ├── IrSerializeTransformer.kt │ │ ├── PostComposeExtensions.kt │ │ └── PreComposeExtension.kt │ └── resources │ └── META-INF │ └── services │ ├── org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor │ └── org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar ├── composeApp ├── build.gradle.kts ├── icons │ ├── icon.icns │ ├── icon.ico │ └── icon.png └── src │ ├── commonMain │ ├── composeResources │ │ ├── drawable │ │ │ ├── data.svg │ │ │ ├── empty_group.svg │ │ │ ├── expand_all.svg │ │ │ ├── expand_data.svg │ │ │ ├── expand_down.svg │ │ │ ├── expand_right.svg │ │ │ ├── file.svg │ │ │ ├── fold_all.svg │ │ │ ├── fold_data.svg │ │ │ ├── folder_close.svg │ │ │ ├── folder_open.svg │ │ │ ├── group_attributes.svg │ │ │ ├── ic_launcher.png │ │ │ └── refresh.svg │ │ └── font │ │ │ ├── jetbrainsmono_bold.ttf │ │ │ ├── jetbrainsmono_bold_italic.ttf │ │ │ ├── jetbrainsmono_extrabold.ttf │ │ │ ├── jetbrainsmono_extrabold_italic.ttf │ │ │ ├── jetbrainsmono_italic.ttf │ │ │ ├── jetbrainsmono_medium.ttf │ │ │ ├── jetbrainsmono_medium_italic.ttf │ │ │ └── jetbrainsmono_regular.ttf │ └── kotlin │ │ └── com │ │ └── decomposer │ │ ├── ir │ │ └── IrProcessor.kt │ │ ├── server │ │ ├── Adb.kt │ │ └── Server.kt │ │ └── ui │ │ ├── CompositionPanel.kt │ │ ├── CompositionVisualizer.kt │ │ ├── ConnectionState.kt │ │ ├── DeviceDiscovery.kt │ │ ├── FileTreePanel.kt │ │ ├── FilterableTree.kt │ │ ├── Fonts.kt │ │ ├── IrPanel.kt │ │ ├── IrVisualizer.kt │ │ ├── MainApp.kt │ │ ├── Panels.kt │ │ ├── Theme.kt │ │ └── ToolBar.kt │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── decomposer │ │ └── ui │ │ └── FileTreePanelTest.kt │ └── desktopMain │ └── kotlin │ └── com │ └── decomposer │ └── main.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── publish.sh ├── runtime ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── decomposer │ │ └── runtime │ │ ├── AndroidReflections.kt │ │ ├── Client.android.kt │ │ ├── Logger.android.kt │ │ ├── Runtime.android.kt │ │ ├── composition │ │ └── CompositionNormalizer.android.kt │ │ └── ir │ │ └── ProjectScanner.android.kt │ └── commonMain │ ├── kotlin │ └── com │ │ └── decomposer │ │ └── runtime │ │ ├── Annotations.kt │ │ ├── Utils.kt │ │ ├── compose │ │ ├── CompositionNormalizer.kt │ │ └── Reflections.kt │ │ ├── connection │ │ ├── AbstractOkHttpClient.kt │ │ ├── Client.kt │ │ ├── ConnectionContract.kt │ │ └── model │ │ │ ├── Composition.kt │ │ │ ├── Contract.kt │ │ │ ├── Device.kt │ │ │ ├── Ir.kt │ │ │ └── SessionData.kt │ │ └── ir │ │ └── Project.kt │ └── proto │ └── com │ └── decomposer │ └── runtime │ └── ir │ ├── JvmIr.proto │ └── KotlinIr.proto ├── sample ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── decomposer │ │ └── sample │ │ ├── InteropSamples.kt │ │ ├── MainActivity.kt │ │ ├── SampleApplication.kt │ │ ├── SimpleStateReaderSample.kt │ │ ├── SubcomposeSamples.kt │ │ ├── TextSamples.kt │ │ └── ir │ │ ├── Classes.kt │ │ ├── Expressions.kt │ │ ├── Functions.kt │ │ ├── Operators.kt │ │ ├── Properties.kt │ │ ├── SimpleComposable.kt │ │ ├── TryCatchFinally.kt │ │ └── When.kt │ └── res │ ├── drawable │ ├── ic_launcher.png │ ├── ic_launcher_background.xml │ └── ic_launcher_foreground.xml │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ └── network_security_config.xml └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .kotlin 3 | .gradle 4 | **/build/ 5 | xcuserdata 6 | !src/**/build/ 7 | local.properties 8 | .idea 9 | .fleet 10 | .DS_Store 11 | captures 12 | .externalNativeBuild 13 | .cxx 14 | *.xcodeproj/* 15 | !*.xcodeproj/project.pbxproj 16 | !*.xcodeproj/xcshareddata/ 17 | !*.xcodeproj/project.xcworkspace/ 18 | !*.xcworkspace/contents.xcworkspacedata 19 | **/xcshareddata/WorkspaceSettings.xcsettings 20 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo 2 | 3 | ![Debugging JetLagged App](https://github.com/composexy/decomposer/blob/master/assets/decomposer.gif) 4 | 5 | 6 | ## Introduction 7 | 8 | Decomposer is a desktop app to help developers investigating jetpack compose internals. The app is built with jetpack compose for desktop. This tool provides two core utilities: 9 | * Viewing the ir structure of your app 10 | * Viewing the composition structure of your app 11 | 12 | ### Ir structure 13 | 14 | As you know, the compose framework uses a kotlin compiler plugin to rewrite your compose code. It might be helpful to understand how the compiler plugin rewrite your compose code. Compose compiler mainly works on the IR stage of kotlin compiler pipeline and there are challenges to view the IR structure in action. First of all, the IR tree is a transient structure in kotlin compiler, it is not serialized to disk after compilation. Secondly, the IR tree is not human readable. So this decomposer tool solves this problem by rendering the IR to a kotlin like format which makes it much easier to read. 15 | * Compare the IR tree before and after compose compiler plugin kicks in with a single click. 16 | * View the IR tree in origin format and kotlin like format with a single click. 17 | 18 | ### Composition structure 19 | 20 | The compose framework stores composition data in an internal data structure called SlotTable. It might be helpful to directly view the SlotTable in a human readable tree structure. The decomposer tool also does that. 21 | * View the whole SlotTable as a tree structure or only a subtree of the SlotTable. 22 | * Filter out empty or leaf tree groups in SlotTable. 23 | * Filter out compose framework created nodes. 24 | * View a subtree of the SlotTable. Currently supporting composition subtree, compose node subtree and recompose scope tree. 25 | * View the structure of state table, snapshot observers, layout nodes, recompose scopes, composable lambdas in the SlotTable. 26 | 27 | 28 | ## Usage 29 | 30 | There are three main components of this tool: 31 | * The decomposer desktop app 32 | * Decomposer kotlin compiler plugin and gradle plugin 33 | * Decomposer runtime 34 | 35 | ### Steps 36 | 1. Clone the repository 37 | 2. Download android studio and install the [kotlin multi-platform plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform). 38 | 3. Import the project. After importing, there should be two run configuration imported by android studio. The first one is the decomposer desktop app and the second one is a sample android app to use as a playground. 39 | 4. You can also run the desktop app via command line: ```./gradlew :composeApp:run```. Now you can run the sample app on an android device that is connected to the PC via usb cable. This makes sure the desktop app can find the device via adb command. 40 | 5. Optionally, you can create an installer for the desktop app via ```./gradlew package```. 41 | 42 | ### Use it in another app 43 | 1. Build the project via ```./gradlew assemble```. 44 | 2. Publish the artifacts to maven local: ```./gradlew publishToMavenLocal```. 45 | 3. Add mavenLocal to your project's artifacts searching repo. 46 | 4. The artifacts are also published to maven central if you do not want to build it locally. 47 | 48 | ``` 49 | gradle/libs.version.toml: 50 | 51 | [versions] 52 | decomposer = "[version]" 53 | 54 | [libs] 55 | decomposer-runtime = { group = "io.github.composexy-decomposer", name = "runtime-android", version.ref = "decomposer" } 56 | 57 | [plugins] 58 | decomposer = { id = "io.github.composexy-decomposer", version.ref = "decomposer" } 59 | 60 | 61 | root project setting.gradle.kts: 62 | 63 | pluginManagement { 64 | repositories { 65 | ... 66 | mavenLocal() 67 | } 68 | } 69 | 70 | 71 | root project build.gradle.kts: 72 | 73 | plugins { 74 | ... 75 | alias(libs.plugins.decomposer) apply false 76 | } 77 | 78 | 79 | app/build.gradle.kts: 80 | 81 | plugins { 82 | alias(libs.plugins.decomposer) 83 | } 84 | 85 | // Only enable in debug build 86 | kotlin { 87 | compilerOptions { 88 | val isDebug = project.hasProperty("android") 89 | && android.buildTypes.find { it.name == "debug" } != null 90 | if (isDebug) { 91 | freeCompilerArgs.addAll( 92 | "-P", "plugin:com.decomposer.compiler:enabled=true", 93 | ) 94 | } else { 95 | freeCompilerArgs.addAll( 96 | "-P", "plugin:com.decomposer.compiler:enabled=false" 97 | ) 98 | } 99 | } 100 | } 101 | 102 | dependencies { 103 | implementation(libs.decomposer.runtime) 104 | } 105 | 106 | 107 | AndroidManifest.xml: 108 | 109 | 110 | 111 | // Decomposer runtime uses websocket to communicate with the desktop app 112 | 113 | 114 | 118 | 119 | 120 | 121 | network_security_config.xml: 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | // Makes sure clear traffic is permitted on localhost 131 | 132 | localhost 133 | 134 | 135 | 136 | 137 | app/MyApplication.kt 138 | 139 | class MyApplication : Application() { 140 | override fun onCreate() { 141 | super.onCreate() 142 | // Only enable on debug build 143 | if (BuildConfig.DEBUG) { 144 | runtimeInit { 145 | packagePrefixes = listOf(this@MyApplication.packageName) 146 | } 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | ### notes 153 | 154 | * Each time you changed some code in your app while debugging, make sure you first uninstall your app from your phone and do a clean reinstall. Otherwise android studio may take some shortcuts which make the app's dex files in an inconsistent state. 155 | * The decomposer relies heavily on kotlin compiler plugin and embedded kotlin compiler internals. And the decomposer runtime uses reflection to retrieve composition data. That means this tool relies heavily on hidden apis of kotlin compiler and compose runtime. When you start the desktop app, you will see the a message about targeting compose runtime version and kotlin version. These versions are what the current decomposer tool is tested against. If your app uses a different kotlin version or compose runtime version. This tool may only partially work or not working at all. For example, your app needs to use at least kotlin 2.1.0 for decomposer to work. 156 | -------------------------------------------------------------------------------- /assets/decomposer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/assets/decomposer.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) apply false 3 | alias(libs.plugins.androidLibrary) apply false 4 | alias(libs.plugins.composeMultiplatform) apply false 5 | alias(libs.plugins.composeCompiler) apply false 6 | alias(libs.plugins.kotlinMultiplatform) apply false 7 | alias(libs.plugins.kotlin.jvm) apply false 8 | alias(libs.plugins.kotlin.android) apply false 9 | alias(libs.plugins.mavenPublish) apply false 10 | } 11 | 12 | subprojects { 13 | if (this.name != "composeApp" && this.name != "sample") { 14 | group = project.property("GROUP") as String 15 | version = project.property("VERSION_NAME") as String 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /compiler-plugin-gradle/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compiler-plugin-gradle/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.jvm) 6 | `java-gradle-plugin` 7 | alias(libs.plugins.mavenPublish) 8 | } 9 | 10 | sourceSets { 11 | main { java.srcDir(layout.buildDirectory.dir("generated/sources/version-templates/kotlin/main")) } 12 | } 13 | 14 | tasks.withType().configureEach { 15 | compilerOptions { 16 | languageVersion.set(KotlinVersion.KOTLIN_1_8) 17 | apiVersion.set(KotlinVersion.KOTLIN_1_8) 18 | } 19 | } 20 | 21 | java { 22 | toolchain { 23 | languageVersion.set(JavaLanguageVersion.of(17)) 24 | } 25 | } 26 | 27 | gradlePlugin { 28 | plugins { 29 | create("decomposer") { 30 | id = "io.github.composexy-decomposer" 31 | implementationClass = "com.decomposer.gradle.DecomposerGradlePlugin" 32 | } 33 | } 34 | } 35 | 36 | dependencies { 37 | compileOnly(libs.kotlin.gradlePlugin) 38 | compileOnly(libs.kotlin.gradlePlugin.api) 39 | compileOnly(libs.kotlin.stdlib) 40 | } 41 | -------------------------------------------------------------------------------- /compiler-plugin-gradle/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Decomposer gradle plugin 2 | POM_ARTIFACT_ID=decomposer-gradle-plugin 3 | -------------------------------------------------------------------------------- /compiler-plugin-gradle/src/main/kotlin/com/decomposer/gradle/DecomposerGradlePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.gradle 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Provider 5 | 6 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation 7 | import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin 8 | import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact 9 | import org.jetbrains.kotlin.gradle.plugin.SubpluginOption 10 | 11 | class DecomposerGradlePlugin : KotlinCompilerPluginSupportPlugin { 12 | 13 | override fun apply(target: Project) { 14 | target.extensions.create("decomposer", DecomposerPluginExtension::class.java) 15 | } 16 | 17 | override fun applyToCompilation( 18 | kotlinCompilation: KotlinCompilation<*> 19 | ): Provider> { 20 | val project = kotlinCompilation.target.project 21 | val extension = project.extensions.getByType(DecomposerPluginExtension::class.java) 22 | val enabled = extension.enabled.get() 23 | return project.provider { 24 | listOf(SubpluginOption(key = "enabled", value = enabled.toString())) 25 | } 26 | } 27 | 28 | override fun getCompilerPluginId() = "com.decomposer.compiler" 29 | 30 | override fun getPluginArtifact(): SubpluginArtifact = 31 | SubpluginArtifact( 32 | groupId = "io.github.composexy-decomposer", 33 | artifactId = "decomposer-compiler", 34 | version = VERSION 35 | ) 36 | 37 | override fun isApplicable(kotlinCompilation: KotlinCompilation<*>) = true 38 | 39 | companion object { 40 | const val VERSION = "0.1.0-alpha1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /compiler-plugin-gradle/src/main/kotlin/com/decomposer/gradle/DecomposerPluginExtension.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.gradle 2 | 3 | import javax.inject.Inject 4 | import org.gradle.api.model.ObjectFactory 5 | import org.gradle.api.provider.Property 6 | 7 | abstract class DecomposerPluginExtension @Inject constructor(objects: ObjectFactory) { 8 | val enabled: Property = 9 | objects.property(Boolean::class.javaObjectType).convention(true) 10 | } 11 | -------------------------------------------------------------------------------- /compiler-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /compiler-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | alias(libs.plugins.mavenPublish) 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_11 8 | targetCompatibility = JavaVersion.VERSION_11 9 | } 10 | 11 | kotlin { 12 | compilerOptions { 13 | jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11 14 | } 15 | } 16 | 17 | dependencies { 18 | compileOnly(libs.kotlin.compilerEmbeddable) 19 | compileOnly(libs.kotlin.stdlib) 20 | testImplementation(libs.kotlin.reflect) 21 | testImplementation(libs.kotlin.stdlib) 22 | testImplementation(libs.kotlin.compilerEmbeddable) 23 | testImplementation(libs.junit) 24 | } 25 | -------------------------------------------------------------------------------- /compiler-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Decomposer compiler plugin 2 | POM_ARTIFACT_ID=decomposer-compiler 3 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/BaseDecomposerTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.config.CompilerConfiguration 8 | import org.jetbrains.kotlin.config.JVMConfigurationKeys 9 | import org.jetbrains.kotlin.config.JvmSerializeIrMode 10 | import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET 11 | import org.jetbrains.kotlin.ir.expressions.IrConst 12 | import org.jetbrains.kotlin.ir.expressions.IrConstKind 13 | import org.jetbrains.kotlin.ir.expressions.IrExpression 14 | import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl 15 | import org.jetbrains.kotlin.ir.expressions.impl.IrVarargImpl 16 | import org.jetbrains.kotlin.ir.symbols.IrClassSymbol 17 | import org.jetbrains.kotlin.ir.types.typeWith 18 | import org.jetbrains.kotlin.name.ClassId 19 | 20 | abstract class BaseDecomposerTransformer( 21 | private val messageCollector: MessageCollector, 22 | private val context: IrPluginContext 23 | ) : IrElementTransformerVoidWithContext() { 24 | protected fun log(message: String) { 25 | messageCollector.report(CompilerMessageSeverity.LOGGING, message) 26 | } 27 | 28 | protected fun withSerializeIrOption( 29 | compilerConfiguration: CompilerConfiguration, 30 | block: () -> R 31 | ): R { 32 | val previous = compilerConfiguration[JVMConfigurationKeys.SERIALIZE_IR] 33 | ?: JvmSerializeIrMode.NONE 34 | val result = try { 35 | compilerConfiguration.put(JVMConfigurationKeys.SERIALIZE_IR, JvmSerializeIrMode.ALL) 36 | block() 37 | } finally { 38 | compilerConfiguration.put(JVMConfigurationKeys.SERIALIZE_IR, previous) 39 | } 40 | return result 41 | } 42 | 43 | protected fun getTopLevelClass(classId: ClassId): IrClassSymbol { 44 | return context.referenceClass(classId) 45 | ?: error("Class not find ${classId.asSingleFqName()} in classpath!") 46 | } 47 | 48 | protected fun irConst(value: String): IrConst { 49 | return IrConstImpl( 50 | UNDEFINED_OFFSET, 51 | UNDEFINED_OFFSET, 52 | context.irBuiltIns.stringType, 53 | IrConstKind.String, 54 | value 55 | ) 56 | } 57 | 58 | protected fun irConst(value: Boolean): IrConst { 59 | return IrConstImpl( 60 | UNDEFINED_OFFSET, 61 | UNDEFINED_OFFSET, 62 | context.irBuiltIns.booleanType, 63 | IrConstKind.Boolean, 64 | value 65 | ) 66 | } 67 | 68 | protected fun irStringArray(value: Array): IrExpression { 69 | val builtIns = context.irBuiltIns 70 | val arrayType = builtIns.arrayClass.typeWith(builtIns.stringType) 71 | return IrVarargImpl( 72 | UNDEFINED_OFFSET, 73 | UNDEFINED_OFFSET, 74 | type = arrayType, 75 | varargElementType = builtIns.stringType, 76 | elements = value.map { irConst(it) } 77 | ) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/CommandLineProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 4 | import org.jetbrains.kotlin.compiler.plugin.CliOption 5 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 6 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 7 | import org.jetbrains.kotlin.config.CompilerConfiguration 8 | import org.jetbrains.kotlin.config.CompilerConfigurationKey 9 | 10 | @OptIn(ExperimentalCompilerApi::class) 11 | class DecomposerCommandLineProcessor : CommandLineProcessor { 12 | 13 | internal companion object { 14 | private const val DESCRIPTION_BOOLEAN = "" 15 | 16 | val OPTION_ENABLED = CliOption( 17 | optionName = "enabled", 18 | valueDescription = DESCRIPTION_BOOLEAN, 19 | description = KEY_ENABLED.toString(), 20 | required = false, 21 | allowMultipleOccurrences = false 22 | ) 23 | } 24 | 25 | override val pluginId = "com.decomposer.compiler" 26 | 27 | override val pluginOptions: Collection = listOf(OPTION_ENABLED) 28 | 29 | override fun processOption( 30 | option: AbstractCliOption, 31 | value: String, 32 | configuration: CompilerConfiguration 33 | ) { 34 | when (option.optionName) { 35 | OPTION_ENABLED.optionName -> { 36 | configuration.put(KEY_ENABLED, value.toBoolean()) 37 | } 38 | else -> error("Unknown decomposer plugin option: ${option.optionName}") 39 | } 40 | } 41 | } 42 | 43 | internal val KEY_ENABLED = CompilerConfigurationKey( 44 | "Disable all decomposer features if false." 45 | ) 46 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/DecomposerComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("DEPRECATION", "UnstableApiUsage") 2 | 3 | package com.decomposer.compiler 4 | 5 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 6 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 7 | import org.jetbrains.kotlin.com.intellij.mock.MockProject 8 | import org.jetbrains.kotlin.com.intellij.openapi.extensions.LoadingOrder 9 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 10 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 11 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 12 | import org.jetbrains.kotlin.config.CompilerConfiguration 13 | 14 | @OptIn(ExperimentalCompilerApi::class) 15 | class DecomposerComponentRegistrar : ComponentRegistrar { 16 | override val supportsK2: Boolean 17 | get() = true 18 | 19 | override fun registerProjectComponents( 20 | project: MockProject, 21 | configuration: CompilerConfiguration 22 | ) { 23 | if (configuration[KEY_ENABLED] == false) return 24 | val messageCollector = configuration.get( 25 | CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) 26 | project.extensionArea.getExtensionPoint(IrGenerationExtension.extensionPointName).run { 27 | registerExtension( 28 | PreComposeExtension(messageCollector, configuration), 29 | LoadingOrder.FIRST, 30 | project 31 | ) 32 | registerExtension( 33 | PostComposeExtensions(messageCollector, configuration), 34 | LoadingOrder.LAST, 35 | project 36 | ) 37 | } 38 | } 39 | 40 | companion object { 41 | private const val COMPOSE_PLUGIN_ID = "androidx.compose.compiler.plugins.kotlin" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/FqNames.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.name.ClassId 4 | import org.jetbrains.kotlin.name.FqName 5 | 6 | const val decomposerRuntimePackage = "com.decomposer.runtime" 7 | 8 | val FQ_PRE_COMPOSE_IR: FqName = FqName("$decomposerRuntimePackage.PreComposeIr") 9 | val CLASS_ID_PRE_COMPOSE_IR: ClassId = ClassId.topLevel(FQ_PRE_COMPOSE_IR) 10 | val FQ_POST_COMPOSE_IR: FqName = FqName("$decomposerRuntimePackage.PostComposeIr") 11 | val CLASS_ID_POST_COMPOSE_IR: ClassId = ClassId.topLevel(FQ_POST_COMPOSE_IR) 12 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/IrSerializeTransformer.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 4 | import org.jetbrains.kotlin.backend.jvm.JvmIrSerializerImpl 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.config.CompilerConfiguration 7 | import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET 8 | import org.jetbrains.kotlin.ir.declarations.IrClass 9 | import org.jetbrains.kotlin.ir.declarations.IrFile 10 | import org.jetbrains.kotlin.ir.expressions.IrConstructorCall 11 | import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl 12 | import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI 13 | import org.jetbrains.kotlin.ir.types.defaultType 14 | import org.jetbrains.kotlin.ir.util.constructors 15 | import org.jetbrains.kotlin.ir.util.dump 16 | import org.jetbrains.kotlin.metadata.jvm.deserialization.BitEncoding 17 | 18 | @OptIn(UnsafeDuringIrConstructionAPI::class) 19 | class IrSerializeTransformer( 20 | composed: Boolean, 21 | messageCollector: MessageCollector, 22 | private val configuration: CompilerConfiguration, 23 | context: IrPluginContext 24 | ) : BaseDecomposerTransformer(messageCollector, context) { 25 | 26 | private val irSerializer = JvmIrSerializerImpl(configuration) 27 | private val postComposeIrClass = getTopLevelClass(CLASS_ID_POST_COMPOSE_IR) 28 | private val preComposeIrClass = getTopLevelClass(CLASS_ID_PRE_COMPOSE_IR) 29 | private val composeIrClass = if (composed) { 30 | postComposeIrClass 31 | } else { 32 | preComposeIrClass 33 | } 34 | 35 | override fun visitFileNew(declaration: IrFile): IrFile { 36 | return withSerializeIrOption(configuration) { 37 | val fileIr = irSerializer.serializeIrFile(declaration) 38 | val fileIrDump = withoutDecomposerAnnotations(declaration) { 39 | irStringArray(BitEncoding.encodeBytes(dump().encodeToByteArray())) 40 | } 41 | var dumpAnnotated = false 42 | 43 | if (fileIr != null) { 44 | declaration.annotations += irComposeIrCall().apply { 45 | putValueArgument(0, irConst(declaration.fileEntry.name)) 46 | putValueArgument(1, irConst(declaration.packageFqName.asString())) 47 | putValueArgument(2, irConst(true)) 48 | putValueArgument(3, fileIrDump) 49 | putValueArgument(4, irStringArray(BitEncoding.encodeBytes(fileIr))) 50 | } 51 | dumpAnnotated = true 52 | } 53 | 54 | for (irClass in declaration.declarations.filterIsInstance()) { 55 | val topLevelClassIr = irSerializer.serializeTopLevelIrClass(irClass) ?: continue 56 | val irDump = if (dumpAnnotated) irStringArray(emptyArray()) else fileIrDump 57 | irClass.annotations += irComposeIrCall().apply { 58 | putValueArgument(0, irConst(declaration.fileEntry.name)) 59 | putValueArgument(1, irConst(declaration.packageFqName.asString())) 60 | putValueArgument(2, irConst(false)) 61 | putValueArgument(3, irDump) 62 | putValueArgument(4, irStringArray(BitEncoding.encodeBytes(topLevelClassIr))) 63 | } 64 | dumpAnnotated = true 65 | } 66 | 67 | declaration 68 | } 69 | } 70 | 71 | private fun irComposeIrCall(): IrConstructorCall = 72 | IrConstructorCallImpl( 73 | startOffset = UNDEFINED_OFFSET, 74 | endOffset = UNDEFINED_OFFSET, 75 | type = composeIrClass.defaultType, 76 | symbol = composeIrClass.constructors.first(), 77 | typeArgumentsCount = 0, 78 | constructorTypeArgumentsCount = 0 79 | ) 80 | 81 | private fun withoutDecomposerAnnotations( 82 | declaration: IrFile, 83 | block: IrFile.() -> R 84 | ): R { 85 | val removeOnFile = declaration.annotations.filter { 86 | it.type == postComposeIrClass.defaultType || it.type == preComposeIrClass.defaultType 87 | } 88 | val removeOnClasses = mutableMapOf>() 89 | return try { 90 | declaration.annotations -= removeOnFile 91 | declaration.declarations.filterIsInstance().forEach { irClass -> 92 | val removeOnClass = irClass.annotations.filter { 93 | it.type == postComposeIrClass.defaultType || 94 | it.type == preComposeIrClass.defaultType 95 | } 96 | removeOnClasses[irClass] = removeOnClass 97 | irClass.annotations -= removeOnClass 98 | } 99 | block(declaration) 100 | } finally { 101 | declaration.annotations += removeOnFile 102 | declaration.declarations.filterIsInstance().forEach { irClass -> 103 | removeOnClasses[irClass]?.let { 104 | irClass.annotations += it 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/PostComposeExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.config.CompilerConfiguration 7 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 8 | 9 | class PostComposeExtensions( 10 | private val messageCollector: MessageCollector, 11 | private val configuration: CompilerConfiguration 12 | ) : IrGenerationExtension { 13 | override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { 14 | moduleFragment.transform( 15 | IrSerializeTransformer( 16 | composed = true, 17 | messageCollector = messageCollector, 18 | configuration = configuration, 19 | context = pluginContext 20 | ), 21 | null 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/kotlin/com/decomposer/compiler/PreComposeExtension.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.compiler 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.config.CompilerConfiguration 7 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 8 | 9 | class PreComposeExtension( 10 | private val messageCollector: MessageCollector, 11 | private val configuration: CompilerConfiguration 12 | ) : IrGenerationExtension { 13 | override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { 14 | moduleFragment.transform( 15 | IrSerializeTransformer( 16 | composed = false, 17 | messageCollector = messageCollector, 18 | configuration = configuration, 19 | context = pluginContext 20 | ), 21 | null 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor: -------------------------------------------------------------------------------- 1 | com.decomposer.compiler.DecomposerCommandLineProcessor -------------------------------------------------------------------------------- /compiler-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar: -------------------------------------------------------------------------------- 1 | com.decomposer.compiler.DecomposerComponentRegistrar -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.composeMultiplatform) 6 | alias(libs.plugins.composeCompiler) 7 | kotlin(libs.plugins.kotlinx.serialization.get().pluginId) version libs.versions.kotlin 8 | } 9 | 10 | kotlin { 11 | jvm("desktop") 12 | 13 | sourceSets { 14 | val desktopMain by getting 15 | commonMain.dependencies { 16 | implementation(compose.runtime) 17 | implementation(compose.foundation) 18 | implementation(compose.material) 19 | implementation(compose.material3) 20 | implementation(compose.ui) 21 | implementation(compose.components.resources) 22 | implementation(compose.components.uiToolingPreview) 23 | implementation(libs.ktor.server.core) 24 | implementation(libs.ktor.server.netty) 25 | implementation(libs.ktor.server.websockets) 26 | implementation(libs.ktor.server.contentNegotiation) 27 | implementation(libs.ktor.serialization.kotlinx.json) 28 | implementation(libs.kotlin.compilerEmbeddable) 29 | implementation(libs.squareup.moshi) 30 | implementation(libs.squareup.moshiAdapters) 31 | implementation(libs.squareup.wire.moshiAdapter) 32 | implementation(libs.kotlinx.serializationJson) 33 | implementation(projects.runtime) 34 | } 35 | commonTest.dependencies { 36 | implementation(libs.junit) 37 | } 38 | desktopMain.dependencies { 39 | implementation(compose.desktop.currentOs) 40 | implementation(libs.kotlinx.coroutines.swing) 41 | } 42 | } 43 | } 44 | 45 | compose.desktop { 46 | application { 47 | mainClass = "com.decomposer.MainKt" 48 | 49 | nativeDistributions { 50 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 51 | packageName = "decomposer" 52 | packageVersion = "1.0.0" 53 | description = "Decomposer desktop app" 54 | macOS { 55 | iconFile.set(project.file("icons/icon.icns")) 56 | } 57 | windows { 58 | iconFile.set(project.file("icons/icon.ico")) 59 | } 60 | linux { 61 | iconFile.set(project.file("icons/icon.png")) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /composeApp/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/icons/icon.icns -------------------------------------------------------------------------------- /composeApp/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/icons/icon.ico -------------------------------------------------------------------------------- /composeApp/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/icons/icon.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/empty_group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/expand_all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/expand_data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/expand_down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/expand_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/fold_all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/fold_data.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/folder_close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/folder_open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/group_attributes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/drawable/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_bold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_bold_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_extrabold.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_extrabold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_extrabold_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_medium.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_medium_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/composeApp/src/commonMain/composeResources/font/jetbrainsmono_regular.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/server/Adb.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.server 2 | 3 | import java.io.BufferedReader 4 | import java.io.InputStreamReader 5 | 6 | class AdbConnection(private val port: Int) { 7 | 8 | fun connect(): AdbConnectResult { 9 | val processBuilder = ProcessBuilder("adb", "reverse", "tcp:$port", "tcp:$port") 10 | try { 11 | val process = processBuilder.start() 12 | val errorReader = BufferedReader(InputStreamReader(process.errorStream)) 13 | val exitCode = process.waitFor() 14 | if (exitCode != 0) { 15 | val errorMessage = errorReader.use { 16 | buildString { 17 | val lines = errorReader.readLines() 18 | lines.forEach { append(it) } 19 | } 20 | } 21 | return AdbConnectResult.Failure(errorMessage) 22 | } else { 23 | return AdbConnectResult.Success 24 | } 25 | } catch (ex: Exception) { 26 | return AdbConnectResult.Failure( 27 | ex.message ?: ex.stackTraceToString().lines().first() 28 | ) 29 | } 30 | } 31 | } 32 | 33 | sealed interface AdbConnectResult { 34 | 35 | data object Idle : AdbConnectResult 36 | 37 | data object Success : AdbConnectResult 38 | 39 | data object Skipped : AdbConnectResult 40 | 41 | data class Failure(val errorMessage: String) : AdbConnectResult 42 | } 43 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/server/Server.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.server 2 | 3 | import com.decomposer.runtime.connection.ConnectionContract 4 | import com.decomposer.runtime.connection.model.Command 5 | import com.decomposer.runtime.connection.model.CommandKeys 6 | import com.decomposer.runtime.connection.model.CommandResponse 7 | import com.decomposer.runtime.connection.model.CompositionDataResponse 8 | import com.decomposer.runtime.connection.model.CompositionRoots 9 | import com.decomposer.runtime.connection.model.DeviceType 10 | import com.decomposer.runtime.connection.model.ProjectSnapshot 11 | import com.decomposer.runtime.connection.model.ProjectSnapshotResponse 12 | import com.decomposer.runtime.connection.model.SessionData 13 | import com.decomposer.runtime.connection.model.VirtualFileIr 14 | import com.decomposer.runtime.connection.model.VirtualFileIrResponse 15 | import com.decomposer.runtime.connection.model.commandResponseSerializer 16 | import io.ktor.http.HttpStatusCode 17 | import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter 18 | import io.ktor.serialization.kotlinx.json.json 19 | import io.ktor.server.application.install 20 | import io.ktor.server.engine.EmbeddedServer 21 | import io.ktor.server.engine.embeddedServer 22 | import io.ktor.server.netty.Netty 23 | import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 24 | import io.ktor.server.response.respond 25 | import io.ktor.server.routing.RoutingContext 26 | import io.ktor.server.routing.get 27 | import io.ktor.server.routing.routing 28 | import io.ktor.server.websocket.DefaultWebSocketServerSession 29 | import io.ktor.server.websocket.WebSockets 30 | import io.ktor.server.websocket.pingPeriod 31 | import io.ktor.server.websocket.receiveDeserialized 32 | import io.ktor.server.websocket.sendSerialized 33 | import io.ktor.server.websocket.timeout 34 | import io.ktor.server.websocket.webSocket 35 | import io.ktor.websocket.close 36 | import kotlinx.coroutines.channels.Channel 37 | import kotlinx.coroutines.channels.ClosedReceiveChannelException 38 | import kotlinx.coroutines.channels.SendChannel 39 | import kotlinx.coroutines.coroutineScope 40 | import kotlinx.coroutines.flow.MutableSharedFlow 41 | import kotlinx.coroutines.flow.MutableStateFlow 42 | import kotlinx.coroutines.flow.filterNotNull 43 | import kotlinx.coroutines.launch 44 | import kotlinx.serialization.json.Json 45 | import java.util.concurrent.TimeUnit 46 | import kotlin.time.Duration.Companion.seconds 47 | import kotlin.uuid.ExperimentalUuidApi 48 | import kotlin.uuid.Uuid 49 | 50 | @OptIn(ExperimentalUuidApi::class) 51 | class DefaultServer(private val serverPort: Int) { 52 | 53 | private val _sessionStateFlow = MutableStateFlow(SessionState.Idle) 54 | val sessionStateFlow = _sessionStateFlow 55 | private var embeddedServer: EmbeddedServer<*, *>? = null 56 | 57 | fun start() { 58 | embeddedServer = embeddedServer(Netty, serverPort) { 59 | install(WebSockets) { 60 | pingPeriod = PING_INTERVAL_SECONDS.seconds 61 | timeout = CONNECTION_TIMEOUT_SECONDS.seconds 62 | contentConverter = KotlinxWebsocketSerializationConverter(Json) 63 | } 64 | install(ContentNegotiation) { 65 | json(commandResponseSerializer) 66 | } 67 | routing { 68 | get(ConnectionContract.DEFAULT_CONNECTION_PATH) { 69 | processSessionCreation() 70 | } 71 | webSocket("/session/{id}") { 72 | val sessionId = call.parameters["id"] 73 | try { 74 | val sessionState = sessionStateFlow.value 75 | when { 76 | sessionState !is SessionState.Connected -> { 77 | println("No active session!") 78 | } 79 | sessionState.session.sessionId != sessionId -> { 80 | println("Expected ${sessionState.session.sessionId} received $sessionId") 81 | } 82 | else -> { 83 | with(sessionState.session) { handleSession() } 84 | } 85 | } 86 | } catch (ex: ClosedReceiveChannelException) { 87 | println("Session $sessionId is closed!") 88 | } catch (ex: Throwable) { 89 | println("Encountered session error ${ex.stackTraceToString()}") 90 | } finally { 91 | println("Session $sessionId ended.") 92 | _sessionStateFlow.emit(SessionState.Disconnected(sessionId!!)) 93 | } 94 | } 95 | } 96 | }.start(wait = false) 97 | _sessionStateFlow.value = SessionState.Started(port = serverPort) 98 | } 99 | 100 | fun stop() { 101 | embeddedServer?.stop( 102 | shutdownGracePeriod = 0, 103 | shutdownTimeout = 0, 104 | timeUnit = TimeUnit.MILLISECONDS 105 | ) 106 | } 107 | 108 | private suspend fun RoutingContext.processSessionCreation() { 109 | val sessionState = _sessionStateFlow.value 110 | if (sessionState is SessionState.Connected) { 111 | println("Cleaning existing session ${sessionState.session.sessionId}") 112 | sessionState.session.close() 113 | _sessionStateFlow.value = SessionState.Disconnected(sessionState.session.sessionId) 114 | } 115 | val deviceType = call.request.headers[ConnectionContract.HEADER_DEVICE_TYPE] 116 | when (deviceType) { 117 | DeviceType.ANDROID.name -> { 118 | val sessionId = Uuid.random().toString() 119 | _sessionStateFlow.emit(SessionState.Connected(Session(sessionId))) 120 | call.respond(HttpStatusCode.OK, SessionData(sessionId, sessionUrl(sessionId))) 121 | } 122 | else -> { 123 | call.respond( 124 | status = HttpStatusCode.BadRequest, 125 | message = "Only android device supported!" 126 | ) 127 | } 128 | } 129 | } 130 | 131 | private fun sessionUrl(sessionId: String) = "/session/$sessionId" 132 | 133 | companion object { 134 | private const val PING_INTERVAL_SECONDS = 5 135 | private const val CONNECTION_TIMEOUT_SECONDS = 5 136 | } 137 | } 138 | 139 | class Session(val sessionId: String) { 140 | private var projectSnapshot: ProjectSnapshot? = null 141 | private val virtualFileIrByFilePath = mutableMapOf() 142 | private val projectSnapshotRequests = 143 | MutableSharedFlow(replay = 1, extraBufferCapacity = 5) 144 | private val virtualFileIrRequests = 145 | MutableSharedFlow(replay = 1, extraBufferCapacity = 20) 146 | private val compositionDataRequests = 147 | MutableSharedFlow(replay = 1, extraBufferCapacity = 5) 148 | private val projectSnapshotWaiters = mutableListOf>() 149 | private val virtualFileIrWaiters = 150 | mutableMapOf>>() 151 | private val compositionDataWaiters = mutableListOf>() 152 | private var websocketSession: DefaultWebSocketServerSession? = null 153 | 154 | internal suspend fun DefaultWebSocketServerSession.handleSession() { 155 | websocketSession = this 156 | println("Start handling session $sessionId") 157 | coroutineScope { 158 | launch { 159 | projectSnapshotRequests.filterNotNull().collect { 160 | sendSerialized(it.command) 161 | synchronized(projectSnapshotWaiters) { 162 | projectSnapshotWaiters.add(it.receive) 163 | } 164 | } 165 | } 166 | 167 | launch { 168 | virtualFileIrRequests.filterNotNull().collect { 169 | sendSerialized(it.command) 170 | synchronized(virtualFileIrWaiters) { 171 | virtualFileIrWaiters.computeIfAbsent(it.filePath) { 172 | mutableListOf() 173 | }.add(it.receive) 174 | } 175 | } 176 | } 177 | 178 | launch { 179 | compositionDataRequests.filterNotNull().collect { 180 | sendSerialized(it.command) 181 | synchronized(compositionDataWaiters) { 182 | compositionDataWaiters.add(it.receive) 183 | } 184 | } 185 | } 186 | 187 | launch { 188 | while (true) { 189 | when (val response = receiveDeserialized()) { 190 | is CompositionDataResponse -> { 191 | val waiters = synchronized(compositionDataWaiters) { 192 | mutableListOf>().also { 193 | it.addAll(compositionDataWaiters) 194 | compositionDataWaiters.clear() 195 | } 196 | } 197 | waiters.forEach { 198 | it.send(response.compositionRoots) 199 | } 200 | } 201 | is ProjectSnapshotResponse -> { 202 | val waiters = synchronized(projectSnapshotWaiters) { 203 | mutableListOf>().also { 204 | it.addAll(projectSnapshotWaiters) 205 | projectSnapshotWaiters.clear() 206 | } 207 | } 208 | waiters.forEach { 209 | it.send(response.projectSnapshot) 210 | } 211 | } 212 | is VirtualFileIrResponse -> { 213 | val waiters = synchronized(virtualFileIrWaiters) { 214 | virtualFileIrWaiters[response.virtualFileIr.filePath]?.let { 215 | mutableListOf>().also { list -> 216 | list.addAll(it) 217 | it.clear() 218 | } 219 | } ?: emptyList() 220 | } 221 | waiters.forEach { 222 | it.send(response.virtualFileIr) 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | suspend fun getProjectSnapshot(): ProjectSnapshot { 232 | val cached = projectSnapshot 233 | if (cached != null) { 234 | return cached 235 | } 236 | val request = ProjectSnapshotRequest() 237 | projectSnapshotRequests.emit(request) 238 | return request.receive.receive() 239 | } 240 | 241 | suspend fun getVirtualFileIr(filePath: String): VirtualFileIr { 242 | val cached = virtualFileIrByFilePath[filePath] 243 | if (cached != null) { 244 | return cached 245 | } 246 | val request = VirtualFileIrRequest(filePath) 247 | virtualFileIrRequests.emit(request) 248 | return request.receive.receive() 249 | } 250 | 251 | suspend fun getCompositionData(): CompositionRoots { 252 | val request = CompositionDataRequest() 253 | compositionDataRequests.emit(request) 254 | return request.receive.receive() 255 | } 256 | 257 | suspend fun close() { 258 | websocketSession?.close() 259 | } 260 | 261 | private sealed class Request( 262 | val command: Command, 263 | val receive: Channel = Channel(1) 264 | ) 265 | 266 | private class ProjectSnapshotRequest : Request( 267 | command = Command(CommandKeys.PROJECT_SNAPSHOT) 268 | ) 269 | 270 | private class VirtualFileIrRequest(val filePath: String) : Request( 271 | command = Command(CommandKeys.VIRTUAL_FILE_IR, listOf(filePath)) 272 | ) 273 | 274 | private class CompositionDataRequest : Request( 275 | command = Command(CommandKeys.COMPOSITION_DATA) 276 | ) 277 | } 278 | 279 | sealed interface SessionState { 280 | 281 | data object Idle : SessionState 282 | 283 | class Started(val port: Int): SessionState 284 | 285 | class Disconnected(val sessionId: String) : SessionState 286 | 287 | class Connected(val session: Session) : SessionState 288 | } 289 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/ConnectionState.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.decomposer.runtime.connection.ConnectionContract 6 | import com.decomposer.server.AdbConnectResult 7 | import com.decomposer.server.AdbConnection 8 | import com.decomposer.server.DefaultServer 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | 12 | class ConnectionState(port: Int) { 13 | private val server: DefaultServer = DefaultServer(port) 14 | private val adbConnection: AdbConnection = AdbConnection(port) 15 | 16 | private val _adbConnectState = MutableStateFlow(AdbConnectResult.Idle) 17 | val adbConnectState: StateFlow = _adbConnectState 18 | 19 | val sessionState = server.sessionStateFlow 20 | 21 | fun skipConnect() { 22 | _adbConnectState.value = AdbConnectResult.Skipped 23 | } 24 | 25 | fun adbConnect() { 26 | val connectResult = adbConnection.connect() 27 | _adbConnectState.value = connectResult 28 | } 29 | 30 | fun serverConnect() { 31 | server.start() 32 | } 33 | 34 | fun serverDisconnect() { 35 | server.stop() 36 | } 37 | } 38 | 39 | @Composable 40 | fun rememberConnectionState(port: Int = ConnectionContract.DEFAULT_SERVER_PORT): ConnectionState { 41 | return remember(port) { 42 | ConnectionState(port) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/DeviceDiscovery.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.heightIn 8 | import androidx.compose.foundation.layout.widthIn 9 | import androidx.compose.foundation.layout.wrapContentHeight 10 | import androidx.compose.material.Button 11 | import androidx.compose.material.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.layout.ContentScale 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import com.decomposer.server.AdbConnectResult 21 | import decomposer.composeapp.generated.resources.Res 22 | import decomposer.composeapp.generated.resources.ic_launcher 23 | import org.jetbrains.compose.resources.painterResource 24 | 25 | @Composable 26 | fun DeviceDiscovery( 27 | modifier: Modifier, 28 | versions: Versions, 29 | adbState: AdbConnectResult, 30 | onConnect: () -> Unit, 31 | onSkip: () -> Unit 32 | ) { 33 | Column( 34 | modifier = modifier.wrapContentHeight(), 35 | horizontalAlignment = Alignment.CenterHorizontally 36 | ) { 37 | Image( 38 | modifier = Modifier.heightIn(80.dp, 120.dp).widthIn(80.dp, 120.dp), 39 | painter = painterResource(Res.drawable.ic_launcher), 40 | contentDescription = "Launcher logo", 41 | contentScale = ContentScale.Fit 42 | ) 43 | DefaultText( 44 | text = """ 45 | Version: ${versions.DECOMPOSER_VERSION} 46 | Target compose runtime: ${versions.TARGET_COMPOSE_RUNTIME_VERSION} 47 | Target kotlin: ${versions.TARGET_KOTLIN_VERSION} 48 | """.trimIndent(), 49 | ) 50 | when (adbState) { 51 | is AdbConnectResult.Failure -> { 52 | Spacer(modifier = Modifier.height(32.dp)) 53 | DefaultText( 54 | text = """ 55 | Connection failed: 56 | ${adbState.errorMessage} 57 | """.trimIndent(), 58 | ) 59 | Spacer(modifier = Modifier.height(32.dp)) 60 | Button( 61 | onClick = { 62 | onConnect() 63 | } 64 | ) { 65 | DefaultText( 66 | text = "Retry" 67 | ) 68 | } 69 | } 70 | AdbConnectResult.Idle -> { 71 | Spacer(modifier = Modifier.height(32.dp)) 72 | DefaultText( 73 | text = """ 74 | Please connect one and only one android device to this PC then click "Connect". 75 | The server runs on port 9801. Or you can run "adb reverse tcp:9801 tcp:9801" 76 | manually then click the "Skip" button. 77 | If you cannot make this port available, set DECOMPOSER_SERVER_PORT to override 78 | the port number. 79 | """.trimIndent() 80 | ) 81 | Spacer(modifier = Modifier.height(32.dp)) 82 | Button(onClick = { onConnect() }) { 83 | DefaultText(text = "Connect") 84 | } 85 | Spacer(modifier = Modifier.height(10.dp)) 86 | Button(onClick = { onSkip() }) { 87 | DefaultText(text = "Skip") 88 | } 89 | } 90 | AdbConnectResult.Success -> { 91 | Spacer(modifier = Modifier.height(32.dp)) 92 | DefaultText( 93 | text = buildString { 94 | append("Connected!") 95 | } 96 | ) 97 | } 98 | AdbConnectResult.Skipped -> { 99 | Spacer(modifier = Modifier.height(32.dp)) 100 | DefaultText( 101 | text = buildString { 102 | append("Skipped!") 103 | } 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | 110 | @Composable 111 | private fun DefaultText(text: String) { 112 | val fontSize = AppSetting.fontSize 113 | Text( 114 | text = text, 115 | textAlign = TextAlign.Center, 116 | fontFamily = Fonts.jetbrainsMono(), 117 | fontSize = fontSize.sp, 118 | fontWeight = FontWeight.Light, 119 | lineHeight = (fontSize * 1.5f).sp 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/FileTreePanel.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.foundation.HorizontalScrollbar 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.VerticalScrollbar 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.horizontalScroll 8 | import androidx.compose.foundation.hoverable 9 | import androidx.compose.foundation.interaction.MutableInteractionSource 10 | import androidx.compose.foundation.layout.Box 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.PaddingValues 13 | import androidx.compose.foundation.layout.Row 14 | import androidx.compose.foundation.layout.fillMaxSize 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.layout.wrapContentHeight 19 | import androidx.compose.foundation.layout.wrapContentSize 20 | import androidx.compose.foundation.lazy.LazyColumn 21 | import androidx.compose.foundation.lazy.rememberLazyListState 22 | import androidx.compose.foundation.rememberScrollState 23 | import androidx.compose.foundation.rememberScrollbarAdapter 24 | import androidx.compose.material.LinearProgressIndicator 25 | import androidx.compose.material.Text 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.Alignment 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.draw.clipToBounds 35 | import androidx.compose.ui.input.pointer.PointerIcon 36 | import androidx.compose.ui.input.pointer.pointerHoverIcon 37 | import androidx.compose.ui.platform.LocalDensity 38 | import androidx.compose.ui.text.font.FontWeight 39 | import androidx.compose.ui.text.style.TextOverflow 40 | import androidx.compose.ui.unit.dp 41 | import androidx.compose.ui.unit.sp 42 | import com.decomposer.runtime.connection.model.ProjectSnapshot 43 | import decomposer.composeapp.generated.resources.Res 44 | import decomposer.composeapp.generated.resources.file 45 | import decomposer.composeapp.generated.resources.folder_close 46 | import decomposer.composeapp.generated.resources.folder_open 47 | import org.jetbrains.compose.resources.painterResource 48 | import java.nio.file.Path 49 | import java.nio.file.Paths 50 | 51 | @Composable 52 | fun FileTreePanel( 53 | modifier: Modifier = Modifier, 54 | projectSnapshot: ProjectSnapshot, 55 | loading: Boolean, 56 | onClickFileEntry: (String) -> Unit 57 | ) { 58 | var fileTree: FilterableTree by remember { mutableStateOf(FilterableTree.EMPTY_TREE) } 59 | 60 | Box(modifier = modifier) { 61 | val verticalScrollState = rememberLazyListState() 62 | val horizontalScrollState = rememberScrollState() 63 | 64 | Column(modifier = Modifier.fillMaxSize()) { 65 | if (loading) { 66 | LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) 67 | } else { 68 | TreeExpander( 69 | onFoldAll = { fileTree.root.setExpandedRecursive(false) }, 70 | onExpandAll = { fileTree.root.setExpandedRecursive(true) } 71 | ) 72 | Box(modifier = Modifier.fillMaxSize().horizontalScroll(horizontalScrollState)) { 73 | LazyColumn( 74 | modifier = Modifier.matchParentSize(), 75 | state = verticalScrollState, 76 | contentPadding = PaddingValues(vertical = 3.dp, horizontal = 8.dp) 77 | ) { 78 | val nodes = fileTree.flattenNodes 79 | items(nodes.size) { 80 | Box(modifier = Modifier.animateItem()) { 81 | nodes[it].TreeNodeIndented() 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | VerticalScrollbar( 90 | modifier = Modifier.align(Alignment.CenterEnd), 91 | adapter = rememberScrollbarAdapter(verticalScrollState) 92 | ) 93 | 94 | HorizontalScrollbar( 95 | modifier = Modifier.align(Alignment.BottomCenter), 96 | adapter = rememberScrollbarAdapter(horizontalScrollState) 97 | ) 98 | } 99 | 100 | LaunchedEffect(projectSnapshot) { 101 | fileTree = projectSnapshot.buildFileTree { 102 | projectSnapshot.findMatching(it)?.let(onClickFileEntry) 103 | } 104 | } 105 | } 106 | 107 | class FileTreeNode( 108 | override val name: String, 109 | override val children: List, 110 | override val level: Int, 111 | override val tags: Set = emptySet(), 112 | val prefix: String, 113 | val onClickFileEntry: (String) -> Unit 114 | ) : BaseTreeNode() { 115 | private val isFile = !expandable 116 | private val isFolder = expandable 117 | 118 | @Composable 119 | override fun TreeNode() { 120 | Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) { 121 | val interactionSource = remember { MutableInteractionSource() } 122 | val fontSize = AppSetting.fontSize 123 | FileIcon(Modifier.align(Alignment.CenterVertically)) 124 | Text( 125 | text = name, 126 | modifier = Modifier 127 | .align(Alignment.CenterVertically) 128 | .clipToBounds() 129 | .run { 130 | if (isFile) { 131 | this.hoverable(interactionSource) 132 | .pointerHoverIcon(PointerIcon.Hand) 133 | .clickable { 134 | onClickFileEntry(Paths.get(prefix, name).toString()) 135 | } 136 | } else { 137 | this 138 | } 139 | }, 140 | softWrap = true, 141 | overflow = TextOverflow.Ellipsis, 142 | maxLines = 1, 143 | fontFamily = Fonts.jetbrainsMono(), 144 | fontSize = fontSize.sp, 145 | fontWeight = FontWeight.Light, 146 | lineHeight = (fontSize * 1.5).sp 147 | ) 148 | } 149 | } 150 | 151 | override fun compareTo(other: TreeNode): Int { 152 | return when { 153 | other !is FileTreeNode -> -1 154 | isFolder.compareTo(other.isFolder) != 0 -> { 155 | isFolder.compareTo(other.isFolder) 156 | } 157 | else -> name.compareTo(other.name) 158 | } 159 | } 160 | 161 | @Composable 162 | private fun FileIcon(modifier: Modifier) { 163 | val size = with(LocalDensity.current) { 164 | (LocalFontSize.current * 1.25).sp.toDp() 165 | } 166 | Box( 167 | modifier = modifier 168 | .wrapContentSize() 169 | .padding(horizontal = 4.dp) 170 | .run { 171 | if (isFolder) { 172 | this.clickable { expanded = !expanded } 173 | } else { 174 | this 175 | } 176 | } 177 | ) { 178 | when { 179 | isFolder && expanded -> { 180 | val interactionSource = remember { MutableInteractionSource() } 181 | Image( 182 | painter = painterResource(Res.drawable.folder_open), 183 | contentDescription = "Fold $name", 184 | modifier = Modifier 185 | .size(size) 186 | .hoverable(interactionSource) 187 | .pointerHoverIcon(PointerIcon.Hand), 188 | ) 189 | } 190 | isFolder && !expanded -> { 191 | val interactionSource = remember { MutableInteractionSource() } 192 | Image( 193 | painter = painterResource(Res.drawable.folder_close), 194 | contentDescription = "Unfold $name", 195 | modifier = Modifier 196 | .size(size) 197 | .hoverable(interactionSource) 198 | .pointerHoverIcon(PointerIcon.Hand), 199 | ) 200 | } 201 | else -> { 202 | Image( 203 | painter = painterResource(Res.drawable.file), 204 | contentDescription = name, 205 | modifier = Modifier.size(size), 206 | ) 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | @Suppress("UNCHECKED_CAST") 214 | internal fun ProjectSnapshot.buildFileTree( 215 | onClickFileEntry: (String) -> Unit 216 | ): FilterableTree { 217 | val paths = this.fileTree 218 | if (paths.isEmpty()) { 219 | return FilterableTree.EMPTY_TREE 220 | } 221 | 222 | val normalizedPaths = paths.map { Paths.get(it).normalize() } 223 | val commonPrefix = findCommonPrefix(normalizedPaths) 224 | val trimmedPaths = normalizedPaths.map { commonPrefix.relativize(it) } 225 | 226 | val rootMap = mutableMapOf() 227 | trimmedPaths.forEach { trimmedPath -> 228 | val parts = trimmedPath.iterator().asSequence().map { it.toString() }.toList() 229 | var currentMap = rootMap 230 | for (part in parts) { 231 | currentMap = currentMap.computeIfAbsent(part) { 232 | mutableMapOf() 233 | } as MutableMap 234 | } 235 | } 236 | 237 | fun createNode( 238 | prefix: String, 239 | name: String, 240 | map: Map, 241 | level: Int 242 | ): FileTreeNode { 243 | val children = map.map { (childName, childMap) -> 244 | createNode( 245 | prefix = Paths.get(prefix, name).toString(), 246 | name = childName, 247 | map = childMap as Map, 248 | level = level + 1 249 | ) 250 | } 251 | return FileTreeNode( 252 | name = name, 253 | children = children, 254 | level = level, 255 | prefix = prefix, 256 | onClickFileEntry = onClickFileEntry 257 | ) 258 | } 259 | 260 | val rootChildren = rootMap.map { (name, map) -> 261 | createNode(commonPrefix.toString(), name, map as Map, level = 1) 262 | } 263 | val root = FileTreeNode( 264 | name = commonPrefix.toString(), 265 | children = rootChildren, 266 | level = 0, 267 | prefix = "", 268 | onClickFileEntry = onClickFileEntry 269 | ) 270 | return FilterableTree(root) 271 | } 272 | 273 | private fun ProjectSnapshot.findMatching(other: String): String? { 274 | return this.fileTree.firstOrNull { 275 | val normalized = Paths.get(it).normalize().toString() 276 | normalized == other 277 | } 278 | } 279 | 280 | private fun findCommonPrefix(paths: List): Path { 281 | if (paths.isEmpty()) return Paths.get("") 282 | var prefix = paths.first() 283 | for (path in paths.drop(1)) { 284 | while (!path.startsWith(prefix)) { 285 | prefix = prefix.parent ?: Paths.get("") 286 | } 287 | } 288 | return prefix 289 | } 290 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/FilterableTree.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.wrapContentHeight 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.Stable 9 | import androidx.compose.runtime.derivedStateOf 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.Dp 15 | import androidx.compose.ui.unit.dp 16 | import kotlin.reflect.KClass 17 | 18 | class FilterableTree(val root: TreeNode) { 19 | 20 | init { setParents(root) } 21 | 22 | private val subtreeCache = mutableMapOf() 23 | 24 | fun subtree(filter: Filter): FilterableTree { 25 | val cachedTree = subtreeCache[filter] 26 | if (cachedTree != null) { 27 | return cachedTree 28 | } 29 | 30 | val filteredNodes = filterNode(root, filter, 0) 31 | 32 | return when { 33 | filteredNodes.isEmpty() -> EMPTY_TREE 34 | filteredNodes.first().level == 0 -> { 35 | FilterableTree(filteredNodes.first()).also { 36 | subtreeCache[filter] = it 37 | } 38 | } 39 | else -> { 40 | val newRoot = SubtreeNode( 41 | wrapped = root, 42 | children = filteredNodes, 43 | level = 0 44 | ) 45 | FilterableTree(newRoot).also { 46 | subtreeCache[filter] = it 47 | } 48 | } 49 | } 50 | } 51 | 52 | private fun filterNode(node: TreeNode, filter: Filter, level: Int): List { 53 | val matches = matches(filter, node) 54 | val filteredChildren = if (matches || node === root) { 55 | node.children.flatMap { filterNode(it, filter, level + 1) } 56 | } else { 57 | node.children.flatMap { filterNode(it, filter, level) } 58 | } 59 | 60 | return if (matches) { 61 | listOf( 62 | SubtreeNode( 63 | wrapped = node, 64 | children = filteredChildren, 65 | level = level 66 | ) 67 | ) 68 | } else filteredChildren 69 | } 70 | 71 | private fun matches(filter: Filter, node: TreeNode): Boolean { 72 | return filter.predicate(node) 73 | } 74 | 75 | private fun setParents(parent: TreeNode) { 76 | parent.children.forEach { child -> 77 | child.parent = parent 78 | setParents(child) 79 | } 80 | } 81 | 82 | val flattenNodes: List 83 | get() = root.flattenedChildren 84 | 85 | class SubtreeNode( 86 | private val wrapped: TreeNode, 87 | override val children: List, 88 | override val level: Int 89 | ): TreeNode by wrapped { 90 | override val flattenedChildren: List by derivedStateOf { 91 | flattenChildren() 92 | } 93 | 94 | @Composable 95 | override fun TreeNodeIndented(keepLevel: Boolean) { 96 | val padding = levelWidth * if (keepLevel) { 97 | wrapped.level 98 | } else { 99 | level 100 | } 101 | Box(modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(start = padding)) { 102 | wrapped.TreeNode() 103 | } 104 | } 105 | 106 | override fun compareTo(other: TreeNode): Int { 107 | return if (other is SubtreeNode) { 108 | wrapped.compareTo(other.wrapped) 109 | } else 0 110 | } 111 | } 112 | 113 | companion object { 114 | object EmptyNode : BaseTreeNode() { 115 | override val name = "Empty" 116 | override var parent: TreeNode? = null 117 | override val children: List = emptyList() 118 | override var expanded = false 119 | override val tags = emptySet() 120 | override val level = 0 121 | 122 | @Composable 123 | override fun TreeNode() { 124 | DefaultPanelText(text = name) 125 | } 126 | 127 | override fun compareTo(other: TreeNode): Int = 0 128 | } 129 | 130 | val EMPTY_TREE = FilterableTree(EmptyNode) 131 | } 132 | } 133 | 134 | data class Filter( 135 | val predicate: (TreeNode) -> Boolean 136 | ) 137 | 138 | @Stable 139 | interface TreeNode : Comparable { 140 | val name: String 141 | var parent: TreeNode? 142 | val children: List 143 | val flattenedChildren: List 144 | val expanded: Boolean 145 | val tags: Set 146 | val expandable: Boolean 147 | val level: Int 148 | val excludes: Set> 149 | val levelWidth: Dp 150 | fun hasTag(clazz: KClass<*>): Boolean 151 | @Composable 152 | fun TreeNode() 153 | @Composable 154 | fun TreeNodeIndented(keepLevel: Boolean = true) 155 | fun setExpandedRecursive(expanded: Boolean) 156 | fun addExcludesRecursive(excludes: Set>) 157 | fun removeExcludesRecursive(excludes: Set>) 158 | } 159 | 160 | fun TreeNode.flattenChildren(): List { 161 | val result = mutableListOf() 162 | val excluded = tags.any { excludes.contains(it::class) } 163 | if (!excluded) { 164 | result.add(this) 165 | if (expanded) { 166 | val sortedChildren = this.children.sortedBy { it } 167 | sortedChildren.forEach { 168 | result.addAll(it.flattenedChildren) 169 | } 170 | } 171 | } 172 | return result 173 | } 174 | 175 | abstract class BaseTreeNode : TreeNode { 176 | override var excludes: Set> by mutableStateOf(emptySet()) 177 | override val levelWidth: Dp 178 | get() = AppSetting.fontSize.dp 179 | override var parent: TreeNode? = null 180 | 181 | override val expandable: Boolean 182 | get() = children.isNotEmpty() 183 | 184 | override fun hasTag(clazz: KClass<*>): Boolean { 185 | return tags.any { it::class == clazz } 186 | } 187 | 188 | override val flattenedChildren: List by derivedStateOf { 189 | flattenChildren() 190 | } 191 | 192 | override var expanded: Boolean by mutableStateOf(false) 193 | 194 | override fun setExpandedRecursive(expanded: Boolean) { 195 | this.expanded = expanded 196 | this.children.forEach { 197 | it.setExpandedRecursive(expanded) 198 | } 199 | } 200 | 201 | override fun addExcludesRecursive(excludes: Set>) { 202 | this.excludes += excludes 203 | this.children.forEach { 204 | it.addExcludesRecursive(excludes) 205 | } 206 | } 207 | 208 | override fun removeExcludesRecursive(excludes: Set>) { 209 | this.excludes -= excludes 210 | this.children.forEach { 211 | it.removeExcludesRecursive(excludes) 212 | } 213 | } 214 | 215 | @Composable 216 | override fun TreeNodeIndented(keepLevel: Boolean) { 217 | Box( 218 | modifier = Modifier.fillMaxWidth() 219 | .wrapContentHeight() 220 | .padding(start = levelWidth * level) 221 | ) { 222 | TreeNode() 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/Fonts.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.text.font.FontFamily 5 | import androidx.compose.ui.text.font.FontStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | import decomposer.composeapp.generated.resources.Res 8 | import decomposer.composeapp.generated.resources.jetbrainsmono_bold 9 | import decomposer.composeapp.generated.resources.jetbrainsmono_bold_italic 10 | import decomposer.composeapp.generated.resources.jetbrainsmono_extrabold 11 | import decomposer.composeapp.generated.resources.jetbrainsmono_extrabold_italic 12 | import decomposer.composeapp.generated.resources.jetbrainsmono_italic 13 | import decomposer.composeapp.generated.resources.jetbrainsmono_medium 14 | import decomposer.composeapp.generated.resources.jetbrainsmono_medium_italic 15 | import decomposer.composeapp.generated.resources.jetbrainsmono_regular 16 | import org.jetbrains.compose.resources.Font 17 | 18 | object Fonts { 19 | @Composable 20 | fun jetbrainsMono() = FontFamily( 21 | Font( 22 | Res.font.jetbrainsmono_regular, 23 | FontWeight.Normal, 24 | FontStyle.Normal 25 | ), 26 | Font( 27 | Res.font.jetbrainsmono_italic, 28 | FontWeight.Normal, 29 | FontStyle.Italic 30 | ), 31 | Font( 32 | Res.font.jetbrainsmono_bold, 33 | FontWeight.Bold, 34 | FontStyle.Normal 35 | ), 36 | Font( 37 | Res.font.jetbrainsmono_bold_italic, 38 | FontWeight.Bold, 39 | FontStyle.Italic 40 | ), 41 | 42 | Font( 43 | Res.font.jetbrainsmono_extrabold, 44 | FontWeight.ExtraBold, 45 | FontStyle.Normal 46 | ), 47 | Font( 48 | Res.font.jetbrainsmono_extrabold_italic, 49 | FontWeight.ExtraBold, 50 | FontStyle.Italic 51 | ), 52 | Font( 53 | Res.font.jetbrainsmono_medium, 54 | FontWeight.Medium, 55 | FontStyle.Normal 56 | ), 57 | Font( 58 | Res.font.jetbrainsmono_medium_italic, 59 | FontWeight.Medium, 60 | FontStyle.Italic 61 | ) 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/IrPanel.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.foundation.HorizontalScrollbar 4 | import androidx.compose.foundation.VerticalScrollbar 5 | import androidx.compose.foundation.horizontalScroll 6 | import androidx.compose.foundation.hoverable 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.fillMaxHeight 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.wrapContentHeight 16 | import androidx.compose.foundation.layout.wrapContentSize 17 | import androidx.compose.foundation.layout.wrapContentWidth 18 | import androidx.compose.foundation.rememberScrollState 19 | import androidx.compose.foundation.rememberScrollbarAdapter 20 | import androidx.compose.foundation.selection.toggleable 21 | import androidx.compose.foundation.text.selection.SelectionContainer 22 | import androidx.compose.foundation.verticalScroll 23 | import androidx.compose.material.Checkbox 24 | import androidx.compose.material.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.LaunchedEffect 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableStateOf 29 | import androidx.compose.runtime.remember 30 | import androidx.compose.runtime.setValue 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.draw.scale 34 | import androidx.compose.ui.graphics.Color 35 | import androidx.compose.ui.input.pointer.PointerIcon 36 | import androidx.compose.ui.input.pointer.pointerHoverIcon 37 | import androidx.compose.ui.semantics.Role 38 | import androidx.compose.ui.text.AnnotatedString 39 | import androidx.compose.ui.text.TextLayoutResult 40 | import androidx.compose.ui.text.font.FontWeight 41 | import androidx.compose.ui.text.style.TextAlign 42 | import androidx.compose.ui.text.style.TextOverflow 43 | import androidx.compose.ui.unit.dp 44 | import androidx.compose.ui.unit.sp 45 | import com.decomposer.ir.IrProcessor 46 | import com.decomposer.runtime.connection.model.ProjectSnapshot 47 | import com.decomposer.runtime.connection.model.VirtualFileIr 48 | import com.decomposer.server.Session 49 | import kotlinx.serialization.json.Json 50 | import java.nio.file.Paths 51 | 52 | @Composable 53 | fun IrPanel( 54 | modifier: Modifier = Modifier, 55 | session: Session, 56 | irProcessor: IrProcessor, 57 | filePath: String?, 58 | projectSnapshot: ProjectSnapshot, 59 | highlight: Pair?, 60 | onShowPopup: (@Composable () -> Unit) -> Unit, 61 | onShowWindow: (Pair Unit>) -> Unit 62 | ) { 63 | var compose by remember { mutableStateOf(true) } 64 | var kotlinLike by remember { mutableStateOf(true) } 65 | var wrapCodeBlock by remember { mutableStateOf(true) } 66 | var renderOperator by remember { mutableStateOf(true) } 67 | var kotlinLikeIr by remember { mutableStateOf(null) } 68 | var standardIr by remember { mutableStateOf(null) } 69 | 70 | Box( 71 | modifier = modifier 72 | ) { 73 | val kotlinLikeIrDump = kotlinLikeIr 74 | val standardIrDump = standardIr 75 | if (kotlinLikeIrDump == null || standardIrDump == null) { 76 | DefaultPanelText( 77 | text = """ 78 | Select a file to view ir! 79 | """.trimIndent(), 80 | modifier = Modifier.align(Alignment.Center) 81 | ) 82 | } else { 83 | Column(modifier = Modifier.fillMaxSize()) { 84 | Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { 85 | ComposeToggle( 86 | text = "Compose", 87 | checked = compose, 88 | onCheckedChanged = { 89 | compose = !compose 90 | } 91 | ) 92 | ComposeToggle( 93 | text = "Kotlin like", 94 | checked = kotlinLike, 95 | onCheckedChanged = { 96 | kotlinLike = !kotlinLike 97 | } 98 | ) 99 | ComposeToggle( 100 | text = "Wrap code block", 101 | checked = wrapCodeBlock, 102 | onCheckedChanged = { 103 | wrapCodeBlock = !wrapCodeBlock 104 | } 105 | ) 106 | ComposeToggle( 107 | text = "Render operators", 108 | checked = renderOperator, 109 | onCheckedChanged = { 110 | renderOperator = !renderOperator 111 | } 112 | ) 113 | } 114 | CodeContent(filePath, kotlinLikeIrDump, standardIrDump, kotlinLike, highlight) 115 | } 116 | } 117 | } 118 | 119 | val theme = LocalTheme.current 120 | 121 | LaunchedEffect(filePath, compose, session.sessionId, highlight, wrapCodeBlock, renderOperator) { 122 | if (filePath != null) { 123 | val packageName = projectSnapshot.packagesByPath[filePath] 124 | val virtualFileIr = session.getVirtualFileIr(filePath) 125 | if (!virtualFileIr.isEmpty) { 126 | irProcessor.processVirtualFileIr(virtualFileIr) 127 | val kotlinFile = if (compose) { 128 | irProcessor.composedFile(filePath) 129 | } else { 130 | irProcessor.originalFile(filePath) 131 | } 132 | val irVisualBuilder = IrVisualBuilder( 133 | kotlinFile = kotlinFile, 134 | packageName = packageName, 135 | wrapCodeBlock = wrapCodeBlock, 136 | renderOperator = renderOperator, 137 | theme = theme, 138 | highlights = highlight?.let { listOf(it) } ?: emptyList() 139 | ) { 140 | onShowWindow( 141 | "Binary format" to @Composable { IrDescription(it.description) } 142 | ) 143 | } 144 | kotlinLikeIr = irVisualBuilder.visualize().annotatedString 145 | standardIr = kotlinFile.standardIrDump 146 | } else { 147 | kotlinLikeIr = null 148 | standardIr = null 149 | } 150 | } 151 | } 152 | } 153 | 154 | @Composable 155 | private fun IrDescription(text: String) { 156 | val fontSize = AppSetting.fontSize 157 | Text( 158 | modifier = Modifier.fillMaxSize(), 159 | text = text, 160 | textAlign = TextAlign.Start, 161 | fontFamily = Fonts.jetbrainsMono(), 162 | fontSize = fontSize.sp, 163 | fontWeight = FontWeight.Light, 164 | lineHeight = (fontSize * 1.5).sp, 165 | overflow = TextOverflow.Ellipsis, 166 | maxLines = Int.MAX_VALUE 167 | ) 168 | } 169 | 170 | private val VirtualFileIr.isEmpty: Boolean 171 | get() { 172 | return this.composedIrFile.isEmpty() && 173 | this.composedTopLevelIrClasses.isEmpty() && 174 | this.originalIrFile.isEmpty() && 175 | this.originalTopLevelIrClasses.isEmpty() 176 | } 177 | 178 | @Composable 179 | fun CodeContent( 180 | filePath: String?, 181 | kotlinLikeIr: AnnotatedString, 182 | standardIr: String, 183 | kotlinLike: Boolean, 184 | highlight: Pair? 185 | ) { 186 | Column(modifier = Modifier.fillMaxSize()) { 187 | filePath?.let { 188 | DefaultPanelText( 189 | text = Paths.get(it).fileName.toString(), 190 | modifier = Modifier.fillMaxWidth() 191 | ) 192 | } 193 | 194 | Box(modifier = Modifier.fillMaxSize()) { 195 | val verticalScrollState = rememberScrollState() 196 | val horizontalScrollState = rememberScrollState() 197 | 198 | Row( 199 | modifier = Modifier 200 | .fillMaxSize() 201 | .verticalScroll(verticalScrollState) 202 | .horizontalScroll(horizontalScrollState), 203 | ) { 204 | LineNumbers( 205 | length = if (kotlinLike) { 206 | kotlinLikeIr.lines().size 207 | } else { 208 | standardIr.lines().size 209 | } 210 | ) 211 | SelectionContainer { 212 | val fontSize = AppSetting.fontSize 213 | if (kotlinLike) { 214 | var textLayoutResult: TextLayoutResult? by remember { 215 | mutableStateOf(null) 216 | } 217 | Text( 218 | modifier = Modifier 219 | .fillMaxSize() 220 | .padding(horizontal = 8.dp), 221 | text = kotlinLikeIr, 222 | fontFamily = Fonts.jetbrainsMono(), 223 | fontSize = fontSize.sp, 224 | fontWeight = FontWeight.Light, 225 | lineHeight = (fontSize * 1.5).sp, 226 | onTextLayout = { textLayoutResult = it } 227 | ) 228 | 229 | LaunchedEffect(highlight, kotlinLikeIr, textLayoutResult) { 230 | val layoutResult = textLayoutResult ?: return@LaunchedEffect 231 | highlight?.let { highlight -> 232 | val annotation = kotlinLikeIr.getStringAnnotations( 233 | tag = IrVisualBuilder.TAG_SOURCE_LOCATION, 234 | start = 0, 235 | end = kotlinLikeIr.text.length 236 | ).firstOrNull { 237 | val location = Json.decodeFromString(it.item) 238 | highlight.first == location.sourceStartOffset 239 | && highlight.second == location.sourceEndOffset 240 | } 241 | 242 | if (annotation != null) { 243 | val top = layoutResult.getBoundingBox(annotation.start).top 244 | verticalScrollState.animateScrollTo(top.toInt()) 245 | } 246 | } 247 | } 248 | } else { 249 | Text( 250 | modifier = Modifier 251 | .fillMaxSize() 252 | .padding(horizontal = 8.dp), 253 | text = standardIr, 254 | fontFamily = Fonts.jetbrainsMono(), 255 | fontSize = fontSize.sp, 256 | fontWeight = FontWeight.Light, 257 | lineHeight = (fontSize * 1.5).sp 258 | ) 259 | } 260 | } 261 | } 262 | 263 | VerticalScrollbar( 264 | modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), 265 | adapter = rememberScrollbarAdapter(verticalScrollState) 266 | ) 267 | 268 | HorizontalScrollbar( 269 | modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth(), 270 | adapter = rememberScrollbarAdapter(horizontalScrollState) 271 | ) 272 | } 273 | } 274 | } 275 | 276 | @Composable 277 | fun LineNumbers( 278 | length: Int 279 | ) { 280 | Column( 281 | modifier = Modifier 282 | .wrapContentWidth() 283 | .fillMaxHeight() 284 | .padding(end = 6.dp), 285 | horizontalAlignment = Alignment.End 286 | ) { 287 | val fontSize = AppSetting.fontSize 288 | for (i in 1 .. length) { 289 | Text( 290 | text = "$i", 291 | fontFamily = Fonts.jetbrainsMono(), 292 | fontSize = fontSize.sp, 293 | fontWeight = FontWeight.Thin, 294 | lineHeight = (fontSize * 1.5).sp, 295 | color = Color.Gray 296 | ) 297 | } 298 | } 299 | } 300 | 301 | @Composable 302 | fun ComposeToggle( 303 | text: String, 304 | checked: Boolean, 305 | onCheckedChanged: (Boolean) -> Unit 306 | ) { 307 | val scale = AppSetting.fontSize.toFloat() / 14.0f 308 | val interactionSource = remember { MutableInteractionSource() } 309 | Row( 310 | Modifier 311 | .wrapContentSize() 312 | .toggleable( 313 | value = checked, 314 | onValueChange = { onCheckedChanged(!checked) }, 315 | role = Role.Checkbox 316 | ) 317 | .hoverable(interactionSource) 318 | .pointerHoverIcon(PointerIcon.Hand) 319 | .padding(horizontal = 10.dp, vertical = 4.dp), 320 | verticalAlignment = Alignment.CenterVertically 321 | ) { 322 | Checkbox( 323 | checked = checked, 324 | onCheckedChange = null, 325 | modifier = Modifier.scale(scale).padding(4.dp) 326 | ) 327 | DefaultPanelText(text = text) 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/MainApp.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.DisposableEffect 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.collectAsState 10 | import androidx.compose.runtime.derivedStateOf 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.rememberCoroutineScope 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import com.decomposer.runtime.connection.ConnectionContract 19 | import com.decomposer.server.AdbConnectResult 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.launch 22 | 23 | @Composable 24 | fun MainApp() { 25 | val serverPort = System.getenv( 26 | "DECOMPOSER_SERVER_PORT" 27 | )?.toIntOrNull() ?: ConnectionContract.DEFAULT_SERVER_PORT 28 | val connectionState = rememberConnectionState(serverPort) 29 | val adbConnectState by connectionState.adbConnectState.collectAsState() 30 | val sessionState by connectionState.sessionState.collectAsState() 31 | val coroutineScope = rememberCoroutineScope() 32 | 33 | Surface(modifier = Modifier.fillMaxSize()) { 34 | val contentState = remember { 35 | derivedStateOf { 36 | when (adbConnectState) { 37 | AdbConnectResult.Success, 38 | AdbConnectResult.Skipped -> PanelContentState.Editor 39 | else -> PanelContentState.DeviceDiscovery 40 | } 41 | } 42 | } 43 | AnimatedContent(contentState) { 44 | when (contentState.value) { 45 | PanelContentState.DeviceDiscovery -> { 46 | DeviceDiscovery( 47 | modifier = Modifier.fillMaxSize(), 48 | adbState = adbConnectState, 49 | versions = Versions, 50 | onConnect = { 51 | coroutineScope.launch { 52 | if (adbConnectState != AdbConnectResult.Success) { 53 | connectionState.adbConnect() 54 | } 55 | } 56 | }, 57 | onSkip = { 58 | if (adbConnectState != AdbConnectResult.Success) { 59 | connectionState.skipConnect() 60 | } 61 | } 62 | ) 63 | } 64 | PanelContentState.Editor -> { 65 | Panels( 66 | modifier = Modifier.fillMaxSize(), 67 | sessionState = sessionState 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | 74 | DetectAdbDisconnect(connectionState) 75 | 76 | DisposableEffect(connectionState) { 77 | connectionState.serverConnect() 78 | onDispose { 79 | connectionState.serverDisconnect() 80 | } 81 | } 82 | } 83 | 84 | @Composable 85 | fun DetectAdbDisconnect(connectionState: ConnectionState) { 86 | val adbConnectState by connectionState.adbConnectState.collectAsState() 87 | LaunchedEffect(adbConnectState) { 88 | if (adbConnectState == AdbConnectResult.Success) { 89 | while (true) { 90 | delay(1000) 91 | connectionState.adbConnect() 92 | } 93 | } 94 | } 95 | } 96 | 97 | private enum class PanelContentState { 98 | DeviceDiscovery, Editor 99 | } 100 | 101 | object AppSetting { 102 | var darkTheme: Boolean by mutableStateOf(true) 103 | var fontSize: Int by mutableIntStateOf(14) 104 | } 105 | 106 | object Versions { 107 | const val DECOMPOSER_VERSION = "0.1.0" 108 | const val TARGET_COMPOSE_RUNTIME_VERSION = "1.7.1" 109 | const val TARGET_KOTLIN_VERSION = "2.1.0" 110 | } 111 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.material.Colors 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.Immutable 9 | import androidx.compose.runtime.compositionLocalOf 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.SpanStyle 12 | import androidx.compose.ui.text.font.FontStyle 13 | 14 | @Immutable 15 | data class Theme( 16 | val materialColors: Colors, 17 | val code: CodeStyle 18 | ) { 19 | @Immutable 20 | data class CodeStyle( 21 | val simple: SpanStyle, 22 | val value: SpanStyle, 23 | val keyword: SpanStyle, 24 | val punctuation: SpanStyle, 25 | val annotation: SpanStyle, 26 | val comment: SpanStyle, 27 | val function: SpanStyle, 28 | val highlight: SpanStyle 29 | ) 30 | 31 | companion object { 32 | val dark = Theme( 33 | materialColors = darkColors( 34 | background = Color(0xFF2B2B2B), 35 | surface = Color(0xFF3C3F41) 36 | ), 37 | code = CodeStyle( 38 | simple = SpanStyle(Color(0xFFC9D7E6)), 39 | value = SpanStyle(Color(0xFF6897BB)), 40 | keyword = SpanStyle(Color(0xFFCC7832)), 41 | punctuation = SpanStyle(Color(0xFFA1C17E)), 42 | annotation = SpanStyle(Color(0xFFBBB529)), 43 | comment = SpanStyle(Color(0xFF808080)), 44 | function = SpanStyle(Color(0xFFC9D7E6), fontStyle = FontStyle.Italic), 45 | highlight = SpanStyle(background = Color(0xAA569CD6)) 46 | ) 47 | ) 48 | 49 | val light = Theme( 50 | materialColors = lightColors( 51 | background = Color(0xFFF5F5F5), 52 | surface = Color(0xFFFFFFFF) 53 | ), 54 | code = CodeStyle( 55 | simple = SpanStyle(Color(0xFF000000)), 56 | value = SpanStyle(Color(0xFF4A86E8)), 57 | keyword = SpanStyle(Color(0xFF000080)), 58 | punctuation = SpanStyle(Color(0xFFA1A1A1)), 59 | annotation = SpanStyle(Color(0xFFBBB529)), 60 | comment = SpanStyle(Color(0xFF808080)), 61 | function = SpanStyle(Color(0xFF000000), fontStyle = FontStyle.Italic), 62 | highlight = SpanStyle(background = Color(0xAA569CD6)) 63 | ) 64 | ) 65 | } 66 | } 67 | 68 | @Composable 69 | fun DecomposerTheme(content: @Composable () -> Unit) { 70 | val theme = LocalTheme.current 71 | MaterialTheme(colors = theme.materialColors) { 72 | content() 73 | } 74 | } 75 | 76 | val LocalTheme = compositionLocalOf { Theme.dark } 77 | val LocalFontSize = compositionLocalOf { 16 } 78 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/com/decomposer/ui/ToolBar.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import androidx.compose.foundation.hoverable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 6 | import androidx.compose.foundation.layout.FlowRow 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.layout.wrapContentSize 11 | import androidx.compose.foundation.layout.wrapContentWidth 12 | import androidx.compose.foundation.selection.toggleable 13 | import androidx.compose.material.Checkbox 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Slider 16 | import androidx.compose.material.SliderDefaults 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableFloatStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.draw.scale 25 | import androidx.compose.ui.input.pointer.PointerIcon 26 | import androidx.compose.ui.input.pointer.pointerHoverIcon 27 | import androidx.compose.ui.platform.LocalDensity 28 | import androidx.compose.ui.semantics.Role 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | 32 | @Composable 33 | fun ToolBar( 34 | modifier: Modifier, 35 | panelsState: PanelsState 36 | ) { 37 | Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { 38 | FontSizeChooser() 39 | ToolBarCheckBox( 40 | checked = panelsState.fileTreeVisible, 41 | text = "Show file tree", 42 | onCheckedChanged = { 43 | panelsState.fileTreeVisible = it 44 | } 45 | ) 46 | ToolBarCheckBox( 47 | checked = panelsState.irViewerVisible, 48 | text = "Show ir tree", 49 | onCheckedChanged = { 50 | panelsState.irViewerVisible = it 51 | } 52 | ) 53 | ToolBarCheckBox( 54 | checked = panelsState.compositionViewerVisible, 55 | text = "Show composition", 56 | onCheckedChanged = { 57 | panelsState.compositionViewerVisible = it 58 | } 59 | ) 60 | ToolBarCheckBox( 61 | checked = AppSetting.darkTheme, 62 | text = "Dark theme", 63 | onCheckedChanged = { 64 | AppSetting.darkTheme = it 65 | } 66 | ) 67 | } 68 | } 69 | 70 | @Composable 71 | fun FontSizeChooser() { 72 | val size = with(LocalDensity.current) { 73 | (LocalFontSize.current * 1.25).sp.toDp() 74 | } 75 | Row( 76 | modifier = Modifier.padding(horizontal = 10.dp), 77 | verticalAlignment = Alignment.CenterVertically 78 | ) { 79 | var sliderValue by remember { mutableFloatStateOf(AppSetting.fontSize.toFloat()) } 80 | 81 | Slider( 82 | modifier = Modifier.width(size * 10), 83 | value = sliderValue, 84 | onValueChange = { sliderValue = it }, 85 | onValueChangeFinished = { AppSetting.fontSize = sliderValue.toInt() }, 86 | valueRange = 10f..40f, 87 | steps = 31, 88 | colors = SliderDefaults.colors( 89 | thumbColor = MaterialTheme.colors.secondary, 90 | activeTrackColor = MaterialTheme.colors.secondary 91 | ) 92 | ) 93 | DefaultPanelText(text = "${AppSetting.fontSize}sp") 94 | } 95 | } 96 | 97 | @Composable 98 | fun ToolBarCheckBox( 99 | checked: Boolean, 100 | text: String, 101 | onCheckedChanged: (Boolean) -> Unit 102 | ) { 103 | val scale = AppSetting.fontSize.toFloat() / 24.0f 104 | val interactionSource = remember { MutableInteractionSource() } 105 | Row( 106 | Modifier 107 | .wrapContentWidth() 108 | .toggleable( 109 | value = checked, 110 | onValueChange = { onCheckedChanged(!checked) }, 111 | role = Role.Checkbox 112 | ) 113 | .hoverable(interactionSource) 114 | .pointerHoverIcon(PointerIcon.Hand), 115 | verticalAlignment = Alignment.CenterVertically 116 | ) { 117 | Checkbox( 118 | checked = checked, 119 | onCheckedChange = null, 120 | modifier = Modifier.scale(scale).padding(4.dp) 121 | ) 122 | DefaultPanelText(text = text) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/com/decomposer/ui/FileTreePanelTest.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.ui 2 | 3 | import com.decomposer.runtime.connection.model.ProjectSnapshot 4 | import org.junit.Test 5 | 6 | class FileTreePanelTest { 7 | @Test 8 | fun testFileTreeMapping() { 9 | val projectSnapshot = ProjectSnapshot( 10 | fileTree = setOf( 11 | "/home/jim/com/example/A.kt", 12 | "/home/jim/com/example/B.kt", 13 | "/home/jim/android/C.kt", 14 | "/home/jim/android/D.kt" 15 | ), 16 | packagesByPath = emptyMap() 17 | ) 18 | val fileTree = projectSnapshot.buildFileTree { } 19 | assert(fileTree.root.level == 0) 20 | assert(fileTree.root.name == "/home/jim") 21 | assert(fileTree.root.children.size == 2) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/com/decomposer/main.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer 2 | 3 | import androidx.compose.runtime.CompositionLocalProvider 4 | import androidx.compose.ui.window.Window 5 | import androidx.compose.ui.window.WindowPlacement 6 | import androidx.compose.ui.window.WindowState 7 | import androidx.compose.ui.window.application 8 | import com.decomposer.ui.AppSetting 9 | import com.decomposer.ui.DecomposerTheme 10 | import com.decomposer.ui.LocalFontSize 11 | import com.decomposer.ui.LocalTheme 12 | import com.decomposer.ui.MainApp 13 | import com.decomposer.ui.Theme 14 | import decomposer.composeapp.generated.resources.Res 15 | import decomposer.composeapp.generated.resources.ic_launcher 16 | import org.jetbrains.compose.resources.painterResource 17 | 18 | fun main() = application { 19 | CompositionLocalProvider( 20 | LocalTheme provides if (AppSetting.darkTheme) Theme.dark else Theme.light, 21 | LocalFontSize provides AppSetting.fontSize 22 | ) { 23 | Window( 24 | onCloseRequest = ::exitApplication, 25 | title = "Decomposer", 26 | state = WindowState(placement = WindowPlacement.Maximized), 27 | icon = painterResource(Res.drawable.ic_launcher) 28 | ) { 29 | DecomposerTheme { 30 | MainApp() 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Kotlin 2 | kotlin.code.style=official 3 | kotlin.daemon.jvmargs=-Xmx4096M 4 | 5 | #Gradle 6 | org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 7 | 8 | #Android 9 | android.nonTransitiveRClass=true 10 | android.useAndroidX=true 11 | 12 | #Publishing 13 | GROUP=io.github.composexy-decomposer 14 | VERSION_NAME=0.1.0-alpha1 15 | POM_LICENCE_NAME=Apache-2.0 16 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 17 | POM_LICENSE_DIST=repo 18 | POM_SCM_URL=https://github.com/composexy/decomposer/tree/main 19 | POM_SCM_CONNECTION=scm:git:github.com/composexy/decomposer.git 20 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/composexy/decomposer.git 21 | POM_DESCRIPTION=A compose desktop app to view the internals of compose runtime. 22 | POM_INCEPTION_YEAR=2024 23 | POM_URL=https://github.com/composexy/decomposer 24 | POM_DEVELOPER_ID=baiqindotfubotv 25 | POM_DEVELOPER_NAME=Baiqin Wang 26 | POM_DEVELOPER_URL=https://medium.com/@baiqin-droid1001 27 | POM_DEVELOPER_EMAIL=baiqin.droidoneooone@gmail.com 28 | SONATYPE_HOST=CENTRAL_PORTAL 29 | SONATYPE_AUTOMATIC_RELEASE=true 30 | RELEASE_SIGNING_ENABLED=true 31 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.7.3" 3 | android-compileSdk = "35" 4 | android-minSdk = "21" 5 | android-targetSdk = "35" 6 | androidx-activityCompose = "1.9.3" 7 | compose-multiplatform = "1.7.1" 8 | junit = "4.13.2" 9 | kotlin = "2.1.0" 10 | kotlinx-coroutines = "1.9.0" 11 | runner = "1.0.2" 12 | espressoCore = "3.0.2" 13 | composeBom = "2024.12.01" 14 | kotlinx-serialization = "1.7.3" 15 | dexlib2 = "2.5.2" 16 | wire = "5.1.0" 17 | moshi = "1.15.1" 18 | ktor = "3.0.1" 19 | collectionJvm = "1.4.5" 20 | publishing = "0.30.0" 21 | 22 | [libraries] 23 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 24 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 25 | junit = { group = "junit", name = "junit", version.ref = "junit" } 26 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activityCompose" } 27 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } 28 | androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 29 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 30 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 31 | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 32 | kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 33 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 34 | kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 35 | kotlin-gradlePlugin-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } 36 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 37 | runner = { group = "com.android.support.test", name = "runner", version.ref = "runner" } 38 | espresso-core = { group = "com.android.support.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 39 | dexlib2 = { module = "org.smali:dexlib2", version.ref = "dexlib2" } 40 | kotlinx-serializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 41 | squareup-wire-moshiAdapter = { group = "com.squareup.wire", name = "wire-moshi-adapter", version.ref = "wire" } 42 | squareup-moshi = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } 43 | squareup-moshiAdapters = { group = "com.squareup.moshi", name = "moshi-adapters", version.ref = "moshi" } 44 | squareup-wire-runtime = { group = "com.squareup.wire", name = "wire-runtime", version.ref = "wire" } 45 | ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 46 | ktor-client-contentNegotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } 47 | ktor-client-websocket = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" } 48 | ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } 49 | ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } 50 | ktor-server-contentNegotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" } 51 | ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" } 52 | ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" } 53 | ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" } 54 | androidx-collection-jvm = { group = "androidx.collection", name = "collection-jvm", version.ref = "collectionJvm" } 55 | 56 | [plugins] 57 | androidApplication = { id = "com.android.application", version.ref = "agp" } 58 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 59 | composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } 60 | composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 61 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 62 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 63 | wire = { id = "com.squareup.wire", version.ref = "wire" } 64 | mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "publishing" } 65 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 66 | kotlinx-serialization = { id = "plugin.serialization", version.ref = "kotlin" } 67 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | ALL_PROJECTS=( 6 | ":compiler-plugin" 7 | ":compiler-plugin-gradle" 8 | ":runtime" 9 | ) 10 | 11 | for PROJECT in "${ALL_PROJECTS[@]}"; do 12 | echo -e "Publishing $PROJECT" 13 | ./gradlew "${PROJECT}:publishToMavenCentral" 14 | done 15 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /runtime/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.mavenPublish) 7 | alias(libs.plugins.wire) 8 | alias(libs.plugins.composeMultiplatform) 9 | alias(libs.plugins.composeCompiler) 10 | kotlin(libs.plugins.kotlinx.serialization.get().pluginId) version libs.versions.kotlin 11 | } 12 | 13 | wire { 14 | kotlin {} 15 | } 16 | 17 | java { 18 | sourceCompatibility = JavaVersion.VERSION_11 19 | targetCompatibility = JavaVersion.VERSION_11 20 | } 21 | 22 | group = "com.decomposer.runtime" 23 | 24 | kotlin { 25 | jvm("desktop") 26 | 27 | androidTarget { 28 | publishLibraryVariants("release") 29 | compilerOptions { 30 | jvmTarget.set(JvmTarget.JVM_11) 31 | } 32 | } 33 | 34 | sourceSets { 35 | val commonMain by getting { 36 | dependencies { 37 | implementation(libs.kotlinx.serializationJson) 38 | implementation(libs.squareup.wire.runtime) 39 | implementation(libs.ktor.client.contentNegotiation) 40 | implementation(libs.ktor.client.okhttp) 41 | implementation(libs.ktor.client.core) 42 | implementation(libs.ktor.client.websocket) 43 | implementation(libs.ktor.serialization.kotlinx.json) 44 | implementation(libs.androidx.collection.jvm) 45 | implementation(libs.kotlin.reflect) 46 | implementation(compose.runtime) 47 | implementation(compose.ui) 48 | } 49 | } 50 | val commonTest by getting { 51 | dependencies { 52 | implementation(libs.kotlin.test) 53 | } 54 | } 55 | val androidMain by getting { 56 | dependencies { 57 | implementation(libs.dexlib2) 58 | } 59 | } 60 | val androidUnitTest by getting { 61 | dependencies {} 62 | } 63 | val desktopMain by getting { 64 | dependencies {} 65 | } 66 | val desktopTest by getting { 67 | dependencies {} 68 | } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.decomposer.runtime" 74 | compileSdk = libs.versions.android.compileSdk.get().toInt() 75 | 76 | defaultConfig { 77 | minSdk = libs.versions.android.minSdk.get().toInt() 78 | } 79 | packaging { 80 | resources { 81 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 82 | } 83 | } 84 | buildTypes { 85 | getByName("release") { 86 | isMinifyEnabled = false 87 | } 88 | } 89 | compileOptions { 90 | sourceCompatibility = JavaVersion.VERSION_11 91 | targetCompatibility = JavaVersion.VERSION_11 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /runtime/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Decomposer runtime 2 | POM_ARTIFACT_ID=runtime 3 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/com/decomposer/runtime/AndroidReflections.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Build 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.view.inspector.WindowInspector 8 | import androidx.compose.runtime.Composition 9 | import androidx.compose.runtime.snapshots.SnapshotStateObserver 10 | import androidx.compose.ui.R 11 | import java.lang.reflect.Field 12 | import kotlin.reflect.KProperty1 13 | import kotlin.reflect.full.declaredMembers 14 | import kotlin.reflect.jvm.isAccessible 15 | 16 | @SuppressLint("PrivateApi", "DiscouragedPrivateApi") 17 | @Suppress("UNCHECKED_CAST") 18 | internal class WindowManagerReflection { 19 | private var windowManager: Any? 20 | private var viewsField: Field 21 | 22 | init { 23 | val windowManagerClazz = Class.forName(WINDOW_MANAGER_GLOBAL) 24 | val getInstanceMethod = windowManagerClazz.getMethod(WINDOW_MANAGER_GET_INSTANCE) 25 | windowManager = getInstanceMethod.invoke(null)!! 26 | viewsField = windowManagerClazz.getDeclaredField(WINDOW_MANAGER_VIEWS).also { 27 | it.isAccessible = true 28 | } 29 | } 30 | 31 | val rootViews: List 32 | get() { 33 | return if (Build.VERSION.SDK_INT >= 29) { 34 | WindowInspector.getGlobalWindowViews() 35 | } else { 36 | viewsField.get(windowManager) as List 37 | } 38 | } 39 | 40 | companion object { 41 | private const val WINDOW_MANAGER_GLOBAL = "android.view.WindowManagerGlobal" 42 | private const val WINDOW_MANAGER_GET_INSTANCE = "getInstance" 43 | private const val WINDOW_MANAGER_VIEWS = "mViews" 44 | private const val TAG = "WindowManagerReflection" 45 | } 46 | } 47 | 48 | @SuppressLint("PrivateApi", "DiscouragedPrivateApi") 49 | @Suppress("UNCHECKED_CAST") 50 | internal class ViewReflection( 51 | private val view: View, 52 | private val logger: Logger 53 | ) { 54 | val composition: Composition? 55 | get() { 56 | val children = mutableListOf(view) 57 | while (children.isNotEmpty()) { 58 | val next = children.last() 59 | if (next.getTag(R.id.wrapped_composition_tag) != null) { 60 | val wrappedComposition = next.getTag(R.id.wrapped_composition_tag) 61 | val clazz = wrappedComposition::class 62 | val property = clazz.declaredMembers 63 | .find { it.name == WRAPPED_COMPOSITION_ORIGINAL } as? KProperty1 64 | if (property == null) { 65 | logger.log(Logger.Level.WARNING, TAG, "Cannot find original property!") 66 | return null 67 | } 68 | return property.get(wrappedComposition) as Composition 69 | } 70 | children.remove(next) 71 | if (next is ViewGroup) { 72 | for (i in 0 until next.childCount) { 73 | children.add(next.getChildAt(i)) 74 | } 75 | } 76 | } 77 | return null 78 | } 79 | 80 | val snapshotStateObserver: SnapshotStateObserver? 81 | get() { 82 | val children = mutableListOf(view) 83 | while (children.isNotEmpty()) { 84 | val next = children.last() 85 | val viewName = next::class.qualifiedName 86 | if (viewName == "androidx.compose.ui.platform.AndroidComposeView") { 87 | val viewClazz = next::class 88 | val ownerObserverProperty = viewClazz.members 89 | .find { it.name == SNAPSHOT_OBSERVER } as? KProperty1 90 | if (ownerObserverProperty == null) { 91 | logger.log(Logger.Level.WARNING, TAG, "Cannot find snapshotObserver property!") 92 | return null 93 | } 94 | ownerObserverProperty.isAccessible = true 95 | val ownerObserver = ownerObserverProperty.get(next) 96 | if (ownerObserver == null) { 97 | logger.log(Logger.Level.WARNING, TAG, "Cannot get owner observer!") 98 | return null 99 | } 100 | val ownerClazz = ownerObserver::class 101 | val observerProperty = ownerClazz.members 102 | .find { it.name == OBSERVER } as? KProperty1 103 | if (observerProperty == null) { 104 | logger.log(Logger.Level.WARNING, TAG, "Cannot find observer property!") 105 | return null 106 | } 107 | observerProperty.isAccessible = true 108 | val snapshotObserver = observerProperty.get(ownerObserver) as? SnapshotStateObserver 109 | if (snapshotObserver == null) { 110 | logger.log(Logger.Level.WARNING, TAG, "Cannot get snapshot observer!") 111 | } 112 | return snapshotObserver 113 | } 114 | children.remove(next) 115 | if (next is ViewGroup) { 116 | for (i in 0 until next.childCount) { 117 | children.add(next.getChildAt(i)) 118 | } 119 | } 120 | } 121 | return null 122 | } 123 | 124 | companion object { 125 | private const val WRAPPED_COMPOSITION_ORIGINAL = "original" 126 | private const val SNAPSHOT_OBSERVER = "snapshotObserver" 127 | private const val OBSERVER = "observer" 128 | private const val TAG = "ViewReflection" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/com/decomposer/runtime/Client.android.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | import com.decomposer.runtime.compose.CompositionNormalizer 4 | import com.decomposer.runtime.connection.AbstractOkHttpClient 5 | import com.decomposer.runtime.connection.ConnectionContract 6 | import com.decomposer.runtime.connection.model.DeviceDescriptor 7 | import com.decomposer.runtime.connection.model.DeviceType 8 | import com.decomposer.runtime.ir.ProjectScanner 9 | 10 | internal class AndroidOkHttpClient( 11 | serverPort: Int = ConnectionContract.DEFAULT_SERVER_PORT, 12 | projectScanner: ProjectScanner, 13 | compositionNormalizer: CompositionNormalizer 14 | ) : AbstractOkHttpClient( 15 | serverPort, 16 | projectScanner, 17 | compositionNormalizer 18 | ), Logger by AndroidLogger { 19 | override fun buildDeviceDescriptor(): DeviceDescriptor = 20 | DeviceDescriptor(deviceType = DeviceType.ANDROID) 21 | } 22 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/com/decomposer/runtime/Logger.android.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | import android.util.Log 4 | 5 | internal object AndroidLogger : Logger { 6 | override fun log(level: Logger.Level, tag: String, message: String) { 7 | when (level) { 8 | Logger.Level.DEBUG -> Log.d(tag, message) 9 | Logger.Level.INFO -> Log.i(tag, message) 10 | Logger.Level.WARNING -> Log.w(tag, message) 11 | Logger.Level.ERROR -> Log.e(tag, message) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/com/decomposer/runtime/Runtime.android.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.decomposer.runtime.composition.AndroidCompositionNormalizer 6 | import com.decomposer.runtime.ir.AndroidProjectScanner 7 | 8 | class AndroidRuntime(context: Context, config: RuntimeConfig) { 9 | private val projectScanner = AndroidProjectScanner( 10 | context = context, 11 | preloadAllIr = config.preloadAllIr, 12 | cacheIr = config.cacheIr, 13 | packagePrefixes = config.packagePrefixes 14 | ) 15 | private val compositionNormalizer = AndroidCompositionNormalizer(context) 16 | private val client = AndroidOkHttpClient( 17 | projectScanner = projectScanner, 18 | compositionNormalizer = compositionNormalizer 19 | ) 20 | 21 | fun init() { 22 | projectScanner.scanProject() 23 | client.start() 24 | } 25 | } 26 | 27 | fun Application.runtimeInit(block: (RuntimeConfigScope.() -> Unit)? = null): AndroidRuntime { 28 | val scope = RuntimeConfigScope() 29 | block?.let { scope.it() } 30 | return AndroidRuntime(this, scope).also { 31 | it.init() 32 | } 33 | } 34 | 35 | interface RuntimeConfig { 36 | val preloadAllIr: Boolean 37 | val cacheIr: Boolean 38 | val packagePrefixes: List? 39 | } 40 | 41 | class RuntimeConfigScope internal constructor( 42 | override var preloadAllIr: Boolean = false, 43 | override var cacheIr: Boolean = true, 44 | override var packagePrefixes: List? = null, 45 | ) : RuntimeConfig 46 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/com/decomposer/runtime/composition/CompositionNormalizer.android.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.composition 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import androidx.compose.runtime.Composition 6 | import androidx.compose.runtime.MonotonicFrameClock 7 | import androidx.compose.runtime.snapshots.SnapshotStateObserver 8 | import androidx.compose.ui.platform.AndroidUiDispatcher 9 | import androidx.compose.ui.platform.isDebugInspectorInfoEnabled 10 | import com.decomposer.runtime.Logger 11 | import com.decomposer.runtime.connection.model.CompositionRoots 12 | import com.decomposer.runtime.AndroidLogger 13 | import com.decomposer.runtime.ViewReflection 14 | import com.decomposer.runtime.WindowManagerReflection 15 | import com.decomposer.runtime.compose.CompositionNormalizer 16 | 17 | internal class AndroidCompositionNormalizer( 18 | private val context: Context 19 | ) : CompositionNormalizer(AndroidLogger), Logger by AndroidLogger { 20 | 21 | private val uiDispatcher = AndroidUiDispatcher.Main 22 | private val frameClock = uiDispatcher[MonotonicFrameClock] 23 | private val compositions = mutableListOf() 24 | private val snapshotStateObservers = mutableListOf() 25 | 26 | init { enableInspection() } 27 | 28 | override suspend fun extractCompositionRoots(): CompositionRoots { 29 | val clock = frameClock ?: throw IllegalArgumentException("Cannot find frame clock!") 30 | val reflection = WindowManagerReflection() 31 | return clock.withFrameNanos { 32 | val rootViews = reflection.rootViews 33 | extractCompositionData(rootViews) 34 | } 35 | } 36 | 37 | private fun extractCompositionData(rootViews: List): CompositionRoots { 38 | compositions.clear() 39 | snapshotStateObservers.clear() 40 | rootViews.forEach { rootView -> 41 | val reflection = ViewReflection(rootView, AndroidLogger) 42 | reflection.composition?.let { 43 | compositions.add(it) 44 | } 45 | reflection.snapshotStateObserver?.let { 46 | snapshotStateObservers.add(it) 47 | } 48 | } 49 | return map(compositions, snapshotStateObservers) 50 | } 51 | 52 | private fun enableInspection() { 53 | isDebugInspectorInfoEnabled = true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/Annotations.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | @MustBeDocumented 4 | @Target(AnnotationTarget.CLASS) 5 | @Retention(AnnotationRetention.RUNTIME) 6 | annotation class PreComposeIr( 7 | val filePath: String, 8 | val packageName: String, 9 | val isFileFacade: Boolean, 10 | val standardDump: Array, 11 | val data: Array, 12 | ) 13 | 14 | @MustBeDocumented 15 | @Target(AnnotationTarget.CLASS) 16 | @Retention(AnnotationRetention.RUNTIME) 17 | annotation class PostComposeIr( 18 | val filePath: String, 19 | val packageName: String, 20 | val isFileFacade: Boolean, 21 | val standardDump: Array, 22 | val data: Array, 23 | ) 24 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime 2 | 3 | internal fun interface Logger { 4 | fun log(level: Level, tag: String, message: String) 5 | 6 | enum class Level { 7 | DEBUG, INFO, WARNING, ERROR 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/AbstractOkHttpClient.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection 2 | 3 | import com.decomposer.runtime.Logger 4 | import com.decomposer.runtime.compose.CompositionNormalizer 5 | import com.decomposer.runtime.connection.model.Command 6 | import com.decomposer.runtime.connection.model.CommandKeys 7 | import com.decomposer.runtime.connection.model.CommandResponse 8 | import com.decomposer.runtime.connection.model.CompositionDataResponse 9 | import com.decomposer.runtime.connection.model.ProjectSnapshot 10 | import com.decomposer.runtime.connection.model.ProjectSnapshotResponse 11 | import com.decomposer.runtime.connection.model.SessionData 12 | import com.decomposer.runtime.connection.model.VirtualFileIr 13 | import com.decomposer.runtime.connection.model.VirtualFileIrResponse 14 | import com.decomposer.runtime.ir.ProjectScanner 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.cancel 18 | import kotlinx.coroutines.delay 19 | import kotlinx.coroutines.launch 20 | import kotlinx.serialization.json.Json 21 | import okhttp3.Call 22 | import okhttp3.Callback 23 | import okhttp3.OkHttpClient 24 | import okhttp3.Request 25 | import okhttp3.Response 26 | import okhttp3.WebSocket 27 | import okhttp3.WebSocketListener 28 | import okio.ByteString 29 | import java.io.IOException 30 | import kotlin.time.Duration.Companion.seconds 31 | 32 | internal abstract class AbstractOkHttpClient( 33 | private val serverPort: Int, 34 | private val projectScanner: ProjectScanner, 35 | private val compositionNormalizer: CompositionNormalizer 36 | ) : Logger, Client { 37 | 38 | private lateinit var webSocket: WebSocket 39 | private val coroutineScope = CoroutineScope(Dispatchers.IO) 40 | private val loggerTag = this::class.java.simpleName 41 | private val okHttpClient = OkHttpClient() 42 | 43 | override fun start() { 44 | val device = buildDeviceDescriptor() 45 | val newSessionRequest = Request.Builder().url( 46 | "http://localhost:$serverPort/${ConnectionContract.DEFAULT_CONNECTION_PATH}" 47 | ).header(ConnectionContract.HEADER_DEVICE_TYPE, device.deviceType.name).build() 48 | 49 | okHttpClient.newCall(newSessionRequest).enqueue(object : Callback { 50 | override fun onFailure(call: Call, e: IOException) { 51 | log(Logger.Level.WARNING, loggerTag, e.stackTraceToString()) 52 | restart() 53 | } 54 | 55 | override fun onResponse(call: Call, response: Response) { 56 | when (val statusCode = response.code) { 57 | 200 -> { 58 | val body = response.body?.string() 59 | log(Logger.Level.DEBUG, loggerTag, "Received sessionData: $body") 60 | if (body == null) { 61 | log(Logger.Level.ERROR, loggerTag, "Unexpected empty body") 62 | restart() 63 | return 64 | } 65 | try { 66 | val sessionData = Json.decodeFromString(body) 67 | webSocket = runSession(sessionData.sessionUrl) 68 | } catch (ex: Exception) { 69 | log(Logger.Level.ERROR, loggerTag, "Unexpected error while parsing body: ${ex.stackTraceToString()}") 70 | restart() 71 | } 72 | } 73 | else -> { 74 | log(Logger.Level.INFO, loggerTag, "Unexpected status code: $statusCode") 75 | restart() 76 | } 77 | } 78 | } 79 | }) 80 | } 81 | 82 | private fun restart() { 83 | coroutineScope.launch { 84 | delay(PROBE_INTERVAL_SECONDS.seconds) 85 | start() 86 | } 87 | } 88 | 89 | override fun stop() { 90 | webSocket.close(1000, "stop") 91 | coroutineScope.cancel() 92 | } 93 | 94 | private fun runSession(sessionUrl: String): WebSocket { 95 | val websocketRequest = Request.Builder() 96 | .url("ws://localhost:$serverPort/$sessionUrl") 97 | .build() 98 | val websocketListener = object : WebSocketListener() { 99 | override fun onOpen(webSocket: WebSocket, response: Response) { 100 | log(Logger.Level.DEBUG, loggerTag, "Websocket onOpen") 101 | } 102 | 103 | override fun onMessage(webSocket: WebSocket, text: String) { 104 | log(Logger.Level.DEBUG, loggerTag, "Received onMessage text: $text") 105 | val command = Json.decodeFromString(text) 106 | when (command.key) { 107 | CommandKeys.VIRTUAL_FILE_IR -> { 108 | val filePaths = command.parameters 109 | processVirtualFileIr(webSocket, filePaths) 110 | } 111 | CommandKeys.PROJECT_SNAPSHOT -> processProjectSnapshot(webSocket) 112 | CommandKeys.COMPOSITION_DATA -> processCompositionData(webSocket) 113 | } 114 | } 115 | 116 | override fun onMessage(webSocket: WebSocket, bytes: ByteString) { 117 | log(Logger.Level.DEBUG, loggerTag, "Received onMessage bytes: $bytes") 118 | } 119 | 120 | override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { 121 | log(Logger.Level.DEBUG, loggerTag, "Websocket onClosing $code $reason") 122 | } 123 | 124 | override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { 125 | log(Logger.Level.DEBUG, loggerTag, "Websocket onClosed $code $reason") 126 | restart() 127 | } 128 | 129 | override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { 130 | log(Logger.Level.WARNING, loggerTag, "Websocket onFailure: ${t.stackTraceToString()}") 131 | restart() 132 | } 133 | } 134 | return okHttpClient.newWebSocket(websocketRequest, websocketListener) 135 | } 136 | 137 | private fun processCompositionData(webSocket: WebSocket) { 138 | coroutineScope.launch { 139 | val compositionRoots = compositionNormalizer.extractCompositionRoots() 140 | val response = CompositionDataResponse(compositionRoots) 141 | val serialized = Json.encodeToString(CommandResponse.serializer(), response) 142 | webSocket.send(serialized) 143 | } 144 | } 145 | 146 | private fun processProjectSnapshot(webSocket: WebSocket) { 147 | coroutineScope.launch { 148 | val scannedResult = projectScanner.fetchProjectSnapshot() 149 | val projectSnapshot = ProjectSnapshot( 150 | fileTree = scannedResult.first, 151 | packagesByPath = scannedResult.second 152 | ) 153 | val response = ProjectSnapshotResponse(projectSnapshot) 154 | val serialized = Json.encodeToString(CommandResponse.serializer(), response) 155 | webSocket.send(serialized) 156 | } 157 | } 158 | 159 | private fun processVirtualFileIr(webSocket: WebSocket, filePaths: List) { 160 | coroutineScope.launch { 161 | filePaths.forEach { 162 | val ir = projectScanner.fetchIr(it) 163 | val virtualFileIr = VirtualFileIr( 164 | filePath = it, 165 | composedIrFile = ir.composedIrFile ?: emptyList(), 166 | composedTopLevelIrClasses = ir.composedTopLevelIrClasses, 167 | composedStandardDump = ir.composedStandardDump, 168 | originalIrFile = ir.originalIrFile ?: emptyList(), 169 | originalTopLevelIrClasses = ir.originalTopLevelIrClasses, 170 | originalStandardDump = ir.originalStandardDump 171 | ) 172 | val response = VirtualFileIrResponse(virtualFileIr) 173 | val serialized = Json.encodeToString(CommandResponse.serializer(), response) 174 | webSocket.send(serialized) 175 | } 176 | } 177 | } 178 | 179 | companion object { 180 | private const val PROBE_INTERVAL_SECONDS = 3L 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/Client.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection 2 | 3 | import com.decomposer.runtime.connection.model.DeviceDescriptor 4 | 5 | internal interface Client { 6 | fun start() 7 | fun stop() 8 | fun buildDeviceDescriptor(): DeviceDescriptor 9 | } 10 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/ConnectionContract.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection 2 | 3 | object ConnectionContract { 4 | const val HEADER_DEVICE_TYPE = "X-Device-Type" 5 | const val DEFAULT_SERVER_PORT = 9801 6 | const val DEFAULT_CONNECTION_PATH = "/connect" 7 | } 8 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/model/Composition.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class CompositionRoots( 7 | val compositionData: List, 8 | val stateTable: List, 9 | val snapshotObserverStateTable: Map>, 10 | val stringTable: List, 11 | val dataTable: List, 12 | val groupTable: List 13 | ) 14 | 15 | @Serializable 16 | class CompositionRoot( 17 | val contextIndex: Int?, 18 | val groupIndexes: List 19 | ) 20 | 21 | @Serializable 22 | class Group( 23 | val attributes: Attributes, 24 | val dataIndexes: List, 25 | val childIndexes: List 26 | ) 27 | 28 | @Serializable 29 | class Attributes( 30 | val key: GroupKey, 31 | val sourceInformationIndex: Int? 32 | ) 33 | 34 | @Serializable 35 | sealed interface GroupKey 36 | 37 | @Serializable 38 | class IntKey(val value: Int) : GroupKey 39 | 40 | @Serializable 41 | class ObjectKey(val valueIndex: Int) : GroupKey 42 | 43 | @Serializable 44 | sealed interface Data { 45 | val toStringIndex: Int 46 | val typeNameIndex: Int? 47 | val hashCode: Int 48 | } 49 | 50 | @Serializable 51 | class EmptyData( 52 | override val toStringIndex: Int, 53 | override val typeNameIndex: Int?, 54 | override val hashCode: Int 55 | ) : Data 56 | 57 | @Serializable 58 | class Context( 59 | val compoundHashKey: Int, 60 | override val toStringIndex: Int, 61 | override val typeNameIndex: Int?, 62 | override val hashCode: Int 63 | ) : Data 64 | 65 | @Serializable 66 | class Default( 67 | override val toStringIndex: Int, 68 | override val typeNameIndex: Int?, 69 | override val hashCode: Int 70 | ) : Data 71 | 72 | @Serializable 73 | class ComposeState( 74 | val valueIndex: Int, 75 | val dependencyIndexes: List, 76 | val readInComposition: Boolean?, 77 | val readInSnapshotFlow: Boolean?, 78 | val readInSnapshotStateObserver: Boolean?, 79 | override val toStringIndex: Int, 80 | override val typeNameIndex: Int?, 81 | override val hashCode: Int 82 | ) : Data 83 | 84 | @Serializable 85 | class RecomposeScope( 86 | val stateIndexes: List, 87 | override val toStringIndex: Int, 88 | override val typeNameIndex: Int?, 89 | override val hashCode: Int 90 | ) : Data 91 | 92 | @Serializable 93 | class SubcomposeState( 94 | val compositions: List, 95 | override val toStringIndex: Int, 96 | override val typeNameIndex: Int?, 97 | override val hashCode: Int 98 | ) : Data 99 | 100 | @Serializable 101 | class RememberObserverHolder( 102 | val wrappedIndex: Int, 103 | override val toStringIndex: Int, 104 | override val typeNameIndex: Int?, 105 | override val hashCode: Int 106 | ) : Data 107 | 108 | @Serializable 109 | class CompositionContextHolder( 110 | val refIndex: Int, 111 | override val toStringIndex: Int, 112 | override val typeNameIndex: Int?, 113 | override val hashCode: Int 114 | ) : Data 115 | 116 | @Serializable 117 | class ComposableLambdaImpl( 118 | val key: Int, 119 | val blockIndex: Int?, 120 | val tracked: Boolean, 121 | val scopeIndex: Int?, 122 | val scopeIndexes: List, 123 | override val toStringIndex: Int, 124 | override val typeNameIndex: Int?, 125 | override val hashCode: Int 126 | ) : Data 127 | 128 | @Serializable 129 | class LayoutNode( 130 | val lookaheadRootIndex: Int?, 131 | val childIndexes: List, 132 | val parentIndex: Int?, 133 | val nodeIndexes: List, 134 | val coordinatorIndexes: List, 135 | override val toStringIndex: Int, 136 | override val typeNameIndex: Int?, 137 | override val hashCode: Int 138 | ) : Data 139 | 140 | @Serializable 141 | data class ModifierNode( 142 | override val toStringIndex: Int, 143 | override val typeNameIndex: Int?, 144 | override val hashCode: Int 145 | ) : Data 146 | 147 | @Serializable 148 | class Coordinator( 149 | val tailNodeIndex: Int, 150 | override val toStringIndex: Int, 151 | override val typeNameIndex: Int?, 152 | override val hashCode: Int 153 | ) : Data 154 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/model/Contract.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection.model 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.json.Json 5 | import kotlinx.serialization.modules.polymorphic 6 | import kotlinx.serialization.modules.SerializersModule 7 | import kotlinx.serialization.modules.subclass 8 | 9 | @Serializable 10 | data class Command( 11 | val key: String, 12 | val parameters: List = emptyList() 13 | ) 14 | 15 | @Serializable 16 | sealed class CommandResponse 17 | 18 | @Serializable 19 | class ProjectSnapshotResponse( 20 | val projectSnapshot: ProjectSnapshot 21 | ) : CommandResponse() 22 | 23 | @Serializable 24 | class VirtualFileIrResponse( 25 | val virtualFileIr: VirtualFileIr 26 | ) : CommandResponse() 27 | 28 | @Serializable 29 | class CompositionDataResponse( 30 | val compositionRoots: CompositionRoots 31 | ) : CommandResponse() 32 | 33 | val commandResponseSerializer = Json { 34 | prettyPrint = true 35 | ignoreUnknownKeys = true 36 | serializersModule = SerializersModule { 37 | polymorphic(CommandResponse::class) { 38 | subclass(ProjectSnapshotResponse::class) 39 | subclass(VirtualFileIrResponse::class) 40 | subclass(CompositionDataResponse::class) 41 | } 42 | } 43 | } 44 | 45 | object CommandKeys { 46 | const val PROJECT_SNAPSHOT = "PROJECT_SNAPSHOT" 47 | const val VIRTUAL_FILE_IR = "VIRTUAL_FILE_IR" 48 | const val COMPOSITION_DATA = "COMPOSITION_DATA" 49 | } 50 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/model/Device.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class DeviceDescriptor( 7 | val deviceType: DeviceType 8 | ) 9 | 10 | @Serializable 11 | enum class DeviceType { 12 | ANDROID 13 | } 14 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/model/Ir.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class ProjectSnapshot( 7 | val fileTree: Set, 8 | val packagesByPath: Map 9 | ) 10 | 11 | @Serializable 12 | class VirtualFileIr( 13 | val filePath: String, 14 | val composedIrFile: List, 15 | val composedTopLevelIrClasses: Set>, 16 | val composedStandardDump: List, 17 | val originalIrFile: List, 18 | val originalTopLevelIrClasses: Set>, 19 | val originalStandardDump: List = emptyList() 20 | ) 21 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/connection/model/SessionData.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.connection.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class SessionData( 7 | val sessionId: String, 8 | val sessionUrl: String 9 | ) 10 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/com/decomposer/runtime/ir/Project.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.runtime.ir 2 | 3 | internal data class VirtualFileIr( 4 | val filePath: String, 5 | val composedIrFile: List? = null, 6 | val composedTopLevelIrClasses: Set> = emptySet(), 7 | val composedStandardDump: List = emptyList(), 8 | val originalIrFile: List? = null, 9 | val originalTopLevelIrClasses: Set> = emptySet(), 10 | val originalStandardDump: List = emptyList() 11 | ) 12 | 13 | internal interface ProjectScanner { 14 | suspend fun fetchProjectSnapshot(): Pair, Map> 15 | suspend fun fetchIr(filePath: String): VirtualFileIr 16 | } 17 | -------------------------------------------------------------------------------- /runtime/src/commonMain/proto/com/decomposer/runtime/ir/JvmIr.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package com.decomposer.runtime.ir; 3 | import "com/decomposer/runtime/ir/KotlinIr.proto"; 4 | 5 | option java_outer_classname = "JvmIr"; 6 | option optimize_for = SPEED; 7 | 8 | /* Stored in JVM .class annotations */ 9 | 10 | message XStatementOrExpression { 11 | oneof kind { 12 | IrStatement statement = 1; 13 | IrExpression expression = 2; 14 | } 15 | } 16 | 17 | message ClassOrFile { 18 | repeated IrDeclaration declaration = 1; 19 | repeated IrType type = 2; 20 | repeated IdSignature signature = 3; 21 | repeated string string = 4; 22 | repeated XStatementOrExpression body = 5; 23 | repeated string debug_info = 6; 24 | required string file_facade_fq_name = 7; 25 | } 26 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.composeCompiler) 5 | } 6 | 7 | android { 8 | namespace = "com.decomposer.sample" 9 | compileSdk = libs.versions.android.compileSdk.get().toInt() 10 | 11 | buildFeatures { 12 | buildConfig = true 13 | } 14 | 15 | defaultConfig { 16 | applicationId = "com.decomposer.sample" 17 | minSdk = libs.versions.android.minSdk.get().toInt() 18 | targetSdk = libs.versions.android.targetSdk.get().toInt() 19 | versionCode = 1 20 | versionName = "1.0.0" 21 | 22 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildTypes { 26 | release { 27 | isMinifyEnabled = true 28 | proguardFiles( 29 | getDefaultProguardFile("proguard-android-optimize.txt"), 30 | "proguard-rules.pro" 31 | ) 32 | signingConfig = signingConfigs.getByName("debug") 33 | } 34 | debug { 35 | isDebuggable = true 36 | isMinifyEnabled = false 37 | } 38 | } 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.VERSION_11 41 | targetCompatibility = JavaVersion.VERSION_11 42 | } 43 | kotlinOptions { 44 | jvmTarget = "11" 45 | } 46 | } 47 | 48 | kotlin { 49 | compilerOptions { 50 | val isDebug = project.hasProperty("android") 51 | && android.buildTypes.find { it.name == "debug" } != null 52 | if (isDebug) { 53 | freeCompilerArgs.addAll( 54 | "-P", "plugin:com.decomposer.compiler:enabled=true", 55 | ) 56 | } else { 57 | freeCompilerArgs.addAll( 58 | "-P", "plugin:com.decomposer.compiler:enabled=false" 59 | ) 60 | } 61 | } 62 | } 63 | 64 | dependencies { 65 | implementation(project.dependencies.platform(libs.androidx.compose.bom)) 66 | implementation(libs.kotlin.stdlib) 67 | implementation(libs.androidx.activity.compose) 68 | implementation(libs.androidx.compose.ui) 69 | implementation(libs.androidx.compose.ui.graphics) 70 | implementation(libs.androidx.compose.ui.tooling.preview) 71 | implementation(libs.androidx.compose.material3) 72 | implementation(projects.runtime) 73 | kotlinCompilerPluginClasspath(projects.compilerPlugin) 74 | testImplementation(libs.junit) 75 | androidTestImplementation(libs.runner) 76 | androidTestImplementation(libs.espresso.core) 77 | } 78 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/InteropSamples.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.unit.sp 14 | import androidx.compose.ui.window.Dialog 15 | import androidx.compose.ui.window.Popup 16 | 17 | @Composable 18 | fun SimplePopup() { 19 | Popup { 20 | Text( 21 | modifier = Modifier.size(160.dp), 22 | text = "I am SimplePopup!", 23 | fontSize = 24.sp, 24 | textAlign = TextAlign.Center 25 | ) 26 | } 27 | } 28 | 29 | @Composable 30 | fun SimpleDialog() { 31 | var showDialog: Boolean by remember { mutableStateOf(true) } 32 | if (showDialog) { 33 | Dialog( 34 | onDismissRequest = { 35 | showDialog = false 36 | } 37 | ) { 38 | Text( 39 | modifier = Modifier.size(160.dp), 40 | text = "I am SimpleDialog!", 41 | fontSize = 24.sp, 42 | textAlign = TextAlign.Center 43 | ) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | 21 | class MainActivity : ComponentActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | val selectedIndex = contentSelected 26 | if (selectedIndex == null) { 27 | ContentList() 28 | } else { 29 | contentList[selectedIndex].content() 30 | } 31 | } 32 | } 33 | 34 | @Suppress("DEPRECATION") 35 | override fun onBackPressed() { 36 | if (contentSelected == null) { 37 | super.onBackPressed() 38 | } else { 39 | contentSelected = null 40 | } 41 | } 42 | } 43 | 44 | @Composable 45 | fun ContentList() { 46 | LazyColumn(modifier = Modifier.fillMaxSize()) { 47 | items(contentList.size) { 48 | Text( 49 | modifier = Modifier.fillMaxWidth() 50 | .padding(16.dp) 51 | .clickable { contentSelected = it }, 52 | text = contentList[it].displayName, 53 | textAlign = TextAlign.Center, 54 | fontSize = 16.sp 55 | ) 56 | } 57 | } 58 | } 59 | 60 | var contentSelected: Int? by mutableStateOf(null) 61 | 62 | val contentList = listOf( 63 | SampleContent(displayName = "Simple Text", content = { SimpleText() }), 64 | SampleContent(displayName = "Simple Dialog", content = { SimpleDialog() }), 65 | SampleContent(displayName = "Simple Popup", content = { SimplePopup() }), 66 | SampleContent(displayName = "Simple BoxWithConstraints", content = { SimpleBoxWithConstraints() }), 67 | SampleContent(displayName = "Simple LazyColumn", content = { SimpleLazyColumn() }), 68 | SampleContent(displayName = "Simple State Reader", content = { SimpleStateReader() }) 69 | ) 70 | 71 | class SampleContent( 72 | val displayName: String, 73 | val content: @Composable () -> Unit 74 | ) 75 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/SampleApplication.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import android.app.Application 4 | import com.decomposer.runtime.runtimeInit 5 | 6 | class SampleApplication : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | if (BuildConfig.DEBUG) { 11 | runtimeInit { 12 | packagePrefixes = listOf(this@SampleApplication.packageName) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/SimpleStateReaderSample.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.derivedStateOf 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.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.graphicsLayer 16 | import androidx.compose.ui.unit.dp 17 | 18 | private var alphaFirst by mutableStateOf(0.5f) 19 | private var alphaSecond by mutableStateOf(0.9f) 20 | private var alphaThird by mutableStateOf(0.8f) 21 | 22 | @Composable 23 | fun SimpleStateReader() { 24 | Column { 25 | Box(modifier = Modifier.graphicsLayer { 26 | alpha = alphaFirst 27 | }.size(240.dp).background(Color.Yellow)) 28 | Box(modifier = Modifier.graphicsLayer( 29 | alpha = alphaSecond 30 | ).size(240.dp).background(Color.Blue)) 31 | val showThird: Boolean by remember { 32 | derivedStateOf { alphaThird > 0.5f } 33 | } 34 | if (showThird) { 35 | Box(modifier = Modifier.graphicsLayer { 36 | alpha = alphaThird 37 | }.size(240.dp).background(Color.Gray)) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/SubcomposeSamples.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import androidx.compose.foundation.layout.BoxWithConstraints 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.wrapContentHeight 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.style.TextAlign 12 | import androidx.compose.ui.unit.sp 13 | 14 | @Composable 15 | fun SimpleBoxWithConstraints() { 16 | BoxWithConstraints(modifier = Modifier.fillMaxSize()) { 17 | Text( 18 | modifier = Modifier.wrapContentHeight().fillMaxWidth(), 19 | text = """ 20 | Hello SimpleBoxWithConstraints! 21 | Your minWidth is ${constraints.minWidth} 22 | Your maxWidth is ${constraints.maxWidth} 23 | Your minHeight is ${constraints.minHeight} 24 | Your maxHeight is ${constraints.maxHeight} 25 | """.trimIndent(), 26 | fontSize = 24.sp, 27 | textAlign = TextAlign.Center 28 | ) 29 | } 30 | } 31 | 32 | @Composable 33 | fun SimpleLazyColumn() { 34 | LazyColumn(modifier = Modifier.fillMaxSize()) { 35 | items(200) { 36 | Text( 37 | modifier = Modifier.wrapContentHeight().fillMaxWidth(), 38 | text = "Index $it", 39 | fontSize = 24.sp, 40 | textAlign = TextAlign.Center 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/TextSamples.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.wrapContentHeight 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.style.TextAlign 9 | import androidx.compose.ui.unit.sp 10 | 11 | @Composable 12 | fun SimpleText() { 13 | Text( 14 | modifier = Modifier.wrapContentHeight().fillMaxWidth(), 15 | text = "Hello Decomposer!", 16 | fontSize = 24.sp, 17 | textAlign = TextAlign.Center, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | typealias A = Map> 4 | 5 | enum class Lists { 6 | LIST1, LIST2, LIST3 7 | } 8 | 9 | enum class State(private val state: String) { 10 | GOOD("good"), 11 | BAD("bad"); 12 | 13 | fun printState() { 14 | println("$state ${GOOD.state}") 15 | } 16 | } 17 | 18 | fun needState(state: State) { 19 | val data = DataClass("A", true) 20 | val real = RealType("real") 21 | if (state == State.BAD) { 22 | println("${Lists.LIST1} ${Lists.LIST3} $data ${real.id}") 23 | } else { 24 | Lists.entries.forEach { 25 | println("$it") 26 | } 27 | } 28 | } 29 | 30 | data class DataClass( 31 | val data1: String, 32 | val data2: Boolean = false 33 | ) 34 | 35 | internal class ToString { 36 | override fun toString(): String { 37 | return "ToString" 38 | } 39 | 40 | operator fun get(index: Int): String { 41 | return "ToString ${Real.INDEX} ${Real.DEFAULT} $EmptyType" 42 | } 43 | } 44 | 45 | @JvmInline 46 | value class MyLong(val long: Long) 47 | 48 | interface MyInterface3 { 49 | fun run3() 50 | } 51 | 52 | private fun interface MyInterface { 53 | fun run() 54 | } 55 | 56 | interface MyInterface2 { 57 | fun run() 58 | fun run2() 59 | } 60 | 61 | abstract class MyBase(val id: String, val value: T) : MyInterface2, MyInterface { 62 | abstract fun myBase(): T 63 | } 64 | 65 | class Real : MyBase>( 66 | "myId", emptyList() 67 | ), MyInterface, MyInterface3>> { 68 | override fun run2() { 69 | val prefix = "prefix" 70 | fun inner(): String { 71 | return "123" 72 | } 73 | val postfix = "postfix" 74 | println("$prefix ${inner()} $postfix") 75 | } 76 | 77 | override fun run() { 78 | run2() 79 | } 80 | 81 | override fun run3() { 82 | run() 83 | } 84 | 85 | override fun myBase(): List { 86 | run() 87 | run3() 88 | return listOf("1", "2", INDEX.toString()) 89 | } 90 | 91 | companion object { 92 | const val INDEX = 0 93 | val DEFAULT = Real() 94 | } 95 | 96 | inner class InnerClass( 97 | val index: Int 98 | ) 99 | } 100 | 101 | sealed interface Type 102 | data object EmptyType : Type 103 | private class RealType(val id: String) : Type 104 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/Expressions.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | fun breaks() { 4 | outerLoop@ for (i in 1..5) { 5 | for (j in 1..5) { 6 | if (j == 3) { 7 | println("Breaking out of outerLoop at i=$i, j=$j") 8 | break@outerLoop 9 | } 10 | println("i=$i, j=$j") 11 | } 12 | } 13 | 14 | for (i in 0 until 100) { 15 | if (i == 50) { 16 | break 17 | } 18 | } 19 | } 20 | 21 | fun typeOps() { 22 | val a = if ((System.currentTimeMillis() % 2).toInt() == 0) { 23 | B() 24 | } else { 25 | C() 26 | } 27 | val b = a as B 28 | val c = a as? C 29 | if (a is B) { 30 | println("Hello World!") 31 | } 32 | } 33 | 34 | interface Interface 35 | private class C : Interface 36 | private class B : Interface 37 | 38 | fun throws() { 39 | throw IllegalArgumentException("Wrong call") 40 | } 41 | 42 | fun whiles() { 43 | var a = 0 44 | do { 45 | a++ 46 | } while (a < 10) 47 | 48 | var b = 10 49 | while (b > 5) { 50 | b-- 51 | } 52 | } 53 | 54 | fun returnLabel(numbers: List, target: Int): Boolean { 55 | numbers.forEach label@{ 56 | if (it == target) { 57 | return@label 58 | } 59 | } 60 | return false 61 | } 62 | 63 | fun varargs(a: Int, b: String, vararg c: Int) { 64 | println("$a $b $c") 65 | val d = IntArray(12) 66 | varargs(10, "d", *d) 67 | } 68 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/Functions.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | fun empty() { } 6 | fun empty2() = Unit 7 | 8 | var outer = 10 9 | 10 | fun nullable(): String? { 11 | outer = 15 12 | var random = Math.random().toInt() 13 | random += outer 14 | var variable = 21L 15 | variable = (outer + random).toLong() 16 | return if (random == 0) "Hi" else null 17 | } 18 | 19 | fun annotated(): @Composable ((Int, String?, Map?) -> Boolean)? { 20 | return { i, _, m -> false } 21 | } 22 | 23 | fun singleParameter(input: Int) { } 24 | 25 | @SinceKotlin("1.3") 26 | fun > complexParameters( 27 | input1: String? = "Dave", 28 | input2: MutableMap<*, *>? = myMap(), 29 | input3: @Composable String.(@Composable Int.() -> Boolean) -> Unit, 30 | input4: String.() -> Unit = if (input1 == "Dave") { 31 | { } 32 | } else { 33 | { println("Non default") } 34 | } 35 | ): Boolean? { 36 | return false 37 | } 38 | 39 | private fun myMap(): MutableMap> = mutableMapOf() 40 | 41 | val time = System.currentTimeMillis() 42 | 43 | private fun String.indented() = "\t$this" 44 | 45 | @PublishedApi 46 | internal inline fun > generics( 47 | input: T, 48 | input2: R, 49 | input3: F 50 | ): String { 51 | if (T::class == String::class) { 52 | val hello = "Hello at $time" 53 | return hello 54 | } else { 55 | return "Hello world" 56 | } 57 | } 58 | 59 | private inline fun inlined(block: Printer.() -> Unit) { 60 | Child().block() 61 | } 62 | 63 | fun interface Printer { 64 | fun print(string: String) 65 | } 66 | 67 | abstract class Base(val base: String, time: Long) : Printer { 68 | private val time = time.toString() 69 | abstract fun base() 70 | open fun open() { 71 | print("$base $time") 72 | } 73 | open fun open2() {} 74 | open fun open3() {} 75 | } 76 | 77 | class Child : Base("Child", System.currentTimeMillis()) { 78 | override fun base() {} 79 | override fun open() {} 80 | override fun open2() { 81 | super.open2() 82 | } 83 | override fun print(string: String) {} 84 | private fun String.spaced() = " $this " 85 | } 86 | 87 | class Constructors private constructor( 88 | val id: String, 89 | val isAdmin: Boolean 90 | ) { 91 | private constructor(id: String) : this(id, id == "decomposer") 92 | } 93 | 94 | class NoPrimary { 95 | constructor(id: String) 96 | } 97 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/Operators.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | var a = 12 4 | val b = arrayOf(1, 2, 3) 5 | val c = b[1] 6 | val d = a++ 7 | val e = a-- 8 | val f = a == c 9 | val g = a != c 10 | val h = a === c 11 | val i = 12 in b 12 | val j = 12 !in b 13 | val k = (d + 9 - a) * 9 / 4 14 | val l = (a + 2 > 8 || d <= 11 && a + 4 < 15 || a * 9 <= 11) && g 15 | var m = if (a != 11) { 16 | 13 17 | } else 15 18 | var n = a !== 8 19 | var o = -a 20 | var p = --a 21 | var q = +a 22 | var r = ++a 23 | val s = 1 to 10 24 | val t = 1 until 100 25 | var u = 10 % 2 26 | var w = 1 .. 11 27 | val x = mapOf("1" to 12, "12" to 11) 28 | var y = x["123"] ?: x["1"] ?: "11" 29 | var z = a and c 30 | val aa = z or z 31 | var ab = z xor z 32 | var ac = z shl 2 33 | var ad = z shr 1 34 | var ae = z ushr 2 35 | var af = x["12"] 36 | 37 | fun test() { 38 | u = a 39 | u += 1 40 | u -= a 41 | u *= e 42 | u /= o 43 | u %= 2 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/Properties.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableIntStateOf 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.platform.LocalView 10 | 11 | val simpleProp = 12 12 | private lateinit var lateVar: String 13 | val withGetter: Int 14 | get() { 15 | return 123 16 | } 17 | 18 | var withSetter: Int = 111 19 | get() { 20 | return field * 2 21 | } 22 | set(value) { 23 | field = value - 2 24 | } 25 | 26 | @JvmField 27 | var jvmField: String = "Hey" 28 | var reference = ::withSetter.get() 29 | var reference2 = Data::prop2 30 | val data = Data(prop2 = false, value = "value") 31 | val reference3 = data::prop3 32 | val reference4 = Data.DataInner::prop6 33 | val reference5 = data.inner::prop6 34 | private val reference6 = data::prop3 35 | val reference7 = data::prop8 36 | val reference8 = data::prop9 37 | var getterSetter: String = "World" 38 | get() { 39 | val value = field.length 40 | return "Hello $value $simpleProp" 41 | } 42 | set(value) { 43 | field = "$value ${Object1.two} ${Data.companion1}" 44 | } 45 | 46 | object Object1 { 47 | private val one = "one" 48 | val two = "two" 49 | internal val three = "three" 50 | internal const val four = "four" 51 | } 52 | 53 | val annotatedAccessor: String 54 | @Composable 55 | get() { 56 | return LocalView.current.transitionName 57 | } 58 | 59 | @get:SinceKotlin("1.3") 60 | var annotatedAccessor2: String = "" 61 | get() { 62 | return "annotatedAccessor2" 63 | } 64 | set(value) { 65 | field = "$value $value" 66 | } 67 | 68 | var initWithExpression: Long = System.currentTimeMillis() / 10 69 | 70 | var propDelegated: Int by mutableIntStateOf(123) 71 | 72 | class Data( 73 | val prop1: Int = Math.random().toInt(), 74 | val prop2: Boolean, 75 | value: String 76 | ) { 77 | val prop3: String = "Inner" 78 | val prop4: String 79 | get() = prop3::class.simpleName ?: "" 80 | val prop5: String by mutableStateOf("Hi") 81 | val prop6: String by lazy { 82 | "Hi" 83 | } 84 | val prop8: IntArray = IntArray(1) 85 | val prop9 = arrayOf(1, 3, 4) 86 | val prop10 = "$value $value" 87 | val inner: DataInner = DataInner("Hello") 88 | 89 | init { 90 | Log.w("Data", "constructing Data") 91 | } 92 | 93 | class DataInner( 94 | val prop6: String 95 | ) 96 | 97 | fun accessProperties() { 98 | val prop7 = prop3 + prop4 99 | val reference = prop6::class 100 | val reference2 = prop6::class.simpleName 101 | val reference3 = ::prop4 102 | val prop8 = prop7 + prop1 103 | } 104 | 105 | companion object { 106 | const val companion1 = "companion1" 107 | } 108 | } 109 | 110 | class This( 111 | val value: String = "1234" 112 | ) { 113 | fun fetch(): String = "$value $this ${this.value}" 114 | } 115 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/SimpleComposable.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.unit.dp 8 | 9 | @Composable 10 | fun Simple() { 11 | Text(text = "Hello World!", modifier = Modifier.padding(1.dp)) 12 | } 13 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/TryCatchFinally.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | class TryCatchFinally { 4 | 5 | fun test() { 6 | val a = try { 7 | val b = "1234" 8 | b.toShort() 9 | } catch (ex: NumberFormatException) { 10 | println(ex.stackTraceToString()) 11 | } catch (ex: ConcurrentModificationException) { 12 | println("ConcurrentModificationException") 13 | } finally { 14 | println("Success!") 15 | } 16 | } 17 | 18 | inline fun withTry(scope: String, block: String.(String) -> Unit) { 19 | try { 20 | println(scope) 21 | scope.block(scope) 22 | } catch (ex: Exception) { 23 | println("Exception") 24 | } finally { 25 | println("finally") 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/decomposer/sample/ir/When.kt: -------------------------------------------------------------------------------- 1 | package com.decomposer.sample.ir 2 | 3 | val val1 = System.currentTimeMillis() 4 | val val2 = System.nanoTime() 5 | var var3 = "Hey" 6 | var var4 = "Hello" 7 | var var5 = true 8 | var var6 = false 9 | 10 | var good = var5 && var6 11 | var lucky = val1.toInt() % 4 == 1 || val2.toInt() % 3 == 2 12 | var notLucky = val1 > 100 || val2 == 1000L && var3.length > 34 || var4.startsWith("He") 13 | 14 | var funny = if (var3.startsWith("Lucy")) { 15 | 100 16 | } else if (var4.length > 11) { 17 | 1000 18 | } else { 19 | 1010 20 | } 21 | 22 | val simple = when (val1) { 23 | 1234L -> "A" 24 | 1231L -> "B" 25 | 10003L -> "C" 26 | else -> "D" 27 | } 28 | 29 | val complex = when { 30 | val1 != 10394L -> 1234 31 | var4.startsWith("A") -> 3133 32 | else -> 1355 33 | } 34 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composexy/decomposer/7ec9e477cf4f00db248e507793ac30c8c20f4d11/sample/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |