├── .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 | 
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