diff --git a/.github/ci/apple-checks.json b/.github/ci/apple-checks.json
index ec63e4e97c1..a0a9f2895ca 100644
--- a/.github/ci/apple-checks.json
+++ b/.github/ci/apple-checks.json
@@ -48,6 +48,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -102,6 +103,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -145,6 +147,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -186,6 +189,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -214,6 +218,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -240,6 +245,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -284,6 +290,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
@@ -320,6 +327,7 @@
"scripts/unity-compat-samples/gallery/**",
"maven/cil-translator/**",
"maven/unity-compat/**",
+ "maven/compose-compat/**",
"maven/build-engine/src/main/java/com/codename1/maven/Unity*",
"maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*",
"scripts/unity-gallery-prebuilt.sh",
diff --git a/.github/workflows/archetype-smoke.yml b/.github/workflows/archetype-smoke.yml
index 424545f63ce..9b6d522cfcc 100644
--- a/.github/workflows/archetype-smoke.yml
+++ b/.github/workflows/archetype-smoke.yml
@@ -73,7 +73,9 @@ concurrency:
jobs:
archetype-smoke:
runs-on: ubuntu-24.04
- timeout-minutes: 30
+ # A green run takes about 28 minutes (the reactor build alone is 8-9), so 30 left
+ # no room for a slow runner and cancelled healthy runs mid-step.
+ timeout-minutes: 45
steps:
- uses: actions/checkout@v6
# A JDK that is NOT 8, for the `cn1:backend-package` check at the end of the
diff --git a/.github/workflows/car-android.yml b/.github/workflows/car-android.yml
index 33a19b2588e..e2a9533aba5 100644
--- a/.github/workflows/car-android.yml
+++ b/.github/workflows/car-android.yml
@@ -13,6 +13,7 @@ name: Car (Android Auto) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -46,6 +47,7 @@ name: Car (Android Auto) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/car-ios.yml b/.github/workflows/car-ios.yml
index caf083dcf16..fa8e248dc9b 100644
--- a/.github/workflows/car-ios.yml
+++ b/.github/workflows/car-ios.yml
@@ -14,6 +14,7 @@ name: Car (CarPlay) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -37,6 +38,7 @@ name: Car (CarPlay) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/check-doc-snippets.yml b/.github/workflows/check-doc-snippets.yml
index 4dea0bf5d2f..f7130371a8a 100644
--- a/.github/workflows/check-doc-snippets.yml
+++ b/.github/workflows/check-doc-snippets.yml
@@ -17,6 +17,7 @@ on:
- 'scripts/savor/**'
- 'scripts/bastion/**'
- 'scripts/skirmish/**'
+ - 'scripts/hellocodenameone/common/src/main/kmp/**'
- 'docs/website/content/blog/**'
- 'CodenameOne/src/**/*.java'
- 'Ports/CLDC11/src/**/*.java'
diff --git a/.github/workflows/developer-guide-docs.yml b/.github/workflows/developer-guide-docs.yml
index 0a7b8557278..8bfbe259398 100644
--- a/.github/workflows/developer-guide-docs.yml
+++ b/.github/workflows/developer-guide-docs.yml
@@ -21,6 +21,7 @@ on:
- 'scripts/bastion/**'
- 'scripts/skirmish/**'
- 'scripts/parley/**'
+ - 'scripts/hellocodenameone/common/src/main/kmp/**'
- 'scripts/reel/**'
# The API-name check derives the published pages from the javadoc's own
# sources, and the guide links into the java.* compatibility classes --
@@ -133,7 +134,7 @@ jobs:
- 'Ports/JavaSE/**'
- 'maven/core/**'
- 'maven/javase/**'
- # The Wayline, Savor, Parley and Bastion chapters include these trees by tag, so a
+ # The Wayline, Savor, Parley, Bastion and Kotlin Multiplatform chapters include these trees by tag, so a
# renamed or deleted tag there has to reach the snippet check.
- 'scripts/wayline/**'
- 'scripts/savor/**'
@@ -141,6 +142,7 @@ jobs:
- 'scripts/parley/**'
- 'scripts/reel/**'
- 'scripts/bastion/**'
+ - 'scripts/hellocodenameone/common/src/main/kmp/**'
workflow:
- '.github/workflows/developer-guide-docs.yml'
diff --git a/.github/workflows/health-android.yml b/.github/workflows/health-android.yml
index 3a4c7bca2db..7bf02cee531 100644
--- a/.github/workflows/health-android.yml
+++ b/.github/workflows/health-android.yml
@@ -20,6 +20,7 @@ name: Health (Health Connect) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -45,6 +46,7 @@ name: Health (Health Connect) integration
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/kmp-compat.yml b/.github/workflows/kmp-compat.yml
new file mode 100644
index 00000000000..4353b14bb6e
--- /dev/null
+++ b/.github/workflows/kmp-compat.yml
@@ -0,0 +1,106 @@
+name: Kotlin Multiplatform compatibility
+
+# Compose Multiplatform code built as a Codename One application: the Compose UI
+# layer written over Codename One (maven/compose-compat, which carries the real
+# Compose runtime and kotlinx.coroutines as they are), the headless harness its
+# tests run on (maven/compat-testing) and the build steps that import and
+# package it (build-engine, the Maven and Gradle plugins, the archetype's
+# compose profile).
+#
+# unit tests layout, recomposition, text and input, and the
+# benchmark's Keynote application driven screen by
+# screen, headless; JDK 17, as the Compose runtime's
+# inline functions are JVM 11 bytecode
+# kmp-compat-test.sh every sample under scripts/kmp-compat-samples
+# imported unmodified, built by Maven and Gradle with
+# the Compose compiler, and staged for iOS, Android,
+# JavaScript and the desktop with identical uploads
+# from both tools
+#
+# No display anywhere: the CSS compiler is headless, and so is every test.
+on:
+ pull_request:
+ branches: [ master ]
+ paths:
+ - 'maven/compose-compat/**'
+ - 'maven/compat-testing/**'
+ - 'maven/build-engine/**'
+ - 'maven/codenameone-maven-plugin/**'
+ - 'maven/codenameone-gradle-plugin/**'
+ - 'maven/project-model/**'
+ - 'maven/cn1app-archetype/**'
+ - 'maven/pom.xml'
+ - 'maven/integration-tests/kmp-compat-test.sh'
+ - 'maven/integration-tests/inc/**'
+ - 'scripts/kmp-compat-samples/**'
+ # The unit tests drive the benchmark's shared application.
+ - 'scripts/kmp-bench/app/shared/**'
+ - '.github/workflows/kmp-compat.yml'
+ push:
+ branches: [ master ]
+ paths:
+ - 'maven/compose-compat/**'
+ - 'maven/compat-testing/**'
+ - 'maven/build-engine/**'
+ - 'maven/codenameone-maven-plugin/**'
+ - 'maven/codenameone-gradle-plugin/**'
+ - 'maven/project-model/**'
+ - 'maven/cn1app-archetype/**'
+ - 'maven/pom.xml'
+ - 'maven/integration-tests/kmp-compat-test.sh'
+ - 'maven/integration-tests/inc/**'
+ - 'scripts/kmp-compat-samples/**'
+ - 'scripts/kmp-bench/app/shared/**'
+ - '.github/workflows/kmp-compat.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ kmp-compat:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 60
+ env:
+ CN1_GRADLE_REQUIRED: '1'
+ steps:
+ - uses: actions/checkout@v6
+ # Installed FIRST so the JDK 8 below is the one JAVA_HOME names.
+ - name: Set up JDK 17 for Gradle and the Compose tests
+ uses: actions/setup-java@v5
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - run: echo "GRADLE_JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV
+ - name: Set up JDK 8
+ uses: actions/setup-java@v5
+ with:
+ java-version: '8'
+ distribution: 'temurin'
+ cache: 'maven'
+ - name: Build Codename One Maven artifacts (includes archetypes and both plugins)
+ env:
+ RETRY_ONLY_MATCHING: transient
+ RETRY_ATTEMPTS: '5'
+ RETRY_DELAY_SECONDS: '60'
+ run: |
+ cd maven
+ bash ../scripts/ci/retry.sh mvn install -Plocal-dev-javase -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true
+ - name: Unit tests
+ working-directory: maven
+ run: JAVA_HOME="$GRADLE_JAVA_HOME" mvn -B -Plocal-dev-javase -pl compose-compat test
+ - name: Refresh archetype catalog
+ run: |
+ cd maven
+ mvn archetype:update-local-catalog -Plocal-dev-javase
+ mvn archetype:crawl -Plocal-dev-javase
+ - name: Kotlin Multiplatform samples, imported and built by Maven and Gradle
+ working-directory: maven/integration-tests
+ run: bash kmp-compat-test.sh
+ - name: Upload the test projects' logs
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: kmp-compat-logs
+ path: maven/integration-tests/build/kmp-compat/**/*.log
+ if-no-files-found: ignore
diff --git a/.github/workflows/linux-build-run.yml b/.github/workflows/linux-build-run.yml
index ada0e96fdd6..faf61d9f108 100644
--- a/.github/workflows/linux-build-run.yml
+++ b/.github/workflows/linux-build-run.yml
@@ -37,6 +37,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -76,6 +77,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -162,7 +164,7 @@ jobs:
run: |
cd maven
JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh \
- mvn -B -pl codenameone-maven-plugin,cn1-ads-mock,backend,backend-test -am \
+ mvn -B -pl codenameone-maven-plugin,cn1-ads-mock,compose-compat,backend,backend-test -am \
-DskipTests '-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' install
# After everything that fills the local Maven repository and before the
@@ -233,6 +235,20 @@ jobs:
- name: Install clang and time
run: bash scripts/ci/apt-get-install.sh clang time
+ # The suite's C build compiles ~7,000 translation units, several of them
+ # large, with full debug info. On ubuntu-24.04-arm it exhausted memory and
+ # the runner was shut down mid-build, twice at the same step; extra swap
+ # lets the peak spill instead of taking the machine down.
+ - name: Add swap (arm64)
+ if: matrix.arch == 'arm64'
+ run: |
+ free -m
+ sudo fallocate -l 12G /cn1-swapfile
+ sudo chmod 600 /cn1-swapfile
+ sudo mkswap /cn1-swapfile
+ sudo swapon /cn1-swapfile
+ free -m
+
- name: Set up JDK 8
uses: actions/setup-java@v5
with: { distribution: 'temurin', java-version: '8' }
diff --git a/.github/workflows/parparvm-tests-windows.yml b/.github/workflows/parparvm-tests-windows.yml
index 63893065026..204b568b0a5 100644
--- a/.github/workflows/parparvm-tests-windows.yml
+++ b/.github/workflows/parparvm-tests-windows.yml
@@ -30,6 +30,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -63,6 +64,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -444,7 +446,7 @@ jobs:
Write-Host "mvn failed; retrying in $delay s..."
Start-Sleep -Seconds $delay
}
- mvn -B -pl windows,codenameone-maven-plugin,cn1-ads-mock,backend,backend-test -am -DskipTests '-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' install
+ mvn -B -pl windows,codenameone-maven-plugin,cn1-ads-mock,compose-compat,backend,backend-test -am -DskipTests '-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' install
if ($LASTEXITCODE -eq 0) { $ok = $true; break }
}
if (-not $ok) { throw "mvn install failed after all retries" }
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index c1f925931e2..0efe70b9db7 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -402,7 +402,7 @@ jobs:
-Dcn1.binaries="${CN1_BINARIES}" \
-Pcompile-android \
-DbackendAnalysis \
- -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,swing-compat,javafx-compat,fxml-compiler,desktop-compat-runtime -am verify
+ -pl android,ios,project-model,build-engine,codenameone-maven-plugin,codenameone-gradle-plugin,backend,backend-test,backend-parparvm,javac,android-res-compiler,android-compat,swing-compat,javafx-compat,compose-compat,fxml-compiler,desktop-compat-runtime -am verify
- name: Type-check the SQLCipher-backed database package
if: ${{ matrix.java-version == 8 }}
env:
@@ -620,7 +620,7 @@ jobs:
- name: Generate static analysis HTML summaries
if: ${{ !cancelled() && matrix.java-version == 8 }}
env:
- QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/fxml-compiler/target:maven/desktop-compat-runtime/target
+ QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/compose-compat/target:maven/fxml-compiler/target:maven/desktop-compat-runtime/target
QUALITY_REPORT_SERVER_URL: ${{ github.server_url }}
QUALITY_REPORT_REPOSITORY: ${{ github.repository }}
QUALITY_REPORT_REF: ${{ github.event.pull_request.head.sha || github.sha }}
@@ -644,6 +644,7 @@ jobs:
"android-compat:maven/android-compat/target" \
"swing-compat:maven/swing-compat/target" \
"javafx-compat:maven/javafx-compat/target" \
+ "compose-compat:maven/compose-compat/target" \
"fxml-compiler:maven/fxml-compiler/target" \
"desktop-compat-runtime:maven/desktop-compat-runtime/target" \
"project-model:maven/project-model/target" \
@@ -776,11 +777,11 @@ jobs:
- name: Generate quality report summary
if: ${{ !cancelled() && matrix.java-version == 8 }}
env:
- QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/fxml-compiler/target:maven/desktop-compat-runtime/target
+ QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-engine/target:maven/project-model/target:maven/codenameone-gradle-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-test/target:maven/backend-parparvm/target:maven/javac/target:maven/android-res-compiler/target:maven/android-compat/target:maven/swing-compat/target:maven/javafx-compat/target:maven/compose-compat/target:maven/fxml-compiler/target:maven/desktop-compat-runtime/target
# Every project listed here must produce a SpotBugs report and that
# report must be empty. A missing report fails the gate too, so a
# SpotBugs run that silently stops running is not mistaken for clean.
- QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat:swing-compat:javafx-compat:fxml-compiler:desktop-compat-runtime
+ QUALITY_REPORT_REQUIRED_SPOTBUGS: core-unittests:android:ios:ByteCodeTranslator:codenameone-maven-plugin:build-engine:project-model:codenameone-gradle-plugin:build-hint-catalog:build-hint-tools:backend:backend-test:javac:android-res-compiler:android-compat:swing-compat:javafx-compat:compose-compat:fxml-compiler:desktop-compat-runtime
# PMD and Checkstyle run on the core and on the backend runtime, and each
# must produce its report: without one, the PMD gate below it
# has nothing to read and passes.
diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml
index 7b6986efa41..49a2faa393d 100644
--- a/.github/workflows/scripts-android.yml
+++ b/.github/workflows/scripts-android.yml
@@ -15,6 +15,7 @@ name: Test Android build scripts
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -72,6 +73,7 @@ name: Test Android build scripts
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/scripts-javascript.yml b/.github/workflows/scripts-javascript.yml
index 571c51dd780..65d3d66239a 100644
--- a/.github/workflows/scripts-javascript.yml
+++ b/.github/workflows/scripts-javascript.yml
@@ -20,6 +20,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -76,6 +77,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/scripts-macos.yml b/.github/workflows/scripts-macos.yml
index efd1bdec976..988651ebeec 100644
--- a/.github/workflows/scripts-macos.yml
+++ b/.github/workflows/scripts-macos.yml
@@ -27,6 +27,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -91,6 +92,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
diff --git a/.github/workflows/unity-compat.yml b/.github/workflows/unity-compat.yml
index 81600bdabb4..4ee9a2a25f2 100644
--- a/.github/workflows/unity-compat.yml
+++ b/.github/workflows/unity-compat.yml
@@ -330,7 +330,7 @@ jobs:
run: |
rm -rf ~/.m2/repository/com/codenameone/codenameone-unity-compat ~/.m2/repository/com/codenameone/codenameone-cil-translator
cd maven
- bash ../scripts/ci/retry.sh xvfb-run -a mvn -B install -Plocal-dev-javase -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -pl codenameone-maven-plugin,cn1-ads-mock,android-compat -am
+ bash ../scripts/ci/retry.sh xvfb-run -a mvn -B install -Plocal-dev-javase -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -pl codenameone-maven-plugin,cn1-ads-mock,compose-compat,android-compat -am
test ! -e ~/.m2/repository/com/codenameone/codenameone-unity-compat
- name: Unpack the Unity gallery prebuilt
uses: ./.github/actions/unity-gallery-prebuilt
diff --git a/.github/workflows/unity-gallery-prebuilt.yml b/.github/workflows/unity-gallery-prebuilt.yml
index 29e9859de4b..1ffae407378 100644
--- a/.github/workflows/unity-gallery-prebuilt.yml
+++ b/.github/workflows/unity-gallery-prebuilt.yml
@@ -112,7 +112,7 @@ jobs:
if: steps.pack.outputs.cache-hit != 'true'
run: |
cd maven
- bash ../scripts/ci/retry.sh xvfb-run -a mvn -B install -Plocal-dev-javase -Dunity-compat -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -pl codenameone-maven-plugin,cn1-ads-mock,android-compat,cil-translator,unity-compat -am
+ bash ../scripts/ci/retry.sh xvfb-run -a mvn -B install -Plocal-dev-javase -Dunity-compat -DskipTests -Darchetype.test.skip=true -Dmaven.javadoc.skip=true -pl codenameone-maven-plugin,cn1-ads-mock,compose-compat,android-compat,cil-translator,unity-compat -am
# generate-sources is as far as the application has to go: compile-unity
# is bound to it, and nothing after it writes what the archive carries.
diff --git a/.github/workflows/website-docs.yml b/.github/workflows/website-docs.yml
index 90bc6c9338a..90b782d0671 100644
--- a/.github/workflows/website-docs.yml
+++ b/.github/workflows/website-docs.yml
@@ -67,6 +67,7 @@ on:
- 'scripts/bastion/**'
- 'scripts/skirmish/**'
- 'scripts/parley/**'
+ - 'scripts/hellocodenameone/common/src/main/kmp/**'
- 'scripts/website/**'
- 'scripts/skindesigner/**'
- 'Ports/JavaScriptPort/**'
diff --git a/.github/workflows/windows-cross-build-run.yml b/.github/workflows/windows-cross-build-run.yml
index e5ca2637355..ca002958f13 100644
--- a/.github/workflows/windows-cross-build-run.yml
+++ b/.github/workflows/windows-cross-build-run.yml
@@ -31,6 +31,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -59,6 +60,7 @@ on:
- 'scripts/unity-compat-samples/gallery/**'
- 'maven/cil-translator/**'
- 'maven/unity-compat/**'
+ - 'maven/compose-compat/**'
- 'maven/build-engine/src/main/java/com/codename1/maven/Unity*'
- 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/*Unity*'
- 'scripts/unity-gallery-prebuilt.sh'
@@ -219,7 +221,7 @@ jobs:
run: |
cd maven
JAVA_HOME="$JDK_8_HOME" bash ../scripts/ci/retry.sh \
- mvn -B -pl windows,codenameone-maven-plugin,cn1-ads-mock -am \
+ mvn -B -pl windows,codenameone-maven-plugin,cn1-ads-mock,compose-compat -am \
-DskipTests '-Dmaven.javadoc.skip=true' '-Plocal-dev-javase' install
test -f core/target/classes/com/codename1/ui/Form.class
test -f windows/target/classes/com/codename1/impl/windows/WindowsImplementation.class
diff --git a/CLAUDE.md b/CLAUDE.md
index 8a8978da375..e80f4dd8644 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1321,6 +1321,66 @@ Traps that have already cost a fix:
port turns Space and Enter into the same key code, and drops every
Ctrl/Alt/Meta combination before the form sees it.
+### Kotlin Multiplatform: Compose code as a Codename One app
+
+A KMP module's `commonMain` (and its JVM source set, minus the file that opens a
+desktop window) dropped into `common/src/main/kmp` -- or imported with
+`cn1:import-kmp-project` (`KmpProjectImporter`) -- builds as a Codename One
+application. There is no transpiler: kotlinc compiles it for the JVM, with the
+Compose compiler plugin, and ParparVM translates the classes like any others.
+
+- **The Compose runtime and `kotlinx.coroutines` ship as they are.** Only the
+ UI layer is ours: `maven/compose-compat` is `androidx.compose.{ui,foundation,
+ material3,animation}` written over Codename One components and `Graphics`,
+ authored under the real names. `androidx/compose/` is **never relocated**
+ (`Relocation.isCompose`), not even when the Android layer is active: the
+ runtime the application was compiled against is the one it runs.
+- **Measuring is Compose's, not Codename One's.** `UiNode` is a `Container`
+ whose layout walks the modifier chain outermost first under Compose
+ constraints (`BoxPolicy`, `LinearPolicy`, `LeafPolicy`); the Codename One
+ layout pass only copies the result out. `ComposeHost` runs the `Recomposer`
+ with a `BroadcastFrameClock` fed by `Form.registerAnimated`.
+- **`Dispatchers.Main` is the EDT**, found by kotlinx.coroutines through
+ `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory`
+ (`EdtDispatcherFactory`). The translator compiles that file into a registry;
+ there is no service loader on the device.
+- **JVM 11, not 8.** The Compose runtime's inline functions are JVM 11
+ bytecode, so the archetype's `compose` profile (active when `src/main/kmp`
+ exists) sets kotlinc's `jvmTarget` and javac's source/target to 11. The
+ compiler plugin version must equal the Kotlin version.
+- **Gradle**: the plugin adds compose-compat and the kmp source roots, and
+ refuses the build, naming the line to add, unless
+ `org.jetbrains.kotlin.plugin.compose` is applied; `convert-to-gradle` writes
+ it. The settings plugin adds Google's repository, limited to `androidx.*`.
+
+Traps that have already cost a fix:
+
+- **The runtime's own dependencies are in Gradle metadata only.**
+ `runtime-desktop`'s pom does not name `androidx.collection`, so Maven
+ resolved the runtime without it, every JVM test passed, and the Xcode build
+ failed on a missing `androidx_collection_ScatterSet.h`. compose-compat
+ declares them itself.
+- **A public inline function cannot call internal code**, and the Compose
+ layer's internals are internal: `Box`, `Row` and `Column` are not inline
+ here, as they are upstream.
+- **Tests need JDK 17** (class version 55); on JDK 8 the module builds and
+ the `jdk8-no-tests` profile skips them. SpotBugs still runs on the JDK 8 leg.
+- **`--` ends an XML comment.** A `spotbugs-exclude.xml` with one in a comment
+ is rejected without an error, and every finding comes back.
+- **`LazyColumn`/`LazyRow` compose only what is in view** (`LazyListNode`):
+ every item is a composition of its own under the list's, made within a
+ quarter view of the window and disposed of half a view past it. Sizes of
+ items never measured are estimated from the average, and every pass anchors
+ on the first visible item, so learning a size above the view cannot move
+ what is on screen. `scrollToItem` repeats until the target's real position
+ holds. `reverseLayout = true` throws; it is not implemented.
+
+`maven/integration-tests/kmp-compat-test.sh` imports every sample under
+`scripts/kmp-compat-samples`, builds it with Maven and Gradle and stages every
+target's upload (`.github/workflows/kmp-compat.yml`). `scripts/kmp-bench` runs
+the same Compose source against Compose Multiplatform on an iOS simulator; see
+its README for the numbers and what they do not show.
+
### Integration Tests
Located in `maven/integration-tests/`:
diff --git a/CodenameOne/src/com/codename1/ui/Component.java b/CodenameOne/src/com/codename1/ui/Component.java
index 1e41c8273d6..3b00374f7d1 100644
--- a/CodenameOne/src/com/codename1/ui/Component.java
+++ b/CodenameOne/src/com/codename1/ui/Component.java
@@ -3100,6 +3100,9 @@ final void paintInternal(Graphics g, boolean paintIntersects) {
if (!isVisible() || doNotPaint) {
return;
}
+ // Painted, whether as a root or by a parent: a later region repaint
+ // must not be dropped as already covered by a full one.
+ repaintPending = false;
if (paintLockImage != null) {
if (paintLockImage instanceof Image) {
diff --git a/CodenameOne/src/com/codename1/ui/Container.java b/CodenameOne/src/com/codename1/ui/Container.java
index 7d252c6f81c..8df363a4f09 100644
--- a/CodenameOne/src/com/codename1/ui/Container.java
+++ b/CodenameOne/src/com/codename1/ui/Container.java
@@ -2932,6 +2932,13 @@ private boolean snapToSafeAreaInternal() {
}
boolean changed = false;
+ // The padding set here lives for one measurement: the caller hands it
+ // straight back to TmpInsets.restore, which suppresses its events too.
+ // Announcing it made Component.styleChanged queue a revalidate of the
+ // whole Form from inside its own layout, so every pass queued the next
+ // and a form with a safe-area container never stopped laying out.
+ boolean suppressEvents = style.isSuppressChangeEvents();
+ style.setSuppressChangeEvents(true);
if (newPaddingTop != paddingTop || newPaddingBottom != paddingBottom) {
if (!hasScrollableYParentInternal()) {
changed = true;
@@ -2964,31 +2971,26 @@ private boolean snapToSafeAreaInternal() {
}
}
+ style.setSuppressChangeEvents(suppressEvents);
+ if (changed && !shouldCalcPreferredSize) {
+ // What the suppressed PADDING event did, minus its revalidateLater
+ // of the parent: this and every nested container measure and lay
+ // out again against the snapped padding. Without it a nested
+ // container kept a layout made for the unsnapped one.
+ setShouldCalcPreferredSize(true);
+ }
return changed;
}
- // NOT suppressed, deliberately -- see the history before changing this.
- //
- // The padding written by the safe-area snap is scaffolding: doLayout and
- // calcPreferredSize set it, use it for one measurement, and hand it straight
- // back to TmpInsets.restore, which has always suppressed events. Only the
- // SETTING half announces, and a padding change is exactly what
- // Component.styleChanged answers with revalidateLater() on the parent -- so
- // laying out a safe-area container queues another revalidate of the whole
- // Form, which lays it out again, which queues another. On a busy event
- // dispatch thread that treadmill measured ~200ms per pass against ~15ms of
- // real painting.
- //
- // Suppressing the announcement removes the treadmill and is WRONG here
- // anyway: peer components rely on those repaints. The JavaSE video peer
- // fills its buffer from the AWT side and never asks for a repaint itself
- // (Peer.paint deliberately calls paintOnBuffer rather than cnt.repaint, to
- // avoid a loop), so with nothing else repainting the form its frames never
- // reach the screen -- a video that decoded correctly and displayed nothing.
- //
- // Fixing the treadmill therefore needs the peers to drive their own
- // repaints first; until then the wasted passes are the cheaper defect.
+ // The safe-area snap's padding is suppressed (see snapToSafeAreaInternal).
+ // It was once left announcing because peers seemed to depend on the
+ // resulting treadmill: the JavaSE video peer showed nothing without it. The
+ // real cause was Component.repaint(int, int, int, int) dropping every
+ // region repaint while repaintPending was still set, and that flag was only
+ // cleared when the component was painted as a root -- never when a parent
+ // painted it. Component.paintInternal now clears it too, so a peer's own
+ // region repaints reach the screen without the form laying out each cycle.
void doLayout() {
doLayoutDepth++;
diff --git a/CodenameOne/src/com/codename1/ui/Font.java b/CodenameOne/src/com/codename1/ui/Font.java
index d2251579f8c..15359aa963d 100644
--- a/CodenameOne/src/com/codename1/ui/Font.java
+++ b/CodenameOne/src/com/codename1/ui/Font.java
@@ -153,6 +153,11 @@ public class Font extends CN {
private static final Hashtable bitmapCache = new Hashtable();
private static final HashMap derivedFontCache = new HashMap();
+ /// What [#derive(float, int)] made, per unique id. A hit costs a map lookup
+ /// and a short scan, and allocates nothing: a String key of id, size and
+ /// weight made three objects per call even when the font was already there,
+ /// and a theme load derives every font of every style.
+ private static final HashMap derivedBySize = new HashMap();
/// Internal theme hook: inherit only native UI fonts, never icon or bundled fonts.
public Font deriveNativeThemeFont(String family, float size) {
@@ -183,12 +188,14 @@ public Font deriveNativeThemeFont(String family, float size) {
/// constants instead of returning a stale pre-theme paint.
public static void clearDerivedFontCache() {
derivedFontCache.clear();
+ derivedBySize.clear();
}
private static Font defaultFont = new Font(null);
private static boolean enableBitmapFont = true;
private static float fontReturnedHeight;
private Object font;
private boolean ttf;
+ private int derivedWeight;
private float pixelSize = -1; // for derived fonts only, the size that was requested
private String fontUniqueId;
@@ -603,16 +610,39 @@ public Font deriveLetterSpacing(float letterSpacing) {
public Font derive(float sizePixels, int weight) {
if (fontUniqueId != null) {
// derive should recycle instances of Font to allow smarter caching and logic on the native side of the fence
+ Font[] derived = derivedBySize.get(fontUniqueId);
+ int count = 0;
+ if (derived != null) {
+ int bits = Float.floatToIntBits(sizePixels);
+ for (; count < derived.length && derived[count] != null; count++) {
+ Font d = derived[count];
+ if (d.derivedWeight == weight && Float.floatToIntBits(d.pixelSize) == bits) {
+ return d;
+ }
+ }
+ }
+ // A miss falls back to the shared cache, whose keys createTrueTypeFont
+ // also writes: a derive of a font it made, at that font's own size,
+ // answers that font rather than a second native one, and the reverse.
String key = fontUniqueId + "_" + sizePixels + "_" + weight;
Font f = derivedFontCache.get(key);
- if (f != null) {
- return f;
+ if (f == null) {
+ f = new Font(Display.impl.deriveTrueTypeFont(font, sizePixels, weight));
+ f.pixelSize = sizePixels;
+ f.fontUniqueId = fontUniqueId;
+ f.ttf = true;
+ derivedFontCache.put(key, f);
+ }
+ f.derivedWeight = weight;
+ if (derived == null || count == derived.length) {
+ Font[] grown = new Font[derived == null ? 4 : derived.length * 2];
+ if (derived != null) {
+ System.arraycopy(derived, 0, grown, 0, derived.length);
+ }
+ derived = grown;
+ derivedBySize.put(fontUniqueId, derived);
}
- f = new Font(Display.impl.deriveTrueTypeFont(font, sizePixels, weight));
- f.pixelSize = sizePixels;
- f.fontUniqueId = fontUniqueId;
- f.ttf = true;
- derivedFontCache.put(key, f);
+ derived[count] = f;
return f;
} else {
// not sure if this ever happens but don't want to break that code
diff --git a/CodenameOne/src/com/codename1/ui/geom/Geometry.java b/CodenameOne/src/com/codename1/ui/geom/Geometry.java
index 1bd48848fef..49fff4cf852 100644
--- a/CodenameOne/src/com/codename1/ui/geom/Geometry.java
+++ b/CodenameOne/src/com/codename1/ui/geom/Geometry.java
@@ -649,18 +649,20 @@ public void segment(Rectangle2D rect, List out) {
n() == 3 ? ShapeUtil.intersectCubic(x[0], y[0], x[1], y[1], x[2], y[2], x[3], y[3], rect.getX(), rect.getY(), rect.getX() + rect.getWidth(), rect.getHeight() + rect.getY()) :
n() == 1 ? ShapeUtil.intersectLine(x[0], y[0], x[1], y[1], rect.getX(), rect.getY(), rect.getX() + rect.getWidth(), rect.getY() + rect.getHeight()) :
-1;
- if (numIntersections == -1) {
+ if (n() < 1 || n() > 3) {
throw new IllegalArgumentException("Cannot segment bezier curve of this order: " + n());
}
+ // The intersect functions return a signed crossing count (-1 for a
+ // curve running right to left) or CROSSING, not a number of points,
+ // so only zero is meaningful here.
if (numIntersections == 0) {
out.add(new BezierCurve(this));
return;
}
-
- // To store t values of intersection points
- double[] tvals = new double[numIntersections];
+ // To store t values of intersection points: up to n() per edge.
+ double[] tvals = new double[4 * n()];
int nextTvalIndex = 0;
double[] res = new double[3];
int numMatches = 0;
diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java
index 7321dee6a9a..541f9d89d2d 100644
--- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java
+++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java
@@ -289,7 +289,7 @@ private void restoreNativeBorderColors() {
nativeBorderDerived.clear();
}
- private void recordNativeThemeOwnership(Hashtable incoming) {
+ private void recordNativeThemeOwnership(HashMap incoming) {
if (refreshingTheme) {
return;
}
@@ -303,14 +303,14 @@ private void recordNativeThemeOwnership(Hashtable incoming) {
nativeBaseFontSize = nativeFontSize((Font) base);
}
}
- for (Object item : incoming.keySet()) {
- String key = (String) item;
+ for (Map.Entry e : incoming.entrySet()) {
+ String key = e.getKey();
if (key.startsWith("@")) {
if (!nativeLayer) {
applicationConstants.add(key.substring(1));
}
} else if (nativeLayer) {
- if (incoming.get(key) instanceof Font) {
+ if (e.getValue() instanceof Font) {
nativeFontKeys.add(key);
}
if (key.endsWith("border")) {
@@ -566,7 +566,7 @@ private static Font parseFont(String fontStr) {
/// Resources theme = null;
/// try {
/// theme = Resources.openLayered(resourceFile);
- /// UIManager.getInstance().setThemeProps(theme.getTheme(theme.getThemeResourceNames()[0]));
+ /// UIManager.getInstance().setThemeMap(theme.getThemeMap(theme.getThemeResourceNames()[0]));
/// } catch (IOException e) {
/// Log.e(e);
/// }
@@ -582,7 +582,7 @@ private static Font parseFont(String fontStr) {
public static Resources initFirstTheme(String resourceFile) {
try {
Resources theme = Resources.openLayered(resourceFile);
- UIManager.getInstance().setThemeProps(theme.getTheme(theme.getThemeResourceNames()[0]));
+ UIManager.getInstance().setThemeMap(theme.getThemeMap(theme.getThemeResourceNames()[0]));
Resources.setGlobalResources(theme);
return theme;
} catch (IOException e) {
@@ -605,7 +605,7 @@ public static Resources initFirstTheme(String resourceFile) {
public static Resources initNamedTheme(String resourceFile, String resName) {
try {
Resources theme = Resources.openLayered(resourceFile);
- UIManager.getInstance().setThemeProps(theme.getTheme(resName));
+ UIManager.getInstance().setThemeMap(theme.getThemeMap(resName));
Resources.setGlobalResources(theme);
return theme;
} catch (IOException e) {
@@ -1113,11 +1113,46 @@ HashMap getThemeProps() {
///
/// - `themeProps`: the properties of the given theme
public void setThemeProps(Hashtable themeProps) {
+ setThemePropsFrom(themeMap(themeProps), themeProps);
+ }
+
+ /// Allows manual theme loading from a map of key/value pairs. Prefer this to
+ /// [#setThemeProps(Hashtable)]: a `HashMap` is what the theme is built from,
+ /// so nothing is copied, and its loops compile to direct table walks with
+ /// no iterator and no locking.
+ ///
+ /// #### Parameters
+ ///
+ /// - `themeProps`: the properties of the given theme
+ public void setThemeMap(HashMap themeProps) {
+ setThemePropsFrom(themeProps, themeProps);
+ }
+
+ /// A legacy theme table as the map the theme is built from. Keys that are
+ /// not strings are not theme entries and are left out.
+ private static HashMap themeMap(Hashtable themeProps) {
+ if (themeProps == null) {
+ return null;
+ }
+ HashMap map = new HashMap(themeProps.size() * 2);
+ Enumeration e = themeProps.keys();
+ while (e.hasMoreElements()) {
+ Object key = e.nextElement();
+ if (key instanceof String) {
+ map.put((String) key, themeProps.get(key));
+ }
+ }
+ return map;
+ }
+
+ /// `source` is what the theme listeners are handed: the table the caller
+ /// passed, so a listener written against the legacy API still sees one.
+ private void setThemePropsFrom(HashMap themeProps, Object source) {
// Replacing the whole theme supersedes a deferred base theme: it would be
// reset away here before anything saw it. See setDeferredBaseTheme.
deferredBaseTheme = null;
if (accessible) {
- setThemePropsImpl(themeProps);
+ setThemePropsImpl(themeProps, source);
}
wasThemeInstalled = true;
}
@@ -1129,7 +1164,7 @@ public void setThemeProps(Hashtable themeProps) {
/// - `installedTheme`: @param installedTheme the theme to be installed or null, this is used
/// to check if style inheritance is used in which case we must NOT init
/// style defaults for that particular component
- private void resetThemeProps(Hashtable installedTheme) {
+ private void resetThemeProps(HashMap installedTheme) {
themeProps = new HashMap();
wasThemeInstalled = false;
if (current == null) {
@@ -2064,6 +2099,17 @@ private void resetThemeProps(Hashtable installedTheme) {
///
/// - `themeProps`: the properties of the given theme
public void addThemeProps(Hashtable themeProps) {
+ addThemeMap(themeMap(themeProps));
+ }
+
+ /// Adds the given theme properties on top of the existing properties without
+ /// clearing the existing theme first. Prefer this to [#addThemeProps(Hashtable)],
+ /// which copies the table into a map first.
+ ///
+ /// #### Parameters
+ ///
+ /// - `themeProps`: the properties of the given theme
+ public void addThemeMap(HashMap themeProps) {
// Layered ON the base theme, so a deferred one has to be installed first.
ensureBaseTheme();
if (accessible) {
@@ -2098,17 +2144,11 @@ public void addThemeProps(Hashtable themeProps) {
/// staying in place). For each style key being re-set by the incoming
/// load that does NOT re-assert its binding, drop the matching binding
/// from `themeConstants` so the new literal wins.
- private void dropSupersededBindings(Hashtable themeProps) {
+ private void dropSupersededBindings(HashMap themeProps) {
if (themeProps == null || themeConstants == null || themeConstants.isEmpty()) {
return;
}
- Enumeration e = themeProps.keys();
- while (e.hasMoreElements()) {
- Object keyObj = e.nextElement();
- if (!(keyObj instanceof String)) {
- continue;
- }
- String key = (String) keyObj;
+ for (String key : themeProps.keySet()) {
if (key.startsWith("@")) {
continue;
}
@@ -2232,10 +2272,7 @@ public void refreshTheme() {
}
restoreLargerTextFonts();
restoreNativeThemeValues();
- Hashtable props = new Hashtable();
- for (Map.Entry e : themeProps.entrySet()) {
- props.put(e.getKey(), e.getValue());
- }
+ HashMap props = new HashMap(themeProps);
// buildTheme strips `@`-prefixed constants into themeConstants and
// drops them from the main themeProps map. Round-tripping through
// setThemePropsImpl would therefore lose every constant - so
@@ -2248,7 +2285,7 @@ public void refreshTheme() {
}
refreshingTheme = true;
try {
- setThemePropsImpl(props);
+ setThemePropsImpl(props, props);
} finally {
refreshingTheme = false;
}
@@ -2377,7 +2414,12 @@ public Object getThemeMaskConstant(String constantName) {
return o;
}
+ // for internal use by the resource editor, which installs a theme it is editing
void setThemePropsImpl(Hashtable themeProps) {
+ setThemePropsImpl(themeMap(themeProps), themeProps);
+ }
+
+ void setThemePropsImpl(HashMap themeProps, Object source) {
if (!refreshingTheme && buildThemeDepth == 0) {
restoreNativeBorderColors();
nativeFontKeys.clear();
@@ -2394,7 +2436,7 @@ void setThemePropsImpl(Hashtable themeProps) {
themeGeneration++;
imageCache.clear();
if (themelisteners != null) {
- themelisteners.fireActionEvent(new ActionEvent(themeProps, ActionEvent.Type.Theme));
+ themelisteners.fireActionEvent(new ActionEvent(source, ActionEvent.Type.Theme));
}
buildTheme(themeProps);
breakTitleAreaToolbarDeriveCycle();
@@ -2444,7 +2486,7 @@ private void breakTitleAreaToolbarDeriveCycle() {
}
}
- private void buildTheme(Hashtable themeProps) {
+ private void buildTheme(HashMap themeProps) {
buildThemeDepth++;
try {
buildThemeImpl(themeProps);
@@ -2453,7 +2495,7 @@ private void buildTheme(Hashtable themeProps) {
}
}
- private void buildThemeImpl(Hashtable themeProps) {
+ private void buildThemeImpl(HashMap themeProps) {
// A new theme may change constants that the platform consults while deriving
// fonts (e.g. a native theme's text letter spacing). Flush the derived-font
// cache so those fonts are rebuilt against the incoming constants rather than
@@ -2472,16 +2514,14 @@ private void buildThemeImpl(Hashtable themeProps) {
if (!refreshingTheme && !"true".equals(themeProps.get("@nativeThemeDefaultsBool"))) {
dropSupersededBindings(themeProps);
}
- Enumeration e = themeProps.keys();
- while (e.hasMoreElements()) {
- String key = (String) e.nextElement();
-
+ for (Map.Entry e : themeProps.entrySet()) {
+ String key = e.getKey();
// this is a constant not a theme entry
if (key.startsWith("@")) {
- themeConstants.put(key.substring(1), themeProps.get(key));
+ themeConstants.put(key.substring(1), e.getValue());
continue;
}
- this.themeProps.put(key, themeProps.get(key));
+ this.themeProps.put(key, e.getValue());
}
String overlayThemes = (String) themeProps.get("@OverlayThemes");
@@ -2496,7 +2536,7 @@ private void buildThemeImpl(Hashtable themeProps) {
Resources res = Resources.openLayered("/" + th);
boolean a = accessible;
accessible = true;
- addThemeProps(res.getTheme(res.getThemeResourceNames()[0]));
+ addThemeMap(res.getThemeMap(res.getThemeResourceNames()[0]));
accessible = a;
} catch (Exception ex) {
System.err.println("Failed to load overlay theme file specified by @overlayThemes theme constant: " + th);
@@ -2586,6 +2626,10 @@ private void applyThemeBindings() {
return;
}
final String prefix = "cn1-bind:";
+ // One normalized copy per override: every key bound to `--accent-color`
+ // shares the String it resolves to, where normalizing per key stored 230
+ // equal copies of the same few colors in the theme.
+ HashMap normalized = null;
for (Map.Entry entry : themeConstants.entrySet()) {
String constantKey = entry.getKey();
if (constantKey == null || !constantKey.startsWith(prefix)) {
@@ -2616,9 +2660,17 @@ private void applyThemeBindings() {
}
String overrideValue = (String) override;
if (themeKey.endsWith("Color")) {
- overrideValue = normalizeBoundColorValue(overrideValue);
+ if (normalized == null) {
+ normalized = new HashMap();
+ }
+ String raw = overrideValue;
+ overrideValue = normalized.get(raw);
if (overrideValue == null) {
- continue;
+ overrideValue = normalizeBoundColorValue(raw);
+ if (overrideValue == null) {
+ continue;
+ }
+ normalized.put(raw, overrideValue);
}
}
themeProps.put(themeKey, overrideValue);
@@ -2657,6 +2709,9 @@ private void syncBoundRoundBorderColor(String themeKey, String colorValue, boole
}
}
+ /// Scratch for `normalizeBoundColorValue`, which always produces 6 digits.
+ private static final char[] HEX_COLOR_BUFFER = new char[6];
+
/// `loadTheme` stores color theme entries as plain hex strings (no `#`,
/// lowercase). User-supplied overrides may use either form, so trim a
/// leading `#` and lowercase the value before assigning it to a bound
@@ -2667,29 +2722,34 @@ private static String normalizeBoundColorValue(String raw) {
return null;
}
String value = raw.trim();
- if (value.length() == 0) {
+ int len = value.length();
+ int start = len > 0 && value.charAt(0) == '#' ? 1 : 0;
+ int digits = len - start;
+ if (digits != 3 && digits != 6) {
return null;
}
- if (value.charAt(0) == '#') {
- value = value.substring(1);
- }
- if (value.length() == 3) {
- char r = value.charAt(0);
- char g = value.charAt(1);
- char b = value.charAt(2);
- value = "" + r + r + g + g + b + b;
- }
- if (value.length() != 6) {
- return null;
- }
- for (int i = 0; i < 6; i++) {
- char c = value.charAt(i);
- boolean hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
- if (!hex) {
+ // One pass into a reused buffer: validate, expand the 3-digit form and
+ // fold hex digits by hand, since toLowerCase() is locale sensitive.
+ // Themes are installed from one thread at a time, as the rest of
+ // UIManager assumes.
+ char[] c = HEX_COLOR_BUFFER;
+ boolean changed = start != 0 || digits == 3;
+ for (int i = 0; i < digits; i++) {
+ char x = value.charAt(start + i);
+ if (x >= 'A' && x <= 'F') {
+ x = (char) (x + 32);
+ changed = true;
+ } else if (!((x >= '0' && x <= '9') || (x >= 'a' && x <= 'f'))) {
return null;
}
+ if (digits == 3) {
+ c[2 * i] = x;
+ c[2 * i + 1] = x;
+ } else {
+ c[i] = x;
+ }
}
- return value.toLowerCase();
+ return changed ? new String(c) : value;
}
private Map parseCache() {
diff --git a/CodenameOne/src/com/codename1/ui/util/Resources.java b/CodenameOne/src/com/codename1/ui/util/Resources.java
index 91075fa8a88..19aa2708eac 100644
--- a/CodenameOne/src/com/codename1/ui/util/Resources.java
+++ b/CodenameOne/src/com/codename1/ui/util/Resources.java
@@ -519,7 +519,7 @@ private void openFileImpl(InputStream input) throws IOException {
continue;
case MAGIC_THEME:
case MAGIC_THEME_LEGACY:
- setResource(id, MAGIC_THEME, loadTheme(id, magic == MAGIC_THEME));
+ setResource(id, MAGIC_THEME, loadThemeMap(id, magic == MAGIC_THEME));
continue;
case MAGIC_IMAGE:
Image img = createImage();
@@ -1058,51 +1058,121 @@ public Font getFont(String id) {
///
/// cached theme instance
public Hashtable getTheme(String id) {
- Hashtable h = (Hashtable) resources.get(id);
+ Object stored = resources.get(id);
+ if (stored instanceof HashMap) {
+ // Callers of this method edit the table in place (the CSS compiler, the
+ // resource editor), so the table handed out has to be the stored one.
+ HashMap map = getThemeMap(id);
+ Hashtable h = new Hashtable(map.size() * 2);
+ for (Map.Entry e : map.entrySet()) {
+ Object value = e.getValue();
+ if (value != null) {
+ h.put(e.getKey(), value);
+ }
+ }
+ resources.put(id, h);
+ return h;
+ }
+ Hashtable h = (Hashtable) stored;
// theme can be null in valid use cases such as the resource editor
if (h != null && h.containsKey("uninitialized")) {
Enumeration e = h.keys();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
- if (key.endsWith("font") || (key.endsWith("Image") && !key.endsWith("scaledImage"))) {
- Object value = h.get(key);
- if (value == null) {
- throw new IllegalArgumentException("Couldn't find resource: " + key);
- }
+ Object value = resolveThemeEntry(key, h.get(key));
+ if (value != null) {
+ h.put(key, value);
+ }
+ }
+ h.remove("uninitialized");
+ }
+ return h;
+ }
+
+ /// Returns the theme resource from the file as the map
+ /// [com.codename1.ui.plaf.UIManager#setThemeMap(HashMap)] takes. Prefer this
+ /// to [#getTheme(String)]: the theme is loaded as this map, so nothing is
+ /// copied, and its loops compile to direct table walks with no iterator and
+ /// no locking.
+ ///
+ /// #### Parameters
+ ///
+ /// - `id`: name of the theme resource
+ ///
+ /// #### Returns
+ ///
+ /// cached theme instance
+ public HashMap getThemeMap(String id) {
+ Object stored = resources.get(id);
+ if (stored instanceof Hashtable) {
+ // Set as a table (MutableResource, the resource editor) or handed out
+ // by getTheme: resolve it there, then keep the map from now on.
+ Hashtable h = getTheme(id);
+ HashMap map = new HashMap(h.size() * 2);
+ Enumeration e = h.keys();
+ while (e.hasMoreElements()) {
+ Object key = e.nextElement();
+ if (key instanceof String) {
+ map.put((String) key, h.get(key));
+ }
+ }
+ resources.put(id, map);
+ return map;
+ }
+ if (!(stored instanceof HashMap)) {
+ return null;
+ }
+ HashMap map = (HashMap) stored;
+ if (map.containsKey("uninitialized")) {
+ for (Map.Entry e : map.entrySet()) {
+ Object value = resolveThemeEntry(e.getKey(), e.getValue());
+ if (value != null) {
+ e.setValue(value);
+ }
+ }
+ map.remove("uninitialized");
+ }
+ return map;
+ }
- // the resource was not already loaded when we loaded the theme
- // it must be loaded now so we can resolve the temporary name
- if (value instanceof String) {
- Object o;
+ /// The loaded form of a theme entry that still names a resource loaded after
+ /// the theme, or null when the entry needs nothing.
+ private Object resolveThemeEntry(String key, Object value) {
+ Object result = null;
+ if (key.endsWith("font") || (key.endsWith("Image") && !key.endsWith("scaledImage"))) {
+ if (value == null) {
+ throw new IllegalArgumentException("Couldn't find resource: " + key);
+ }
- // we need to trigger multi image logic in the resource editor
- if (key.endsWith("Image")) {
- o = getImage((String) value);
- } else {
- o = resources.get(value);
- }
- if (o == null) {
- throw new IllegalArgumentException("Theme entry for " + key + " could not be found: " + value);
- }
- h.put(key, o);
- }
+ // the resource was not already loaded when we loaded the theme
+ // it must be loaded now so we can resolve the temporary name
+ if (value instanceof String) {
+ Object o;
+
+ // we need to trigger multi image logic in the resource editor
+ if (key.endsWith("Image")) {
+ o = getImage((String) value);
+ } else {
+ o = resources.get(value);
}
- // if this is a border we might need to do additional work for older versions
- // of Codename One and for the case of an image border where the images might not have
- // been loaded yet when the border was created
- if (key.endsWith("order")) {
- Border b = confirmBorder(h, key);
- h.put(key, b);
+ if (o == null) {
+ throw new IllegalArgumentException("Theme entry for " + key + " could not be found: " + value);
}
+ result = o;
+ value = o;
}
- h.remove("uninitialized");
}
- return h;
+ // if this is a border we might need to do additional work for older versions
+ // of Codename One and for the case of an image border where the images might not have
+ // been loaded yet when the border was created
+ if (key.endsWith("order")) {
+ result = confirmBorder(value);
+ }
+ return result;
}
- private Border confirmBorder(Hashtable h, String key) {
- Object val = h.get(key);
+ private Border confirmBorder(Object val) {
if (val == null) {
return null;
}
@@ -1556,7 +1626,21 @@ private com.codename1.ui.Gradient readGradient(DataInputStream input) throws IOE
}
Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
- Hashtable theme = new Hashtable();
+ HashMap map = loadThemeMap(id, newerVersion);
+ Hashtable theme = new Hashtable(map.size() * 2);
+ for (Map.Entry e : map.entrySet()) {
+ Object value = e.getValue();
+ if (value != null) {
+ theme.put(e.getKey(), value);
+ }
+ }
+ return theme;
+ }
+
+ /// Reads a theme as the map [#getThemeMap(String)] hands out; what a resource
+ /// file load stores. [#loadTheme(String, boolean)] is the same table copied.
+ HashMap loadThemeMap(String id, boolean newerVersion) throws IOException {
+ HashMap theme = new HashMap();
String densityStr = Display.getInstance().getDensityStr();
String platformName = Display.getInstance().getPlatformName();
if ("HTML5".equals(platformName)) {
@@ -1572,14 +1656,15 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// marks the theme as uninitialized so we can finish "wiring" cached resources
theme.put("uninitialized", Boolean.TRUE);
int size = input.readShort();
+ ThemeStrings values = new ThemeStrings(size);
List fontKeys = new ArrayList();
Map fontScaleRules = null;
Map mediaRules = new HashMap();
int defaultFontSizeSetPriority = 0;
for (int iter = 0; iter < size; iter++) {
- String key = input.readUTF();
+ String key = values.readUTF(input);
if (key.startsWith("@")) {
- theme.put(key, input.readUTF());
+ theme.put(key, values.readUTF(input));
if (enableMediaQueries) {
if (key.endsWith("-font-scale")) {
if (fontScaleRules == null) {
@@ -1648,7 +1733,10 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
}
}
- if (mediaMatch) {
+ // A key with no media prefix is its own translation, and a rule for it
+ // never wins over one that matched a prefix. A rule per plain key was
+ // 5,000 objects and map entries of garbage per theme load.
+ if (mediaMatch && matchCount > 0) {
MediaRule rule = mediaRules.get(subkey);
boolean replace = rule == null;
if (!replace && rule.matchCount <= matchCount) {
@@ -1674,7 +1762,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// if this is a simple numeric value
if (key.endsWith("Color")) {
- theme.put(key, Integer.toHexString(input.readInt()));
+ theme.put(key, values.hex(input.readInt()));
continue;
}
@@ -1685,7 +1773,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// if this is a short numeric value for transparency
if (key.endsWith("ransparency")) {
- theme.put(key, "" + (input.readByte() & 0xff));
+ theme.put(key, values.decimal(input.readByte() & 0xff));
continue;
}
@@ -1694,7 +1782,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
continue;
}
if (key.endsWith("opacity")) {
- theme.put(key, "" + (input.readInt() & 0xff));
+ theme.put(key, values.decimal(input.readInt() & 0xff));
continue;
}
@@ -1730,13 +1818,13 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
float p2 = input.readFloat();
float p3 = input.readFloat();
float p4 = input.readFloat();
- theme.put(key, "" + p1 + "," + p2 + "," + p3 + "," + p4);
+ theme.put(key, values.floats(p1, p2, p3, p4));
} else {
int p1 = input.readByte() & 0xff;
int p2 = input.readByte() & 0xff;
int p3 = input.readByte() & 0xff;
int p4 = input.readByte() & 0xff;
- theme.put(key, "" + p1 + "," + p2 + "," + p3 + "," + p4);
+ theme.put(key, values.ints(p1, p2, p3, p4));
}
continue;
}
@@ -1765,7 +1853,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// is this a new font?
if (input.readBoolean()) {
- String fontId = input.readUTF();
+ String fontId = values.readUTF(input);
f = (Font) resources.get(fontId);
// if the font is not yet loaded
@@ -1778,8 +1866,8 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
if (minorVersion > 4) {
boolean hasTTF = input.readBoolean();
if (hasTTF) {
- String fileName = input.readUTF();
- String fontName = input.readUTF();
+ String fileName = values.readUTF(input);
+ String fontName = values.readUTF(input);
int sizeSetting = input.readInt();
float fontSize = input.readFloat();
if (isTrueTypeFileSupported()) {
@@ -1810,7 +1898,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// Tiled Both Image
case MAGIC_INDEXED_IMAGE_LEGACY:
// the image name coupled with the type
- theme.put(key + Style.BG_IMAGE, input.readUTF());
+ theme.put(key + Style.BG_IMAGE, values.readUTF(input));
break;
// Aligned Image
@@ -1820,7 +1908,7 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// Tiled Horizontally Image
case 0xF3:
// the image name coupled with the type and with alignment information
- String imageName = input.readUTF();
+ String imageName = values.readUTF(input);
theme.put(key + Style.BG_IMAGE, imageName);
byte align = input.readByte();
theme.put(key + Style.BACKGROUND_ALIGNMENT, Byte.valueOf(align));
@@ -1858,13 +1946,13 @@ Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
// if this is a background image bgImage
if (key.endsWith("derive")) {
- theme.put(key, input.readUTF());
+ theme.put(key, values.readUTF(input));
continue;
}
// if this is a background image bgImage
if (key.endsWith("bgImage")) {
- String imageId = input.readUTF();
+ String imageId = values.readUTF(input);
Image i = getImage(imageId);
// if the font is not yet loaded
diff --git a/CodenameOne/src/com/codename1/ui/util/ThemeStrings.java b/CodenameOne/src/com/codename1/ui/util/ThemeStrings.java
new file mode 100644
index 00000000000..9ed73190ab4
--- /dev/null
+++ b/CodenameOne/src/com/codename1/ui/util/ThemeStrings.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+
+package com.codename1.ui.util;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/// The strings of one theme load, each stored once.
+///
+/// A theme repeats a few values thousands of times -- `0`, `ffffff`,
+/// `native:MainRegular` -- and reading each with `readUTF`, `toHexString` or
+/// `"" + int` made a byte array, a char array and a String per entry, almost all
+/// of them garbage on arrival. The pages that garbage filled stay resident after
+/// it is collected, so deduplicating the result afterwards saves nothing. This
+/// decodes into reusable buffers and creates a String only for a value it has not
+/// seen in this load; a repeated value allocates nothing.
+final class ThemeStrings {
+ private byte[] bytes = new byte[64];
+ private char[] chars = new char[64];
+ private String[] table;
+ private int size;
+
+ /// `entries` is the entry count the theme declares. Every key is a distinct
+ /// string and the values add a few hundred more, so a table sized for that up
+ /// front never has to grow: growing a 5,000 entry theme from a small table
+ /// discarded four tables on the way.
+ ThemeStrings(int entries) {
+ int want = Math.max(entries, 0) * 2 + 1024;
+ int len = 1024;
+ while (len < want && len < (1 << 20)) {
+ len <<= 1;
+ }
+ table = new String[len];
+ }
+
+ /// Reads a string written by `DataOutputStream.writeUTF` (modified UTF-8).
+ String readUTF(DataInputStream in) throws IOException {
+ int len = in.readUnsignedShort();
+ if (bytes.length < len) {
+ bytes = new byte[len];
+ }
+ in.readFully(bytes, 0, len);
+ if (chars.length < len) {
+ chars = new char[len];
+ }
+ byte[] b = bytes;
+ char[] c = chars;
+ int n = 0;
+ int i = 0;
+ while (i < len) {
+ int x = b[i] & 0xff;
+ if (x < 0x80) {
+ c[n++] = (char) x;
+ i++;
+ } else if ((x & 0xe0) == 0xc0) {
+ if (i + 1 >= len || (b[i + 1] & 0xc0) != 0x80) {
+ throw new IOException("Malformed UTF-8 in theme");
+ }
+ c[n++] = (char) (((x & 0x1f) << 6) | (b[i + 1] & 0x3f));
+ i += 2;
+ } else if ((x & 0xf0) == 0xe0) {
+ if (i + 2 >= len || (b[i + 1] & 0xc0) != 0x80 || (b[i + 2] & 0xc0) != 0x80) {
+ throw new IOException("Malformed UTF-8 in theme");
+ }
+ c[n++] = (char) (((x & 0x0f) << 12) | ((b[i + 1] & 0x3f) << 6) | (b[i + 2] & 0x3f));
+ i += 3;
+ } else {
+ throw new IOException("Malformed UTF-8 in theme");
+ }
+ }
+ return intern(n);
+ }
+
+ /// `Integer.toHexString(v)`.
+ String hex(int v) {
+ char[] c = chars;
+ int n = 0;
+ int shift = 28;
+ while (shift > 0 && ((v >>> shift) & 0xf) == 0) {
+ shift -= 4;
+ }
+ for (; shift >= 0; shift -= 4) {
+ int d = (v >>> shift) & 0xf;
+ c[n++] = (char) (d < 10 ? '0' + d : 'a' + d - 10);
+ }
+ return intern(n);
+ }
+
+ /// `"" + v` for a value from 0 to 255, which is all a theme stores this way.
+ String decimal(int v) {
+ char[] c = chars;
+ int n = 0;
+ if (v >= 100) {
+ c[n++] = (char) ('0' + v / 100);
+ }
+ if (v >= 10) {
+ c[n++] = (char) ('0' + v / 10 % 10);
+ }
+ c[n++] = (char) ('0' + v % 10);
+ return intern(n);
+ }
+
+ /// `"" + a + "," + b + "," + c + "," + d`, the form padding and margin are
+ /// stored in. Each `+` on a float made a String of its own.
+ String floats(float a, float b, float c, float d) {
+ ensureChars(64);
+ int n = appendFloat(a, 0);
+ chars[n++] = ',';
+ n = appendFloat(b, n);
+ chars[n++] = ',';
+ n = appendFloat(c, n);
+ chars[n++] = ',';
+ n = appendFloat(d, n);
+ return intern(n);
+ }
+
+ /// `"" + a + "," + b + "," + c + "," + d` for the integer form older themes use.
+ String ints(int a, int b, int c, int d) {
+ ensureChars(64);
+ int n = appendInt(a, 0);
+ chars[n++] = ',';
+ n = appendInt(b, n);
+ chars[n++] = ',';
+ n = appendInt(c, n);
+ chars[n++] = ',';
+ n = appendInt(d, n);
+ return intern(n);
+ }
+
+ /// Appends `Float.toString(f)`. A theme's values have at most one decimal
+ /// (2.0, 0.5, 48.9); those are written digit by digit, which is what
+ /// `Float.toString` prints for them, and anything else is handed to it.
+ /// The digits are the shortest form that reads back as `f`; a VM whose
+ /// `Float.toString` prints more digits for such a value produced a
+ /// longer string before, never a different float once the theme parses it.
+ private int appendFloat(float f, int n) {
+ float tenths = f * 10f;
+ int t = (int) tenths;
+ if (t == tenths && t > -10000000 && t < 10000000 && (float) (t / 10.0) == f && !(t == 0 && 1 / f < 0)) {
+ if (t < 0) {
+ chars[n++] = '-';
+ t = -t;
+ }
+ n = appendInt(t / 10, n);
+ chars[n++] = '.';
+ chars[n++] = (char) ('0' + t % 10);
+ return n;
+ }
+ String s = Float.toString(f);
+ int len = s.length();
+ ensureChars(n + len + 48);
+ s.getChars(0, len, chars, n);
+ return n + len;
+ }
+
+ private int appendInt(int v, int n) {
+ if (v < 0) {
+ String s = String.valueOf(v);
+ int len = s.length();
+ s.getChars(0, len, chars, n);
+ return n + len;
+ }
+ int start = n;
+ do {
+ chars[n++] = (char) ('0' + v % 10);
+ v /= 10;
+ } while (v != 0);
+ for (int i = start, j = n - 1; i < j; i++, j--) {
+ char x = chars[i];
+ chars[i] = chars[j];
+ chars[j] = x;
+ }
+ return n;
+ }
+
+ private void ensureChars(int n) {
+ if (chars.length < n) {
+ char[] c = new char[n];
+ System.arraycopy(chars, 0, c, 0, chars.length);
+ chars = c;
+ }
+ }
+
+ /// The shared copy of a string built elsewhere.
+ String share(String s) {
+ int n = s.length();
+ if (chars.length < n) {
+ chars = new char[n];
+ }
+ s.getChars(0, n, chars, 0);
+ return intern(n);
+ }
+
+ private String intern(int n) {
+ char[] c = chars;
+ int h = 0;
+ for (int i = 0; i < n; i++) {
+ h = 31 * h + c[i];
+ }
+ String[] t = table;
+ int mask = t.length - 1;
+ int i = (h ^ (h >>> 16)) & mask;
+ while (true) {
+ String s = t[i];
+ if (s == null) {
+ break;
+ }
+ if (s.length() == n && same(s, c, n)) {
+ return s;
+ }
+ i = (i + 1) & mask;
+ }
+ String s = new String(c, 0, n);
+ t[i] = s;
+ size++;
+ if (size * 2 > t.length) {
+ grow();
+ }
+ return s;
+ }
+
+ private static boolean same(String s, char[] c, int n) {
+ for (int i = 0; i < n; i++) {
+ if (s.charAt(i) != c[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void grow() {
+ String[] old = table;
+ String[] t = new String[old.length * 2];
+ int mask = t.length - 1;
+ for (String s : old) {
+ if (s != null) {
+ int h = s.hashCode();
+ int i = (h ^ (h >>> 16)) & mask;
+ while (t[i] != null) {
+ i = (i + 1) & mask;
+ }
+ t[i] = s;
+ }
+ }
+ table = t;
+ }
+}
diff --git a/CodenameOneDesigner/src/com/codename1/designer/PreviewInSimulator.java b/CodenameOneDesigner/src/com/codename1/designer/PreviewInSimulator.java
index 5166642ed75..cd6e593d8f4 100644
--- a/CodenameOneDesigner/src/com/codename1/designer/PreviewInSimulator.java
+++ b/CodenameOneDesigner/src/com/codename1/designer/PreviewInSimulator.java
@@ -144,7 +144,7 @@ public void run() {
com.codename1.ui.Display.getInstance().installNativeTheme();
}
} else {
- com.codename1.ui.plaf.UIManager.getInstance().setThemeProps(res.getTheme(theme));
+ com.codename1.ui.plaf.UIManager.getInstance().setThemeMap(res.getThemeMap(theme));
}
com.codename1.ui.util.UIBuilder.registerCustomComponent("Table", com.codename1.ui.table.Table.class);
com.codename1.ui.util.UIBuilder.registerCustomComponent("MediaPlayer", com.codename1.components.MediaPlayer.class);
diff --git a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java
index 482bcfb99a9..6da49f378ec 100644
--- a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java
+++ b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorApp.java
@@ -833,7 +833,7 @@ private static void migrateGuiBuilder(File projectDir, EditableResources res, St
stateMachineBaseSource.append(" String[] themes = res.getThemeResourceNames();\n");
stateMachineBaseSource.append(" Resources.setGlobalResources(res);\n");
stateMachineBaseSource.append(" if(themes != null && themes.length > 0) {\n");
- stateMachineBaseSource.append(" UIManager.getInstance().setThemeProps(res.getTheme(themes[0]));\n");
+ stateMachineBaseSource.append(" UIManager.getInstance().setThemeMap(res.getThemeMap(themes[0]));\n");
stateMachineBaseSource.append(" }\n");
stateMachineBaseSource.append(" }\n\n\n");
stateMachineBaseSource.append(" public StateMachineBase() {\n instance = this;\n");
diff --git a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java
index 954b9fc0bcf..be8effb58dc 100644
--- a/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java
+++ b/CodenameOneDesigner/src/com/codename1/designer/ResourceEditorView.java
@@ -2491,7 +2491,7 @@ protected com.codename1.ui.Component createComponentInstance(String componentTyp
w.write(" protected void initTheme(Resources res) {\n");
w.write(" String[] themes = res.getThemeResourceNames();\n");
w.write(" if(themes != null && themes.length > 0) {\n");
- w.write(" UIManager.getInstance().setThemeProps(res.getTheme(themes[0]));\n");
+ w.write(" UIManager.getInstance().setThemeMap(res.getThemeMap(themes[0]));\n");
w.write(" }\n");
w.write(" }\n\n");
diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
index 6e41882b06a..33f4b83feb0 100644
--- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
+++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java
@@ -6152,9 +6152,9 @@ public void installNativeTheme() {
is = getResourceAsStream(getClass(), "/android_holo_light.res");
}
Resources r = Resources.open(is);
- Hashtable h = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap h = r.getThemeMap(r.getThemeResourceNames()[0]);
h.put("@commandBehavior", "Native");
- UIManager.getInstance().setThemeProps(h);
+ UIManager.getInstance().setThemeMap(h);
is.close();
Display.getInstance().setCommandBehavior(Display.COMMAND_BEHAVIOR_NATIVE);
} catch (IOException ex) {
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java b/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java
index 9e21723f662..fc195c13955 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/CSSWatcher.java
@@ -672,7 +672,7 @@ public void run() {
try {
System.out.println("CSS File "+compilerArgs.input+" has been updated. Reloading styles from "+fDestFile);
Resources res = Resources.open(new FileInputStream(fDestFile));
- UIManager.getInstance().addThemeProps(res.getTheme(res.getThemeResourceNames()[0]));
+ UIManager.getInstance().addThemeMap(res.getThemeMap(res.getThemeResourceNames()[0]));
Form f = CN.getCurrentForm();
if (f != null) {
f.refreshTheme();
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
index a411dd92c40..d5442b336c3 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/ComponentTreeInspector.java
@@ -742,7 +742,7 @@ public void run() {
public void run() {
try {
Resources res = Resources.open(new FileInputStream(themeFile));
- UIManager.getInstance().addThemeProps(res.getTheme(themeName));
+ UIManager.getInstance().addThemeMap(res.getThemeMap(themeName));
Display.getInstance().getCurrent().refreshTheme();
Display.getInstance().getCurrent().revalidate();
} catch(Exception err) {
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
index ecb051aa5d6..54b7db38f0f 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
@@ -3446,7 +3446,7 @@ public void installNativeTheme() {
if (nativeTheme != null) {
try {
Resources r = Resources.open(nativeTheme);
- Hashtable h = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap h = r.getThemeMap(r.getThemeResourceNames()[0]);
Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class);
boolean desktopSkin = pref.getBoolean("desktopSkin", false);
if(desktopSkin) {
@@ -3455,15 +3455,15 @@ public void installNativeTheme() {
h.remove("@paintsTitleBarBool");
}
injectDesktopThemeConstants(h);
- UIManager.getInstance().setThemeProps(h);
+ UIManager.getInstance().setThemeMap(h);
} catch (IOException ex) {
ex.printStackTrace();
}
} else {
if (nativeThemeRes != null) {
- Hashtable h = nativeThemeRes.getTheme(nativeThemeRes.getThemeResourceNames()[0]);
+ HashMap h = nativeThemeRes.getThemeMap(nativeThemeRes.getThemeResourceNames()[0]);
injectDesktopThemeConstants(h);
- UIManager.getInstance().setThemeProps(h);
+ UIManager.getInstance().setThemeMap(h);
}
}
}
@@ -3474,7 +3474,7 @@ public void installNativeTheme() {
/// them on mobile because this method is a no-op there. Values come from the static config
/// (set by the generated desktop Stub) with a {@code codename1.arg.desktop.*} system-property
/// fallback so build hints work too.
- private void injectDesktopThemeConstants(Hashtable h) {
+ private void injectDesktopThemeConstants(HashMap h) {
if (h == null || !isDesktop()) {
return;
}
diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/ffmpeg/FFMPEGMedia.java b/Ports/JavaSE/src/com/codename1/impl/javase/ffmpeg/FFMPEGMedia.java
index 670ca89804e..d1f517a4f8b 100644
--- a/Ports/JavaSE/src/com/codename1/impl/javase/ffmpeg/FFMPEGMedia.java
+++ b/Ports/JavaSE/src/com/codename1/impl/javase/ffmpeg/FFMPEGMedia.java
@@ -51,6 +51,8 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.FloatControl;
@@ -321,11 +323,14 @@ public Component getVideoComponent() {
}
if (SwingUtilities.isEventDispatchThread()) {
videoComponent = new JavaSEPort.Peer(frame, panel);
+ panel.attach(videoComponent);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
- videoComponent = new JavaSEPort.Peer(frame, panel);
+ JavaSEPort.Peer created = new JavaSEPort.Peer(frame, panel);
+ panel.attach(created);
+ videoComponent = created;
}
});
CN.invokeAndBlock(new Runnable() {
@@ -706,6 +711,8 @@ private static class ProbeData {
private static class VideoPanel extends JPanel implements HierarchyListener {
private volatile BufferedImage image;
private volatile int frameCount;
+ private final AtomicReference peer = new AtomicReference();
+ private final AtomicBoolean framePending = new AtomicBoolean();
VideoPanel(int width, int height) {
setBackground(Color.BLACK);
@@ -730,6 +737,38 @@ void updateFrame(byte[] bytes, int width, int height) {
image = next;
frameCount++;
repaint();
+ showFrameInPeer();
+ }
+
+ void attach(JavaSEPort.Peer p) {
+ peer.set(p);
+ }
+
+ // A new frame repaints this Swing panel, but the Codename One form
+ // draws the peer from a buffer copied out of it, and nothing else
+ // asks for that copy or for the form to repaint. It used to work by
+ // accident: a safe-area form relaid itself out every EDT cycle, which
+ // repainted the peer, which refreshed the buffer. Each frame now
+ // copies the buffer on the AWT thread and repaints the peer on the
+ // EDT itself, coalesced so a fast decoder queues at most one of each.
+ private void showFrameInPeer() {
+ final JavaSEPort.Peer p = peer.get();
+ if (p == null || !framePending.compareAndSet(false, true)) {
+ return;
+ }
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ framePending.set(false);
+ p.paintOnBuffer();
+ CN.callSerially(new Runnable() {
+ @Override
+ public void run() {
+ p.repaint();
+ }
+ });
+ }
+ });
}
boolean hasFrame() {
diff --git a/Ports/JavaSE/src/com/codename1/testing/junit/CodenameOneExtension.java b/Ports/JavaSE/src/com/codename1/testing/junit/CodenameOneExtension.java
index dc231c7c132..a147c3d8584 100644
--- a/Ports/JavaSE/src/com/codename1/testing/junit/CodenameOneExtension.java
+++ b/Ports/JavaSE/src/com/codename1/testing/junit/CodenameOneExtension.java
@@ -39,7 +39,7 @@
import java.awt.GraphicsEnvironment;
import java.lang.reflect.Method;
-import java.util.Hashtable;
+import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -336,8 +336,8 @@ private static void installTheme(String resourcePath, ExtensionContext context)
throw new IllegalStateException(
"Theme resource " + resourcePath + " contains no themes");
}
- Hashtable themeProps = r.getTheme(names[0]);
- UIManager.getInstance().setThemeProps(themeProps);
+ HashMap themeProps = r.getThemeMap(names[0]);
+ UIManager.getInstance().setThemeMap(themeProps);
context.getStore(FONT_STATE).put("restore", restore);
} catch (java.io.IOException | RuntimeException | Error e) {
restore.run();
diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
index 1ad4c6cef0b..48bfe21e6d9 100644
--- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
+++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java
@@ -5566,7 +5566,7 @@ public void installNativeTheme(){
String fallback = isAndroid_() ? "/android_holo_light.res" : "/iOS7Theme.res";
r = Resources.open(fallback);
}
- Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap tp = r.getThemeMap(r.getThemeResourceNames()[0]);
// The browser has no OS status bar / notch, so the app must not
// reserve a status-bar strip at the top of the Form the way iOS
@@ -5593,7 +5593,7 @@ public void installNativeTheme(){
// native menu bar at that moment -- which it does until the HTML chrome exists. Forms
// without a Toolbar then never published their MenuBar to it.
installDesktopChromeIfRequested();
- UIManager.getInstance().setThemeProps(tp);
+ UIManager.getInstance().setThemeMap(tp);
return;
} catch (IOException ex){
Log.e(ex);
diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
index 03baee5b7e5..748cfa23e36 100644
--- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
+++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java
@@ -494,7 +494,7 @@ public void installNativeTheme() {
com.codename1.ui.util.Resources r = com.codename1.ui.util.Resources.open(in);
String[] names = r.getThemeResourceNames();
if (names != null && names.length > 0) {
- com.codename1.ui.plaf.UIManager.getInstance().setThemeProps(r.getTheme(names[0]));
+ com.codename1.ui.plaf.UIManager.getInstance().setThemeMap(r.getThemeMap(names[0]));
}
} catch (Throwable t) {
// A bad/absent theme must not stop the app from starting.
diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
index f74de30fb51..73ab1291934 100644
--- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
+++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java
@@ -382,7 +382,7 @@ public void installNativeTheme() {
com.codename1.ui.util.Resources r = com.codename1.ui.util.Resources.open(in);
String[] names = r.getThemeResourceNames();
if (names != null && names.length > 0) {
- com.codename1.ui.plaf.UIManager.getInstance().setThemeProps(r.getTheme(names[0]));
+ com.codename1.ui.plaf.UIManager.getInstance().setThemeMap(r.getThemeMap(names[0]));
}
} catch (Throwable t) {
// A bad/absent theme must not stop the app from starting.
diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
index cd47ef1d7a5..d728ddb0147 100644
--- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
+++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java
@@ -3165,9 +3165,9 @@ public void installNativeTheme() {
InputStream sub = getResourceAsStream("/" + subclassTheme + ".res");
if (sub != null) {
r = Resources.open(sub);
- Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap tp = r.getThemeMap(r.getThemeResourceNames()[0]);
injectDesktopThemeConstants(tp);
- UIManager.getInstance().setThemeProps(tp);
+ UIManager.getInstance().setThemeMap(tp);
return;
}
// Not in the bundle (a framework build that has not generated it yet):
@@ -3200,9 +3200,9 @@ public void installNativeTheme() {
}
if (in != null) {
r = Resources.open(in);
- Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap tp = r.getThemeMap(r.getThemeResourceNames()[0]);
injectDesktopThemeConstants(tp);
- UIManager.getInstance().setThemeProps(tp);
+ UIManager.getInstance().setThemeMap(tp);
return;
}
// Modern theme isn't in the jar (e.g. framework build hasn't
@@ -3210,19 +3210,19 @@ public void installNativeTheme() {
}
if(mode.equals("ios7") || mode.equals("flat") || mode.equals("auto") || mode.equals("modern") || mode.equals("liquid")) {
r = Resources.open("/iOS7Theme.res");
- Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap tp = r.getThemeMap(r.getThemeResourceNames()[0]);
if(!nativeInstance.isIOS7()) {
tp.put("TitleArea.padding", "0,0,0,0");
}
injectDesktopThemeConstants(tp);
- UIManager.getInstance().setThemeProps(tp);
+ UIManager.getInstance().setThemeMap(tp);
return;
}
// "legacy" / "iphone" / anything else: pre-flat iPhone theme.
r = Resources.open("/iPhoneTheme.res");
- Hashtable tp = r.getTheme(r.getThemeResourceNames()[0]);
+ HashMap tp = r.getThemeMap(r.getThemeResourceNames()[0]);
injectDesktopThemeConstants(tp);
- UIManager.getInstance().setThemeProps(tp);
+ UIManager.getInstance().setThemeMap(tp);
} catch (IOException ex) {
ex.printStackTrace();
}
@@ -3234,7 +3234,7 @@ public void installNativeTheme() {
* native menu bar). These theme constants are injected only on the desktop so iOS
* phones/tablets are unaffected. Mirrors JavaSEPort.injectDesktopThemeConstants.
*/
- private void injectDesktopThemeConstants(Hashtable tp) {
+ private void injectDesktopThemeConstants(HashMap tp) {
if (tp == null || !isDesktop()) {
return;
}
@@ -3272,7 +3272,7 @@ private void injectDesktopThemeConstants(Hashtable tp) {
* system accent is used rather than a colour invented here, and the guard
* leaves any theme that does define the UIID alone.
*/
- private void injectListFocusStyle(Hashtable tp) {
+ private void injectListFocusStyle(HashMap tp) {
// Any entry at all under the UIID means the theme has an opinion; leave it.
for (Object key : tp.keySet()) {
if (key instanceof String && ((String) key).indexOf("ListRendererFocus") > -1) {
diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/kmp/SessionCard.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/kmp/SessionCard.java
new file mode 100644
index 00000000000..80a271b2d9f
--- /dev/null
+++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/kmp/SessionCard.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.developerguide.kmp;
+
+import com.codename1.ui.Button;
+import com.codename1.ui.Container;
+import com.codename1.ui.FontImage;
+import com.codename1.ui.Label;
+import com.codename1.ui.layouts.BorderLayout;
+import com.codename1.ui.layouts.BoxLayout;
+
+// tag::kotlin-multiplatform-interop-session-card[]
+public class SessionCard extends Container {
+ public final Label time = new Label("", "SessionTime");
+ public final Label title = new Label("", "SessionTitle");
+ public final Label speaker = new Label("", "SessionSpeaker");
+ public final Button heart = new Button("", "Favorite");
+
+ public SessionCard(Runnable onFavorite) {
+ super(new BorderLayout(), "SessionCard");
+ add(BorderLayout.CENTER, BoxLayout.encloseY(time, title, speaker));
+ add(BorderLayout.EAST, heart);
+ heart.addActionListener(e -> onFavorite.run());
+ }
+
+ public void bind(String when, String talk, String by, boolean favorite) {
+ time.setText(when);
+ title.setText(talk);
+ speaker.setText(by);
+ FontImage.setMaterialIcon(heart, favorite
+ ? FontImage.MATERIAL_FAVORITE : FontImage.MATERIAL_FAVORITE_BORDER);
+ }
+}
+// end::kotlin-multiplatform-interop-session-card[]
diff --git a/docs/demos/common/src/main/snippets/developer-guide/kotlin-multiplatform-interop.sh b/docs/demos/common/src/main/snippets/developer-guide/kotlin-multiplatform-interop.sh
new file mode 100644
index 00000000000..9941d76fe14
--- /dev/null
+++ b/docs/demos/common/src/main/snippets/developer-guide/kotlin-multiplatform-interop.sh
@@ -0,0 +1,10 @@
+// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline.
+
+// tag::kotlin-multiplatform-interop-bash-001[]
+mvn cn1:import-kmp-project -Dcn1.kmp.import=/path/to/MyKmpApp
+// end::kotlin-multiplatform-interop-bash-001[]
+
+// tag::kotlin-multiplatform-interop-bash-002[]
+mvn cn1:run # the simulator
+mvn cn1:build -Dcodename1.platform=ios # an iOS build on the build server
+// end::kotlin-multiplatform-interop-bash-002[]
diff --git a/docs/demos/common/src/test/java/com/codenameone/developerguide/SessionCardTest.java b/docs/demos/common/src/test/java/com/codenameone/developerguide/SessionCardTest.java
new file mode 100644
index 00000000000..3cdebf8b60d
--- /dev/null
+++ b/docs/demos/common/src/test/java/com/codenameone/developerguide/SessionCardTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.developerguide;
+
+// tag::kotlin-multiplatform-interop-session-card-test[]
+import com.codename1.testing.AbstractTest;
+import com.codenameone.developerguide.kmp.SessionCard;
+
+public class SessionCardTest extends AbstractTest {
+ @Override
+ public boolean runTest() throws Exception {
+ int[] taps = {0};
+ SessionCard card = new SessionCard(() -> taps[0]++);
+ card.bind("09:00 - Hall A", "Keynote", "A. Speaker, Codename One", false);
+
+ assertEqual("Keynote", card.title.getText());
+
+ card.heart.released(); // a tap
+ assertTrue(taps[0] == 1, "the heart reports the tap");
+ return true;
+ }
+}
+// end::kotlin-multiplatform-interop-session-card-test[]
diff --git a/docs/developer-guide/Android-Interop.asciidoc b/docs/developer-guide/Android-Interop.asciidoc
index 4174ed3461e..919c0e371b2 100644
--- a/docs/developer-guide/Android-Interop.asciidoc
+++ b/docs/developer-guide/Android-Interop.asciidoc
@@ -1,3 +1,4 @@
+[[android-interop]]
== Running Android applications
A classic Android application -- activities, XML layouts, `res/values`,
diff --git a/docs/developer-guide/Kotlin-Multiplatform-Interop.asciidoc b/docs/developer-guide/Kotlin-Multiplatform-Interop.asciidoc
new file mode 100644
index 00000000000..25edf36286f
--- /dev/null
+++ b/docs/developer-guide/Kotlin-Multiplatform-Interop.asciidoc
@@ -0,0 +1,335 @@
+[[kmp-interop]]
+== Running Kotlin Multiplatform and Compose applications
+
+A Kotlin Multiplatform (KMP) module whose user interface is written with
+Compose Multiplatform builds as a Codename One application. The shared
+`commonMain` code is compiled unchanged, the composables render as Codename
+One components, and the result ships to every Codename One target: iOS,
+Android, the web, macOS, Windows and Linux (as native binaries with no JVM),
+watchOS and tvOS.
+
+=== Motivation
+
+Kotlin Multiplatform with Compose Multiplatform is the toolkit closest to
+Codename One's own model: one codebase, one declarative UI, logic shared
+between every client. It's a good way to write an application. Codename One
+takes that code further -- onto more platforms, into a smaller and faster
+application, and next to a Java API that every target shares.
+
+Compose Multiplatform runs on Kotlin/Native on iOS and links its own drawing
+engine into the application. Codename One takes the JVM half of a KMP project
+instead. kotlinc already compiles `commonMain` to ordinary class files for
+Android and the desktop. Those classes, together with the real Compose runtime
+and the real `kotlinx.coroutines`, are exactly what Codename One's translator
+reads. Only the UI layer -- `androidx.compose.ui`, `foundation` and
+`material3` -- is replaced, by one written over Codename One components and
+`Graphics`.
+
+The same application, built from the same `commonMain` both ways and measured
+on the same iPhone simulator, with Compose Multiplatform as 100%. The second
+bar is the same screens written in Java against Codename One directly, which
+the next section explains:
+
+image::img/kmp-bench-results.svg["Size, memory, start-up, recomposition and scrolling of the same application built by Codename One from Compose and from Java, relative to Compose Multiplatform",scaledwidth=95%]
+
+- **The same Compose code runs in a quarter of the CPU time.** A state change
+ is drawn in 4.6 ms against 19.2 ms, and one step of scrolling in 4.0 ms
+ against 17.0 ms. Both keep pace with the display, so the difference is
+ headroom: for heavier screens, older devices and battery.
+- **The executable is 27% smaller** (16.8 MB against 22.9 MB) and the bundle
+ 20% smaller, although the Codename One one carries the whole iOS port.
+- **Start-up is faster**, 961 ms against 1008 ms cold and 580 ms against
+ 604 ms warm.
+- **Memory is level**, 47 MB of physical footprint against 48 MB.
+
+The benchmark, its method and how to run it are in `scripts/kmp-bench` in the
+Codename One repository. These are simulator numbers; they show where each
+runtime spends CPU in identical code, not a device's frame rate.
+
+image::img/kmp-keynote-schedule.png["The Keynote schedule screen rendered by Compose Multiplatform (left) and by Codename One (right)",scaledwidth=70%]
+
+Beyond size, the composables become real components, so a Compose screen
+uses the platform's own text input and accessibility, can sit inside a
+Codename One form next to any other component, and reaches every Codename One
+API directly -- push notifications, in-app purchase, the native maps, the
+backend -- with no `expect`/`actual` plumbing per platform.
+
+The same build also reaches targets Compose Multiplatform's UI doesn't run
+on: watchOS, tvOS, and Windows, Linux and macOS as native binaries with no JVM
+to install. The Codename One backend runs the same Kotlin on the server, so a
+data class or validation rule in `commonMain` is shared by the app and the
+service that feeds it.
+
+=== Dropping to Java where it pays
+
+A Compose screen and a Codename One screen are the same kind of object at run
+time -- a component in a form -- so an application doesn't have to choose one
+for all its screens. Kotlin calls every Codename One class directly, and a Java
+class can sit beside the composables in the same module. Compose
+Multiplatform has no such floor: below `commonMain` there is a different
+language and a different UI toolkit on every platform.
+
+The benchmark measures what that floor is worth. `JavaBench.java` in
+`scripts/kmp-bench/app/java` is the Keynote screens written against Codename
+One directly, timed by the same steps. Its list is an `InfiniteContainer`,
+which makes cards a batch at a time as the scroll nears the end -- as
+`LazyColumn` composes only what comes into view -- and rebinds a card it has
+already made instead of building another. It ships no Kotlin standard
+library, no Compose runtime and no coroutines:
+
+- **A state change is drawn in 1.5 ms**, thirteen times faster than Compose
+ Multiplatform's 19.2 ms and three times faster than the Compose build.
+- **A step of scrolling takes 1.8 ms**, against 17.0 ms for Compose
+ Multiplatform.
+- **The executable is 8.9 MB**, 39% of Compose Multiplatform's 22.9 MB, and
+ the bundle is 10.2 MB.
+- **Memory is level** with both, at 47 MB.
+
+The time gap is mostly the price of the reactive model. A composition
+records the state it read, compares what it emits against what it emitted
+last time and applies the difference; an imperative update sets three labels
+on a card that already exists. The size gap is Kotlin: the Java build carries
+neither the Kotlin standard library nor the Compose runtime.
+
+Nor does the declarative form buy readability. Here is one session card both
+ways. The Compose version, from the benchmark:
+
+[source,kotlin]
+----
+include::../../scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSessionCard.kt[tag=kotlin-multiplatform-interop-session-card-compose,indent=0]
+----
+
+And in Codename One, with the look in the theme's CSS where a designer can
+change it:
+
+[source,java]
+----
+include::../demos/common/src/main/java/com/codenameone/developerguide/kmp/SessionCard.java[tag=kotlin-multiplatform-interop-session-card,indent=0]
+----
+
+The Java card says what happens and when: the components are made once,
+`bind` fills them, a tap calls back as `onFavorite` does. Nothing reruns
+behind the code's back, so a breakpoint in `bind` stops exactly when a card
+shows a session -- the question a recomposition count leaves open.
+
+It tests as directly. A component is an ordinary object, so a unit test makes
+one, binds it and presses its button, with no test rule, semantics tree or
+tags kept only for the test:
+
+[source,java,role=cn1-no-try]
+----
+include::../demos/common/src/test/java/com/codenameone/developerguide/SessionCardTest.java[tag=kotlin-multiplatform-interop-session-card-test,indent=0]
+----
+
+The card's components are public final fields, so the test reads them
+directly. It
+runs in the simulator with `mvn cn1:test`, like every other Codename One test.
+
+A hot list, a custom chart or a screen that has to fit a watch is where the
+imperative path pays first; nothing stops a team writing every screen that
+way.
+
+=== One platform API on every target
+
+The `expect`/`actual` declarations a KMP project writes per platform are
+mostly calls Codename One already makes the same everywhere. Composable code
+calls each of them as an ordinary Kotlin function:
+
+- <>, in-app purchase and
+ subscriptions, and <>.
+- <> and <>
+ hardening, as build options rather than per-platform SDKs.
+- <> and
+ <>.
+- Storage, SQLite, networking, the camera, location, sharing and the native
+ maps, with one implementation per platform maintained in Codename One.
+
+The targets go past phones and desktops too: Apple Watch and Wear OS from the
+same project as the phone application, CarPlay and Android Auto, tvOS,
+embedded Linux boards with no X11 and no JVM, the browser, and the
+<<_server_side_backend,server-side backend>>.
+
+=== Quick start
+
+Create a Codename One application (for example at
+https://start.codenameone.com) and import the KMP project into it:
+
+[source,bash]
+----
+include::../demos/common/src/main/snippets/developer-guide/kotlin-multiplatform-interop.sh[tag=kotlin-multiplatform-interop-bash-001,indent=0]
+----
+
+The goal copies the module's `commonMain` and its JVM source set (`jvmMain`,
+else `desktopMain`) into `common/src/main/kmp`, and rewrites the
+application's main class so that it shows the module's root composable. Two
+options cover projects it can't decide alone:
+
+`-Dcn1.kmp.module=composeApp`:: The module to import, when more than one has a
+`src/commonMain`.
+`-Dcn1.kmp.entry=com.example.App`:: The fully qualified root composable, when
+`commonMain` declares no single `@Composable fun App()`.
+
+The Android and iOS source sets aren't copied. They call those platforms
+directly, and Codename One supplies both. A JVM file whose `main` opens a
+desktop window (`application { Window { App() } }`) is left out too: the
+generated main class starts the application instead.
+
+Then build and run as usual:
+
+[source,bash]
+----
+include::../demos/common/src/main/snippets/developer-guide/kotlin-multiplatform-interop.sh[tag=kotlin-multiplatform-interop-bash-002,indent=0]
+----
+
+==== Gradle
+
+A Gradle application needs the Compose compiler plugin,
+`id("org.jetbrains.kotlin.plugin.compose")` at the application's Kotlin
+version, applied next to the Codename One plugin. The build stops with the
+line to add when it's missing.
+
+`convert-to-gradle` writes it for you. The Codename One settings plugin adds
+Google's Maven repository, limited to the `androidx.*` groups, which is where
+the Compose runtime is published.
+
+==== Placing the sources by hand
+
+`common/src/main/kmp/commonMain/kotlin` and `common/src/main/kmp/jvmMain/kotlin`
+are ordinary Kotlin source roots. The archetype's `compose` profile turns on
+whenever `common/src/main/kmp` exists: it adds `codenameone-compose-compat`,
+applies the Compose compiler plugin and sets both kotlinc's `jvmTarget` and
+javac's source and target to 11, because the Compose runtime's inline functions are
+JVM 11 bytecode. The Compose compiler plugin's version must equal the Kotlin
+version, which is why both come from one property.
+
+=== The entry point
+
+A composition is shown in a `ComposeForm`, or embedded anywhere as a
+`ComposeView`, which is an ordinary Codename One `Container`:
+
+[source,kotlin]
+----
+include::../../scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSnippets.kt[tag=kotlin-multiplatform-interop-kotlin-002,indent=0]
+----
+
+Or beside other components:
+
+[source,kotlin]
+----
+include::../../scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSnippets.kt[tag=kotlin-multiplatform-interop-kotlin-003,indent=0]
+----
+
+Both live in `com.codename1.compose`. From Java, a `ComposeView` is created
+by a small Kotlin function that returns one, since a composable lambda can
+only be written in Kotlin.
+
+=== How the build works
+
+image::img/kmp-build-pipeline.svg["KMP sources compiled by kotlinc with the Compose plugin, shipped with the unmodified Compose runtime and coroutines, then translated by ParparVM",scaledwidth=95%]
+
+There is no transpiler. kotlinc compiles the sources for the JVM, with the
+Compose compiler plugin, against `compose-compat`. The application's
+classes, the unmodified Compose runtime, `kotlinx.coroutines` and
+`compose-compat` are then handled like any other Codename One code: the
+bytecode compliance check runs over them, and ParparVM translates them to C
+for iOS, the watch, TV and the native desktop targets. Android runs the
+classes on ART and the simulator on the JVM.
+
+`androidx/compose/` is never relocated, not even when the
+<> is active in the
+same application: the Compose runtime the application was compiled against
+is the one it runs.
+
+=== How a composable becomes components
+
+image::img/kmp-runtime-layers.svg["The application's composables over the unmodified Compose runtime, over compose-compat, over Codename One core and the port",scaledwidth=95%]
+
+Composition, snapshots, `remember`, `LaunchedEffect` and recomposition are
+the real Compose runtime's. What it emits are `UiNode` objects, each a
+Codename One `Container`:
+
+* **Measuring is Compose's.** A node walks its modifier chain outermost first
+ under Compose constraints (`Box`, `Row`/`Column` and leaf policies). The
+ Codename One layout pass only copies the result out, so `weight`,
+ `fillMaxWidth`, `wrapContentSize` and intrinsic padding behave as they do
+ in Compose.
+* **Frames come from the Codename One animation loop.** `ComposeHost` runs
+ the `Recomposer` with a `BroadcastFrameClock` that `Form.registerAnimated`
+ ticks, so `withFrameNanos`, `animateFloatAsState` and `AnimatedVisibility` run
+ in step with every other Codename One animation, and an idle screen costs
+ nothing.
+* **`Dispatchers.Main` is the EDT.** kotlinx.coroutines finds its main
+ dispatcher through a service file. There is no service loader on the
+ device, so the translator compiles that file into a registry at build time.
+* **Lazy lists compose what's in view.** `LazyColumn` and `LazyRow` compose
+ each item as its own composition when it comes within a quarter of a view
+ of the window and dispose of it half a view past it. `scrollToItem` works
+ on items that were never measured.
+* **The system bars are respected.** `TopAppBar`, `CenterAlignedTopAppBar`
+ and `NavigationBar` pad themselves by the display's safe area, read when
+ they're measured, so a notch or a home indicator that the native view
+ reports late still moves them.
+
+=== Supported API
+
+`compose-compat` covers what typical application screens use. Anything else
+is a build error (see below), never a silent no-op.
+
+Layout:: `Box`, `Row`, `Column`, `Spacer`, `LazyColumn`, `LazyRow`,
+`Arrangement`, `Alignment`, `PaddingValues`, `Canvas`.
+Modifiers:: size (`size`, `width`, `height`, `fillMax*`, `wrapContent*`,
+`*In`, `required*`, `defaultMinSize`, `aspectRatio`), `padding`, `offset`,
+`weight`, `background`, `border`, `clip`, `alpha`, `drawBehind`,
+`clickable`, `combinedClickable`, `verticalScroll`, `horizontalScroll`,
+`testTag`.
+Text:: `Text`, `BasicText`, `TextField`, `OutlinedTextField`,
+`BasicTextField`, `TextStyle`, `FontWeight`, the Material 3 type scale.
+Material 3:: `MaterialTheme` and `ColorScheme`, `Scaffold`, `Surface`,
+`TopAppBar`, `CenterAlignedTopAppBar`, `NavigationBar` and
+`NavigationBarItem`, `Card`, `ElevatedCard`, `OutlinedCard`, `Button`,
+`ElevatedButton`, `FilledTonalButton`, `OutlinedButton`, `TextButton`,
+`IconButton`, `FloatingActionButton`, `ExtendedFloatingActionButton`,
+`AssistChip`, `FilterChip`, `Checkbox`, `Switch`, the progress indicators,
+dividers, `Icon` with `Icons.Filled`, `Icons.Outlined` and
+`Icons.AutoMirrored`.
+Animation:: `AnimatedVisibility`, `animateFloatAsState`, `animateDpAsState`, `animateColorAsState`,
+`rememberInfiniteTransition` with `animateFloat`.
+Images:: `Image` with `ImageVector` and `ContentScale`.
+
+`LazyColumn(reverseLayout = true)` throws: it's not implemented.
+
+==== Unsupported API is a build error
+
+The bytecode compliance check runs over the compiled classes. A composable or
+modifier that `compose-compat` doesn't provide is a reference to something
+that doesn't exist, and the build reports it under its original name with
+the source line, before anything is translated. Port such a screen by
+replacing the call, or by writing the missing piece in the application with
+the supported building blocks -- most custom components are a `Box` with a
+`drawBehind`.
+
+=== Testing
+
+Compose screens are tested the way any Codename One screen is: a
+`ComposeView` is a component, so screenshot tests and the component inspector
+see it like any other. The Codename One test application
+(`scripts/hellocodenameone`) carries Compose screens under
+`common/src/main/kmp` -- layout and modifiers, typography and buttons, a
+`Scaffold` over a `LazyColumn` of cards, and state with `LaunchedEffect` and
+`AnimatedVisibility` -- and its `KmpCompat*` screenshot tests compare them on
+every platform the test suite runs on.
+
+=== Troubleshooting
+
+The Compose compiler plugin reports a Kotlin version mismatch:: The plugin
+and kotlinc are different versions. Take both from one property.
+kotlinc or javac reject JVM 11 bytecode or inline functions:: The module is
+compiling below JVM 11. The `compose` profile sets it: check that
+`common/src/main/kmp` exists and that nothing in the pom overrides
+`kotlin.compiler.jvmTarget`.
+The Gradle build stops asking for `org.jetbrains.kotlin.plugin.compose`:: Add
+the plugin line shown in the Gradle section.
+`Incremental compilation failed` after changing the Kotlin version:: Kotlin's
+incremental cache from the old compiler is in `target`. Build once with
+`clean`.
diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc
index 3377bf5ba5d..91fbebd15d6 100644
--- a/docs/developer-guide/developer-guide.asciidoc
+++ b/docs/developer-guide/developer-guide.asciidoc
@@ -286,6 +286,8 @@ include::Unity-Interop.asciidoc[]
include::Desktop-Interop.asciidoc[]
+include::Kotlin-Multiplatform-Interop.asciidoc[]
+
include::Working-with-Mac-OS-X.asciidoc[]
include::Working-with-Mac-Catalyst.asciidoc[]
diff --git a/docs/developer-guide/img/kmp-bench-results.svg b/docs/developer-guide/img/kmp-bench-results.svg
new file mode 100644
index 00000000000..ca32ed2c4cb
--- /dev/null
+++ b/docs/developer-guide/img/kmp-bench-results.svg
@@ -0,0 +1,61 @@
+
diff --git a/docs/developer-guide/img/kmp-build-pipeline.svg b/docs/developer-guide/img/kmp-build-pipeline.svg
new file mode 100644
index 00000000000..2362b903fb1
--- /dev/null
+++ b/docs/developer-guide/img/kmp-build-pipeline.svg
@@ -0,0 +1,77 @@
+
diff --git a/docs/developer-guide/img/kmp-keynote-schedule.png b/docs/developer-guide/img/kmp-keynote-schedule.png
new file mode 100644
index 00000000000..c7bbf1a4be9
Binary files /dev/null and b/docs/developer-guide/img/kmp-keynote-schedule.png differ
diff --git a/docs/developer-guide/img/kmp-runtime-layers.svg b/docs/developer-guide/img/kmp-runtime-layers.svg
new file mode 100644
index 00000000000..1b64b17ff55
--- /dev/null
+++ b/docs/developer-guide/img/kmp-runtime-layers.svg
@@ -0,0 +1,44 @@
+
diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt
index a604b227b00..798f31b7ada 100644
--- a/docs/developer-guide/languagetool-accept.txt
+++ b/docs/developer-guide/languagetool-accept.txt
@@ -858,6 +858,9 @@ blocklist
blocklists
Sepolia
ntfy
+composables
+Skia
+kotlinc
MQTT
PKCS
DTOs?
diff --git a/docs/website/data/port_status.json b/docs/website/data/port_status.json
index 9d3e19373b3..0f9c461132e 100644
--- a/docs/website/data/port_status.json
+++ b/docs/website/data/port_status.json
@@ -152,6 +152,13 @@
"description": "Runs unmodified Swing, SwingX and JavaFX code -- layout managers, the standard controls, tables, trees and lists with custom renderers, Graphics2D and Canvas painting, FXML with controllers and style sheets, and modal dialogs that block their caller -- through the desktop compatibility runtimes, embedded in a Codename One form.",
"tests": ["DesktopCompatSwingControlsScreenshotTest","DesktopCompatSwingTablesScreenshotTest","DesktopCompatSwingTreesScreenshotTest","DesktopCompatSwingPaintingScreenshotTest","DesktopCompatSwingContainersScreenshotTest","DesktopCompatSwingOptionPaneScreenshotTest","DesktopCompatSwingXScreenshotTest","DesktopCompatFxControlsScreenshotTest","DesktopCompatFxDataScreenshotTest","DesktopCompatFxShapesScreenshotTest","DesktopCompatFxFxmlScreenshotTest","DesktopCompatFxPanesScreenshotTest","DesktopCompatFxAlertScreenshotTest"]
},
+ {
+ "id": "kmp-compose-compatibility",
+ "category": "Application runtime",
+ "name": "Kotlin Multiplatform and Compose applications (androidx.compose.*)",
+ "description": "Runs Compose Multiplatform code from a Kotlin Multiplatform commonMain -- Box, Row and Column with modifiers, the Material 3 type scale, buttons, cards, Scaffold with a top app bar over a LazyColumn, state, effects and AnimatedVisibility -- on the unmodified Compose runtime, with every node a Codename One component, embedded in a Codename One form.",
+ "tests": ["KmpCompatLayoutScreenshotTest","KmpCompatTypographyScreenshotTest","KmpCompatListScreenshotTest","KmpCompatStateScreenshotTest"]
+ },
{
"id": "ads",
"category": "Application runtime",
@@ -1695,6 +1702,22 @@
{
"pattern": "DesktopCompatFxAlert",
"test": "DesktopCompatFxAlertScreenshotTest"
+ },
+ {
+ "pattern": "KmpCompatLayout",
+ "test": "KmpCompatLayoutScreenshotTest"
+ },
+ {
+ "pattern": "KmpCompatTypography",
+ "test": "KmpCompatTypographyScreenshotTest"
+ },
+ {
+ "pattern": "KmpCompatList",
+ "test": "KmpCompatListScreenshotTest"
+ },
+ {
+ "pattern": "KmpCompatState",
+ "test": "KmpCompatStateScreenshotTest"
}
]
}
diff --git a/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
index c7b4d3a411a..7f75976bca6 100644
--- a/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
+++ b/maven/build-engine/src/main/java/com/codename1/builders/AndroidGradleBuilder.java
@@ -1844,6 +1844,7 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
extendAppCompatActivity = request.getArg("android.extendAppCompatActivity", "false").equals("true");
// When using gradle 8 we need to strip kotlin files from user classes otherwise we get duplicate class errors
stripKotlinFromUserClasses = useGradle8;
+ gradleProvidedLibraryDependencies = "";
if (!request.getArg("android.java8", "true").equals("true")) {
// Java 6 source level was produced by running retrolambda over the compiled
// classes. Retrolambda is gone (it never supported the JDK 17 that Gradle 8
@@ -2491,6 +2492,7 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
}
if (stripKotlinFromUserClasses) {
stripKotlin(dummyClassesDir);
+ gradleProvidedLibraryDependencies = stripGradleProvidedLibraries(dummyClassesDir);
}
File appDir = buildToolsVersionInt >= 27 ?
@@ -8324,6 +8326,32 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
if (hasKotlinSources && kotlinVersion.length() == 0 && !builtInKotlin) {
kotlinVersion = useGradle8 ? "1.9.22" : "1.7.22";
}
+ // A declared kotlinx-coroutines brings its own kotlin-stdlib (2.1 for coroutines
+ // 1.10), and an older compiler refuses that stdlib's metadata ("compiled with an
+ // incompatible version of Kotlin"), so the compiler is raised to read it. That
+ // includes a pinned version: the Health Connect floor above pins 1.9.x, and no
+ // lower pin can compile against the stdlib the application itself shipped.
+ if (hasKotlinSources && kotlinVersion.length() > 0 && useGradle8
+ && gradleProvidedLibraryDependencies.contains("kotlinx-coroutines-core")) {
+ String comparable = HealthManifestFragments.numericVersionPrefix(kotlinVersion);
+ if (comparable == null
+ || compareVersions(comparable, KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES) < 0) {
+ if (request.getArg("android.topDependency", "").contains("kotlin-gradle-plugin")) {
+ // The app's own kotlin-gradle-plugin line wins over ours, so only
+ // the app can raise the compiler; say so instead of claiming to.
+ log("WARNING: the application ships kotlinx-coroutines, whose kotlin-stdlib"
+ + " needs Kotlin " + KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES
+ + " or newer, but android.topDependency pins its own"
+ + " kotlin-gradle-plugin; raise it if :app:compileDebugKotlin"
+ + " reports an incompatible Kotlin version");
+ } else {
+ log("The application ships kotlinx-coroutines, whose kotlin-stdlib Kotlin "
+ + kotlinVersion + " cannot read; compiling with Kotlin "
+ + KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES + " instead");
+ kotlinVersion = KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES;
+ }
+ }
+ }
String kotlinPluginApply = "";
String kotlinRuntimeDependency = "";
if (hasKotlinSources && kotlinVersion.length() > 0) {
@@ -8677,6 +8705,7 @@ && watchModuleName(request) != null && legacyGplayServicesMode) {
+ " "+compile+" fileTree(dir: 'libs', include: ['*.jar'])\n"
+ request.getArg("android.supportv4Dep",supportV4Default) + "\n"
+ kotlinRuntimeDependency
+ + gradleProvidedLibraryDependencies
+ addNewlineIfMissing(additionalDependencies)
+ addNewlineIfMissing(aiExtraGradleDependencies.toString())
+ addNewlineIfMissing(request.getArg("android.gradleDep", ""))
@@ -12710,6 +12739,67 @@ private boolean hasSourceFileWithExtension(File dir, String extension) {
return false;
}
+ // Libraries a Compose application (src/main/kmp) carries in its own jar that the
+ // Android Gradle graph also supplies: AndroidX itself depends on all three. Left in
+ // userClasses.jar they fail checkDebugDuplicateClasses, so the classes are dropped
+ // and the coordinate is declared instead, at the version compose-compat was built
+ // against; Gradle then resolves the higher of that and what AndroidX asks for, never
+ // an older androidx.collection without the ScatterSet the Compose runtime calls.
+ private static final String[][] GRADLE_PROVIDED_LIBRARIES = {
+ // Raising this also raises KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES.
+ {"kotlinx/coroutines", "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"},
+ // The coroutines jar's debugger frames, outside its package; no coordinate
+ // of its own, the line above already declares the jar.
+ {"_COROUTINE", null},
+ {"androidx/collection", "androidx.collection:collection:1.5.0"},
+ {"androidx/annotation", "androidx.annotation:annotation:1.9.1"}
+ };
+
+ /// The Kotlin compiler a Kotlin app gets by default when the coroutines entry of
+ /// GRADLE_PROVIDED_LIBRARIES is declared: the stdlib that coroutines version depends
+ /// on, or newer. Raise it together with that coordinate.
+ private static final String KOTLIN_FOR_GRADLE_PROVIDED_COROUTINES = "2.1.21";
+
+ private String gradleProvidedLibraryDependencies = "";
+
+ private String stripGradleProvidedLibraries(File dummyClassesDir) {
+ StringBuilder deps = new StringBuilder();
+ for (String[] lib : GRADLE_PROVIDED_LIBRARIES) {
+ File directory = new File(dummyClassesDir, lib[0].replace('/', File.separatorChar));
+ if (directory.isDirectory()) {
+ String coordinate = lib[1];
+ if (lib[0].equals("kotlinx/coroutines")) {
+ // The version the application shipped, so a newer one is never
+ // downgraded; the jar records it in this file.
+ String shipped = readTrimmed(new File(dummyClassesDir,
+ "META-INF" + File.separator + "kotlinx_coroutines_core.version"));
+ if (shipped != null && shipped.length() > 0) {
+ coordinate = coordinate.substring(0, coordinate.lastIndexOf(':') + 1) + shipped;
+ }
+ }
+ delTree(directory, true);
+ if (coordinate == null) {
+ log("Deleted directory " + directory);
+ continue;
+ }
+ log("Deleted directory " + directory + "; declaring " + coordinate + " instead");
+ deps.append(" implementation '").append(coordinate).append("'\n");
+ }
+ }
+ return deps.toString();
+ }
+
+ private static String readTrimmed(File f) {
+ if (!f.isFile()) {
+ return null;
+ }
+ try {
+ return new String(java.nio.file.Files.readAllBytes(f.toPath()), "UTF-8").trim();
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
private void stripKotlin(File dummyClassesDir) {
String[] skipDirectories = new String[] {
"org" + File.separator + "jetbrains" + File.separator + "annotations",
diff --git a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java
index e93312440f8..e20dc09906c 100644
--- a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java
+++ b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java
@@ -1514,6 +1514,14 @@ boolean usesWearable() {
* description.
*/
private boolean usesContactPicker;
+
+ /**
+ * Whether the application reads or writes the address book, decided by
+ * the scan the Android builder asks the same question with. Such an
+ * application gets AddressBook linked and, when it declares none, a
+ * generic {@code NSContactsUsageDescription}.
+ */
+ private final ContactsPermissionScan contactsScan = new ContactsPermissionScan();
private boolean usesCalendarEventApi;
private boolean usesCalendarTaskApi;
private String firstBonjourType;
@@ -2761,6 +2769,7 @@ public void usesClass(String cls) {
&& "com/codename1/contacts/ContactPicker".equals(cls)) {
usesContactPicker = true;
}
+ contactsScan.usesClass(cls);
if (!usesLocalNotifications && cls.indexOf("com/codename1/notifications/LocalNotification") == 0) {
usesLocalNotifications = true;
}
@@ -3127,6 +3136,7 @@ public void usesClassMethod(String cls, String method) {
|| method.indexOf("isContactPickerSupported") > -1)) {
usesContactPicker = true;
}
+ contactsScan.usesClassMethod(cls, method);
// Health.getStore()/getWorkouts() mean a real platform
// store; Health.getSensors() means BLE only. The class
// reference alone cannot tell them apart, so the facade
@@ -3792,6 +3802,17 @@ public void usesClassMethod(String cls, String method) {
}
}
+ boolean usesContactsApi = contactsScan.readPermissionRequired()
+ || contactsScan.writePermissionRequired();
+ if (usesContactsApi) {
+ // iOS terminates an app that opens the address book with no purpose
+ // string, so one the developer did not write is supplied.
+ String contacts = privacyUsageDescriptions.get("NSContactsUsageDescription");
+ if (contacts == null || contacts.trim().length() == 0) {
+ privacyUsageDescriptions.put("NSContactsUsageDescription",
+ "Some functionality of the application requires access to your contacts.");
+ }
+ }
if (xcodeVersion >= 9) {
try {
for (String privacyKey : privacyUsageDescriptions.keySet()) {
@@ -4864,6 +4885,15 @@ public void usesClassMethod(String cls, String method) {
}
}
+ // Linked with the define the AddressBook natives compile under,
+ // INCLUDE_CONTACTS_USAGE, which the contacts usage description turns
+ // on; the scan supplies that description for an app that calls the
+ // contacts API without one.
+ if (contactsScan.readPermissionRequired() || contactsScan.writePermissionRequired()
+ || privacyUsageDescriptions.containsKey("NSContactsUsageDescription")) {
+ addLibs = appendFrameworks(addLibs, "AddressBookUI.framework", "AddressBook.framework");
+ }
+
// LocalAuthentication is required only when the app actually uses
// com.codename1.security.Biometrics / SecureStorage. The scanner
// above sets usesBiometrics if any com/codename1/security/ class
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java b/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java
index 70338d573c7..1a31f14427d 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/GradleConversion.java
@@ -423,6 +423,11 @@ public ProjectLayout convert(File sourceDir, File targetDir, String cn1Version)
// nothing: the app would build, minus its Kotlin classes.
addKotlinPlugin(new File(targetDir, "build.gradle.kts"), true);
}
+ if (KmpProjectImporter.isKmpProject(KmpProjectImporter.kmpDir(targetDir))) {
+ // Compose sources compile only with the Compose compiler, and
+ // the plugin refuses the build without it rather than guess.
+ addComposePlugin(new File(targetDir, "build.gradle.kts"));
+ }
} catch (IOException ex) {
throw new BuildFailureException("Could not convert " + sourceDir + ": " + ex.getMessage(), ex);
}
@@ -1044,6 +1049,26 @@ static void addKotlinPlugin(File buildScript, boolean withVersion) throws IOExce
Files.write(buildScript.toPath(), updated.getBytes(StandardCharsets.UTF_8));
}
+ /// Adds the Compose compiler plugin, at the Kotlin plugin's version (the two
+ /// are released together and must match), to the `plugins {}` block
+ /// [#addKotlinPlugin] wrote.
+ static void addComposePlugin(File buildScript) throws IOException {
+ String text = new String(Files.readAllBytes(buildScript.toPath()), StandardCharsets.UTF_8);
+ if (text.contains(KmpProjectImporter.COMPOSE_GRADLE_PLUGIN)) {
+ return;
+ }
+ int at = text.indexOf("kotlin(\"jvm\")");
+ int eol = at < 0 ? -1 : text.indexOf('\n', at);
+ if (eol < 0) {
+ addKotlinPlugin(buildScript, true);
+ addComposePlugin(buildScript);
+ return;
+ }
+ String line = "\n id(\"" + KmpProjectImporter.COMPOSE_GRADLE_PLUGIN + "\") version \"" + KOTLIN_VERSION + "\"";
+ Files.write(buildScript.toPath(), (text.substring(0, eol) + line + text.substring(eol))
+ .getBytes(StandardCharsets.UTF_8));
+ }
+
/// The iOS app extensions and localized strings, from wherever the source
/// build tool keeps them to the Gradle layout's own directories.
private static void copyIosExtras(ProjectLayout from, ProjectLayout to) throws IOException {
@@ -1077,7 +1102,9 @@ private static void copyPlatformResources(ProjectLayout from, ProjectLayout to)
// Added by the plugin for a project with src/main/unity, at its own
// version, together with the translator of that version: a
// declaration carried over from the pom could name another one.
- "codenameone-unity-compat"));
+ "codenameone-unity-compat",
+ // Likewise for a project with src/main/kmp.
+ "codenameone-compose-compat"));
/// `build.gradle.kts` lines for the dependencies a module's pom declares
/// itself, leaving out the framework, which the plugin adds. A dependency of
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/KmpProjectImporter.java b/maven/build-engine/src/main/java/com/codename1/maven/KmpProjectImporter.java
new file mode 100644
index 00000000000..dd463b36f80
--- /dev/null
+++ b/maven/build-engine/src/main/java/com/codename1/maven/KmpProjectImporter.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.maven;
+
+import com.codename1.builders.BuildException;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/// Imports a Kotlin Multiplatform module into a Codename One application.
+///
+/// The module's `commonMain` and its JVM source set (`jvmMain`, else
+/// `desktopMain`) are copied into `common/src/main/kmp`, which the
+/// archetype's `compose` profile compiles with the Compose compiler. The
+/// Android and iOS source sets are not copied: they call the platforms
+/// directly, and Codename One supplies both platforms itself.
+///
+/// The application's main class is rewritten to show the module's root
+/// composable in a `ComposeForm`. The composable is the one named, or the
+/// only `@Composable fun App()` the common sources declare.
+public final class KmpProjectImporter {
+ /// What an import did.
+ public static final class Result {
+ private File module;
+ private int copiedFiles;
+ private String entry;
+ private File mainClass;
+ private File setAside;
+ private final List skippedSourceSets = new ArrayList();
+ private final List skippedEntryPoints = new ArrayList();
+
+ /// The module whose sources were imported.
+ public File getModule() {
+ return module;
+ }
+
+ /// How many source files were copied.
+ public int getCopiedFiles() {
+ return copiedFiles;
+ }
+
+ /// The fully qualified root composable the application shows.
+ public String getEntry() {
+ return entry;
+ }
+
+ /// The main class the import wrote.
+ public File getMainClass() {
+ return mainClass;
+ }
+
+ /// The previous main class, renamed out of the build, or null.
+ public File getSetAside() {
+ return setAside;
+ }
+
+ /// Platform source sets that were not imported.
+ public List getSkippedSourceSets() {
+ return skippedSourceSets;
+ }
+
+ /// JVM sources left out because they start the desktop application.
+ public List getSkippedEntryPoints() {
+ return skippedEntryPoints;
+ }
+ }
+
+ /// The artifact a project with Kotlin Multiplatform sources depends on.
+ public static final String COMPOSE_ARTIFACT = "codenameone-compose-compat";
+ /// The Kotlin compiler plugin that makes a function composable, applied by
+ /// a Gradle build under this id.
+ public static final String COMPOSE_GRADLE_PLUGIN = "org.jetbrains.kotlin.plugin.compose";
+
+ /// `src/main/kmp` of a common module: where the shared sources are imported.
+ public static File kmpDir(File common) {
+ return new File(common, "src/main/kmp");
+ }
+
+ /// The Kotlin source roots under [#kmpDir] a build compiles.
+ public static File[] kotlinDirs(File kmpDir) {
+ return new File[] {new File(kmpDir, "commonMain/kotlin"), new File(kmpDir, "jvmMain/kotlin")};
+ }
+
+ /// Whether the module has imported Kotlin Multiplatform sources.
+ public static boolean isKmpProject(File kmpDir) {
+ return kmpDir.isDirectory();
+ }
+
+ private static final String[] JVM_SETS = {"jvmMain", "desktopMain"};
+ private static final String[] SKIPPED_SETS = {"androidMain", "iosMain", "appleMain", "nativeMain", "jsMain",
+ "wasmJsMain"};
+ private static final Pattern PACKAGE = Pattern.compile("^\\s*package\\s+([\\w.]+)", Pattern.MULTILINE);
+ private static final Pattern WINDOW = Pattern.compile("^\\s*import\\s+androidx\\.compose\\.ui\\.window\\.",
+ Pattern.MULTILINE);
+ private static final Pattern APP = Pattern.compile("@Composable\\s+(?:public\\s+)?fun\\s+App\\s*\\(\\s*\\)");
+
+ /// The directory of the module to import: `source` itself when it has a
+ /// `src/commonMain`, else its only child that has one, else `module`.
+ public static File moduleDir(File source, String module) throws BuildException {
+ if (module != null && module.length() > 0) {
+ File m = new File(source, module);
+ if (!new File(m, "src/commonMain").isDirectory()) {
+ throw new BuildException("Module " + module + " of " + source + " has no src/commonMain");
+ }
+ return m;
+ }
+ if (new File(source, "src/commonMain").isDirectory()) {
+ return source;
+ }
+ List found = new ArrayList();
+ File[] kids = source.listFiles();
+ if (kids != null) {
+ for (File k : kids) {
+ if (new File(k, "src/commonMain").isDirectory()) {
+ found.add(k);
+ }
+ }
+ }
+ if (found.size() == 1) {
+ return found.get(0);
+ }
+ if (found.isEmpty()) {
+ throw new BuildException("No Kotlin Multiplatform module (a directory with src/commonMain) in " + source);
+ }
+ StringBuilder names = new StringBuilder();
+ for (File f : found) {
+ names.append(' ').append(f.getName());
+ }
+ throw new BuildException("Several modules have a src/commonMain; choose one with -Dcn1.kmp.module:" + names);
+ }
+
+ /// Copies the module's sources into `common` and writes the main class
+ /// `packageName`.`mainName`. `entry` is the fully qualified root
+ /// composable, or null to find `App()`.
+ public Result importProject(File source, String module, File common, String entry, String packageName,
+ String mainName) throws BuildException {
+ if (packageName == null || mainName == null) {
+ throw new BuildException("codename1.packageName and codename1.mainName must be set in "
+ + "codenameone_settings.properties");
+ }
+ Result r = new Result();
+ r.module = moduleDir(source, module);
+ File kmp = kmpDir(common);
+ File commonSrc = new File(r.module, "src/commonMain/kotlin");
+ if (!commonSrc.isDirectory()) {
+ throw new BuildException(r.module + " has no src/commonMain/kotlin");
+ }
+ try {
+ r.copiedFiles += copyTree(commonSrc, new File(kmp, "commonMain/kotlin"));
+ for (String set : JVM_SETS) {
+ File d = new File(r.module, "src/" + set + "/kotlin");
+ if (d.isDirectory()) {
+ r.copiedFiles += copyJvmTree(d, new File(kmp, "jvmMain/kotlin"), r);
+ break;
+ }
+ }
+ for (String set : SKIPPED_SETS) {
+ if (new File(r.module, "src/" + set).isDirectory()) {
+ r.skippedSourceSets.add(set);
+ }
+ }
+ r.entry = entry != null && entry.length() > 0 ? entry : findApp(commonSrc);
+ writeMain(common, packageName, mainName, r);
+ } catch (IOException e) {
+ throw new BuildException("Importing " + r.module + " failed: " + e.getMessage(), e);
+ }
+ return r;
+ }
+
+ private static String findApp(File dir) throws IOException, BuildException {
+ List found = new ArrayList();
+ collectApps(dir, found);
+ if (found.size() == 1) {
+ return found.get(0);
+ }
+ throw new BuildException((found.isEmpty() ? "No" : "More than one") + " @Composable fun App() in the common "
+ + "sources" + (found.isEmpty() ? "" : " (" + found + ")")
+ + "; name the root composable with -Dcn1.kmp.entry=com.example.App");
+ }
+
+ private static void collectApps(File dir, List out) throws IOException {
+ File[] kids = dir.listFiles();
+ if (kids == null) {
+ return;
+ }
+ for (File k : kids) {
+ if (k.isDirectory()) {
+ collectApps(k, out);
+ } else if (k.getName().endsWith(".kt")) {
+ String text = new String(Files.readAllBytes(k.toPath()), StandardCharsets.UTF_8);
+ if (APP.matcher(text).find()) {
+ Matcher p = PACKAGE.matcher(text);
+ out.add(p.find() ? p.group(1) + ".App" : "App");
+ }
+ }
+ }
+ }
+
+ private static void writeMain(File common, String pkg, String name, Result r) throws IOException {
+ String path = pkg.replace('.', '/');
+ File kotlin = new File(common, "src/main/kotlin/" + path + "/" + name + "Compose.kt");
+ kotlin.getParentFile().mkdirs();
+ write(kotlin, "package " + pkg + "\n\n"
+ + "import com.codename1.compose.ComposeForm\n\n"
+ + "/// Shows the imported application's root composable. Called from " + name + ".runApp.\n"
+ + "fun startCompose() {\n"
+ + " ComposeForm { " + r.entry + "() }.show()\n"
+ + "}\n");
+ File java = new File(common, "src/main/java/" + path + "/" + name + ".java");
+ String source = "package " + pkg + ";\n\n"
+ + "import com.codename1.system.Lifecycle;\n\n"
+ + "/// Starts the imported Compose Multiplatform application, whose root\n"
+ + "/// composable " + name + "Compose.kt shows.\n"
+ + "public class " + name + " extends Lifecycle {\n"
+ + " @Override\n"
+ + " public void runApp() {\n"
+ + " " + name + "ComposeKt.startCompose();\n"
+ + " }\n"
+ + "}\n";
+ if (java.isFile()) {
+ String old = new String(Files.readAllBytes(java.toPath()), StandardCharsets.UTF_8);
+ if (!old.equals(source)) {
+ File aside = new File(java.getParentFile(), name + ".java.before-kmp-import");
+ Files.move(java.toPath(), aside.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ r.setAside = aside;
+ }
+ }
+ java.getParentFile().mkdirs();
+ write(java, source);
+ r.mainClass = java;
+ }
+
+ private static void write(File f, String text) throws IOException {
+ Files.write(f.toPath(), text.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /// Like [#copyTree], minus the files that open a desktop window. A desktop
+ /// target's `main` is `application { Window { App() } }`, which is Compose
+ /// for Desktop's windowing API; here the generated main class starts the
+ /// application instead, and that API does not exist.
+ private static int copyJvmTree(File from, File to, Result r) throws IOException {
+ int n = 0;
+ File[] kids = from.listFiles();
+ if (kids == null) {
+ return 0;
+ }
+ to.mkdirs();
+ for (File k : kids) {
+ File dest = new File(to, k.getName());
+ if (k.isDirectory()) {
+ n += copyJvmTree(k, dest, r);
+ } else if (k.getName().endsWith(".kt") && WINDOW.matcher(new String(Files.readAllBytes(k.toPath()),
+ StandardCharsets.UTF_8)).find()) {
+ r.skippedEntryPoints.add(k);
+ } else {
+ Files.copy(k.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ n++;
+ }
+ }
+ return n;
+ }
+
+ private static int copyTree(File from, File to) throws IOException {
+ int n = 0;
+ File[] kids = from.listFiles();
+ if (kids == null) {
+ return 0;
+ }
+ to.mkdirs();
+ for (File k : kids) {
+ File dest = new File(to, k.getName());
+ if (k.isDirectory()) {
+ n += copyTree(k, dest);
+ } else {
+ Files.copy(k.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ n++;
+ }
+ }
+ return n;
+ }
+}
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java
index b45763156c9..d6bac8dcedc 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/Relocation.java
@@ -128,6 +128,9 @@ String shim(String internalName) {
/// The relocated name when `internalName` falls under this layer's bridge
/// package or one of its prefixes, else null.
String relocate(String internalName) {
+ if (isCompose(internalName)) {
+ return null;
+ }
if (runtimePackage != null && internalName.startsWith(runtimePackage)) {
return runtimeTarget + internalName.substring(runtimePackage.length());
}
@@ -143,6 +146,9 @@ String relocate(String internalName) {
/// part of the API this layer provides. Unlike [#relocate] this leaves the
/// bridge package out: that one is the layer's own, never an API name.
boolean owns(String internalName) {
+ if (isCompose(internalName)) {
+ return false;
+ }
for (String p : prefixes) {
if (internalName.startsWith(p)) {
return true;
@@ -155,13 +161,29 @@ boolean owns(String internalName) {
/// this layer's packages (`javax.swing.`), in an import or anywhere else.
boolean namedInSource(String source) {
for (String p : prefixes) {
- if (source.indexOf(p.replace('/', '.')) >= 0) {
- return true;
+ String dotted = p.replace('/', '.');
+ int at = source.indexOf(dotted);
+ while (at >= 0) {
+ if (!source.startsWith(COMPOSE_DOTTED, at)) {
+ return true;
+ }
+ at = source.indexOf(dotted, at + 1);
}
}
return false;
}
+ /// Compose's `androidx.compose` packages belong to the Compose layer, which
+ /// ships them under their own names: the Android layer's `androidx/` prefix
+ /// must neither relocate them nor take an application naming them for an
+ /// Android one.
+ static boolean isCompose(String internalName) {
+ return internalName.startsWith(COMPOSE);
+ }
+
+ static final String COMPOSE = "androidx/compose/";
+ private static final String COMPOSE_DOTTED = "androidx.compose.";
+
/// The name the application was compiled against, when `relocated` is one
/// this layer produced from a prefix, else null. Build messages use it:
/// a developer knows `javax.swing.JTable`, not where it ships.
diff --git a/maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java b/maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java
index 1bbf17282a1..9a1502ae1c7 100644
--- a/maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java
+++ b/maven/build-engine/src/test/java/com/codename1/builders/AndroidDependencyCoordinates.java
@@ -83,10 +83,13 @@ public final class AndroidDependencyCoordinates {
static final Map NOT_SHIPPED = new LinkedHashMap();
static {
+ // GRADLE_PROVIDED_LIBRARIES: a Compose application's copies, declared instead.
+ INJECTED.put("androidx.annotation:annotation", "1.9.1");
INJECTED.put("androidx.appcompat:appcompat", "1.6.1");
INJECTED.put("androidx.browser:browser", "1.8.0");
INJECTED.put("androidx.car.app:app", "1.4.0");
INJECTED.put("androidx.car.app:app-projected", "1.4.0");
+ INJECTED.put("androidx.collection:collection", "1.5.0");
INJECTED.put("androidx.concurrent:concurrent-futures", "1.1.0");
INJECTED.put("androidx.credentials:credentials", "1.3.0");
INJECTED.put("androidx.credentials:credentials-play-services-auth", "1.3.0");
@@ -117,6 +120,7 @@ public final class AndroidDependencyCoordinates {
INJECTED.put("com.huawei.hms:push", "6.3.0.302");
INJECTED.put("com.scottyab:rootbeer-lib", "0.1.2");
INJECTED.put("org.jetbrains.kotlin:kotlin-stdlib", "1.9.22");
+ INJECTED.put("org.jetbrains.kotlinx:kotlinx-coroutines-core", "1.10.2");
INJECTED.put("org.unifiedpush.android:connector", "3.3.5");
NOT_SHIPPED.put("com.android.tools.build:gradle", "the Android Gradle plugin");
diff --git a/maven/build-engine/src/test/java/com/codename1/maven/KmpProjectImporterTest.java b/maven/build-engine/src/test/java/com/codename1/maven/KmpProjectImporterTest.java
new file mode 100644
index 00000000000..9bdf79068e8
--- /dev/null
+++ b/maven/build-engine/src/test/java/com/codename1/maven/KmpProjectImporterTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.maven;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class KmpProjectImporterTest {
+ @TempDir
+ Path tmp;
+
+ private static void write(File f, String text) throws IOException {
+ f.getParentFile().mkdirs();
+ Files.write(f.toPath(), text.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static String read(File f) throws IOException {
+ return new String(Files.readAllBytes(f.toPath()), StandardCharsets.UTF_8);
+ }
+
+ @Test
+ void importsSharedSourcesAndLeavesOutTheDesktopWindow() throws Exception {
+ File source = tmp.resolve("kmp").toFile();
+ File module = new File(source, "composeApp");
+ write(new File(module, "src/commonMain/kotlin/demo/App.kt"),
+ "package demo\n\n@Composable\nfun App() {\n}\n");
+ write(new File(module, "src/desktopMain/kotlin/demo/Main.kt"),
+ "package demo\n\nimport androidx.compose.ui.window.application\n\nfun main() = application { }\n");
+ write(new File(module, "src/desktopMain/kotlin/demo/Platform.kt"), "package demo\n\nval name = \"jvm\"\n");
+ write(new File(module, "src/iosMain/kotlin/demo/Ios.kt"), "package demo\n");
+ File common = tmp.resolve("app/common").toFile();
+ common.mkdirs();
+
+ KmpProjectImporter.Result r = new KmpProjectImporter().importProject(source, null, common, null,
+ "com.acme", "Main");
+
+ assertEquals(module, r.getModule());
+ assertEquals("demo.App", r.getEntry());
+ File kmp = KmpProjectImporter.kmpDir(common);
+ assertTrue(new File(kmp, "commonMain/kotlin/demo/App.kt").isFile());
+ assertTrue(new File(kmp, "jvmMain/kotlin/demo/Platform.kt").isFile());
+ assertFalse(new File(kmp, "jvmMain/kotlin/demo/Main.kt").exists(), "the desktop window was copied");
+ assertEquals(1, r.getSkippedEntryPoints().size());
+ assertTrue(r.getSkippedSourceSets().contains("iosMain"));
+ assertTrue(read(r.getMainClass()).contains("extends Lifecycle"));
+ }
+
+ @Test
+ void conversionAppliesTheComposeCompilerAtTheKotlinVersion() throws Exception {
+ File script = tmp.resolve("build.gradle.kts").toFile();
+ write(script, "// header\n\ndependencies {\n}\n");
+ GradleConversion.addKotlinPlugin(script, true);
+ GradleConversion.addComposePlugin(script);
+ GradleConversion.addComposePlugin(script);
+ String text = read(script);
+ assertTrue(text.contains("plugins {\n kotlin(\"jvm\") version \"" + GradleConversion.KOTLIN_VERSION
+ + "\"\n id(\"org.jetbrains.kotlin.plugin.compose\") version \""
+ + GradleConversion.KOTLIN_VERSION + "\"\n}"), text);
+ assertEquals(text.indexOf("plugin.compose"), text.lastIndexOf("plugin.compose"), "added twice");
+ }
+}
diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml
index 58cb693c8e3..19380f4dfe6 100644
--- a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml
+++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/pom.xml
@@ -143,6 +143,68 @@
+
+
+ compose
+
+
+ ${basedir}/src/main/kmp
+
+
+
+
+ 11
+
+ 23.0.0
+
+
+
+
+ google
+ https://maven.google.com
+
+
+
+
+ com.codenameone
+ codenameone-compose-compat
+
+
+
+
+
+
+ maven-compiler-plugin
+
+ 11
+ 11
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-compose-compiler-plugin-embeddable
+ ${kotlin.version}
+
+
+
+
+
+ ${project.basedir}/src/main/desktop/kotlin${project.basedir}/src/main/desktop/java
+
+ ${project.basedir}/src/main/kmp/commonMain/kotlin
+ ${project.basedir}/src/main/kmp/jvmMain/kotlin-no-reflect
diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/pom.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/pom.xml
index f74281b64f4..68b2db3fe01 100644
--- a/maven/cn1app-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/pom.xml
@@ -28,6 +28,9 @@
${cn1PluginVersion}${cn1Version}UTF-8
+
+ 13.01.811
@@ -83,6 +86,11 @@
${cn1.version}provided
+
+ com.codenameone
+ codenameone-compose-compat
+ ${cn1.version}
+
+ 2.2.10
+ 1.12.1
+
+ 1.10.2
+
+
+
+
+
+ google
+ https://maven.google.com
+
+
+
+
+
+ com.codenameone
+ codenameone-core
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ androidx.compose.runtime
+ runtime-desktop
+ ${compose.runtime.version}
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+
+
+
+
+
+ androidx.compose.runtime
+ runtime-annotation-jvm
+ ${compose.runtime.version}
+
+
+ androidx.collection
+ collection-jvm
+ 1.5.0
+
+
+ androidx.annotation
+ annotation-jvm
+ 1.9.1
+
+
+
+ org.jetbrains
+ annotations
+ 23.0.0
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core-jvm
+ ${coroutines.version}
+
+
+ junit
+ junit
+ test
+
+
+ com.codenameone
+ codenameone-compat-testing
+ ${project.version}
+ test
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ 4.7.3.4
+
+ Max
+ Low
+ false
+ true
+ ${project.build.directory}
+ ${project.basedir}/spotbugs-exclude.xml
+
+
+
+ spotbugs
+ verify
+
+ spotbugs
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.4.0
+
+
+ hero-app
+ generate-test-sources
+
+ add-test-source
+
+
+
+ ${project.basedir}/../../scripts/kmp-bench/app/shared/commonMain/kotlin
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ 11
+
+ -no-reflect
+
+
+
+
+ compile
+
+ compile
+
+
+
+ test-compile
+
+ test-compile
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-compose-compiler-plugin-embeddable
+ ${kotlin.version}
+
+
+
+
+
+
+
+
+ jdk8-no-tests
+
+ 1.8
+
+
+ true
+
+
+
+
diff --git a/maven/compose-compat/spotbugs-exclude.xml b/maven/compose-compat/spotbugs-exclude.xml
new file mode 100644
index 00000000000..41eec971d31
--- /dev/null
+++ b/maven/compose-compat/spotbugs-exclude.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/animation/AnimatedVisibility.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/animation/AnimatedVisibility.kt
new file mode 100644
index 00000000000..d3349c78bad
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/animation/AnimatedVisibility.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.animation
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+
+/// How content appears. Every transition fades; sizes and slides fade too.
+class EnterTransition internal constructor(internal val millis: Int) {
+ operator fun plus(other: EnterTransition): EnterTransition = EnterTransition(Math.max(millis, other.millis))
+}
+
+class ExitTransition internal constructor(internal val millis: Int) {
+ operator fun plus(other: ExitTransition): ExitTransition = ExitTransition(Math.max(millis, other.millis))
+}
+
+fun fadeIn(animationSpec: AnimationSpec? = null): EnterTransition = EnterTransition(220)
+fun fadeOut(animationSpec: AnimationSpec? = null): ExitTransition = ExitTransition(220)
+fun expandVertically(): EnterTransition = EnterTransition(220)
+fun shrinkVertically(): ExitTransition = ExitTransition(220)
+fun slideInVertically(initialOffsetY: (Int) -> Int = { -it / 2 }): EnterTransition = EnterTransition(220)
+fun slideOutVertically(targetOffsetY: (Int) -> Int = { -it / 2 }): ExitTransition = ExitTransition(220)
+fun scaleIn(): EnterTransition = EnterTransition(220)
+fun scaleOut(): ExitTransition = ExitTransition(220)
+
+@Composable
+fun AnimatedVisibility(
+ visible: Boolean,
+ modifier: Modifier = Modifier,
+ enter: EnterTransition = fadeIn() + expandVertically(),
+ exit: ExitTransition = fadeOut() + shrinkVertically(),
+ label: String = "AnimatedVisibility",
+ content: @Composable () -> Unit
+) {
+ val alpha by animateFloatAsState(if (visible) 1f else 0f, tween(if (visible) enter.millis else exit.millis))
+ if (visible || alpha > 0f) {
+ Box(modifier.alpha(alpha)) {
+ content()
+ }
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/animation/core/Animation.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/animation/core/Animation.kt
new file mode 100644
index 00000000000..77ce654cfda
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/animation/core/Animation.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.animation.core
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+
+/// Maps a fraction of an animation's time to a fraction of its distance.
+@Immutable
+fun interface Easing {
+ fun transform(fraction: Float): Float
+}
+
+/// A cubic bezier from (0, 0) to (1, 1) through two control points, as CSS
+/// defines `cubic-bezier`.
+@Immutable
+class CubicBezierEasing(private val a: Float, private val b: Float, private val c: Float, private val d: Float) : Easing {
+ private fun bezier(t: Float, p1: Float, p2: Float): Float {
+ val u = 1 - t
+ return 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t
+ }
+
+ override fun transform(fraction: Float): Float {
+ if (fraction <= 0f) return 0f
+ if (fraction >= 1f) return 1f
+ var lo = 0f
+ var hi = 1f
+ var t = fraction
+ for (i in 0 until 20) {
+ val x = bezier(t, a, c)
+ if (Math.abs(x - fraction) < 0.0005f) break
+ if (x < fraction) lo = t else hi = t
+ t = (lo + hi) / 2
+ }
+ return bezier(t, b, d)
+ }
+}
+
+val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
+val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
+val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
+val LinearEasing: Easing = Easing { it }
+val EaseInOut: Easing = CubicBezierEasing(0.42f, 0.0f, 0.58f, 1.0f)
+
+/// How a value moves to its target.
+interface AnimationSpec
+
+/// Answers the value at a time for an animation of a float from `from` to `to`.
+internal interface FloatSpec {
+ /// The value at `playTimeNanos`, or null once the animation is over.
+ fun valueAt(playTimeNanos: Long, from: Float, to: Float, velocity: FloatArray): Float?
+}
+
+@Immutable
+class TweenSpec(val durationMillis: Int = DefaultDurationMillis, val delay: Int = 0, val easing: Easing = FastOutSlowInEasing) :
+ AnimationSpec, FloatSpec {
+ override fun valueAt(playTimeNanos: Long, from: Float, to: Float, velocity: FloatArray): Float? {
+ val ms = playTimeNanos / 1_000_000L - delay
+ if (ms < 0) return from
+ if (ms >= durationMillis) return null
+ val f = easing.transform(ms.toFloat() / Math.max(1, durationMillis))
+ return from + (to - from) * f
+ }
+}
+
+/// A damped spring. Integrated per frame from the velocity it carries.
+@Immutable
+class SpringSpec(val dampingRatio: Float = Spring.DampingRatioNoBouncy, val stiffness: Float = Spring.StiffnessMedium) :
+ AnimationSpec, FloatSpec {
+ override fun valueAt(playTimeNanos: Long, from: Float, to: Float, velocity: FloatArray): Float? {
+ // velocity[0] = current value, velocity[1] = velocity, velocity[2] = last time.
+ if (velocity[2] < 0f) {
+ velocity[0] = from
+ velocity[1] = 0f
+ velocity[2] = 0f
+ }
+ val t = playTimeNanos / 1e9f
+ var dt = t - velocity[2]
+ velocity[2] = t
+ val omega = Math.sqrt(stiffness.toDouble()).toFloat()
+ var x = velocity[0]
+ var v = velocity[1]
+ while (dt > 0f) {
+ val step = Math.min(dt, 1f / 240f)
+ val a = -stiffness * (x - to) - 2f * dampingRatio * omega * v
+ v += a * step
+ x += v * step
+ dt -= step
+ }
+ velocity[0] = x
+ velocity[1] = v
+ val range = Math.max(Math.abs(to - from), 1f)
+ if (Math.abs(x - to) < range * 0.001f && Math.abs(v) < range * 0.01f) return null
+ return x
+ }
+}
+
+object Spring {
+ const val DampingRatioHighBouncy: Float = 0.2f
+ const val DampingRatioMediumBouncy: Float = 0.5f
+ const val DampingRatioLowBouncy: Float = 0.75f
+ const val DampingRatioNoBouncy: Float = 1f
+ const val StiffnessHigh: Float = 10_000f
+ const val StiffnessMedium: Float = 1500f
+ const val StiffnessMediumLow: Float = 400f
+ const val StiffnessLow: Float = 200f
+ const val StiffnessVeryLow: Float = 50f
+}
+
+const val DefaultDurationMillis: Int = 300
+
+fun tween(durationMillis: Int = DefaultDurationMillis, delayMillis: Int = 0, easing: Easing = FastOutSlowInEasing): TweenSpec =
+ TweenSpec(durationMillis, delayMillis, easing)
+
+fun spring(dampingRatio: Float = Spring.DampingRatioNoBouncy, stiffness: Float = Spring.StiffnessMedium,
+ visibilityThreshold: T? = null): SpringSpec = SpringSpec(dampingRatio, stiffness)
+
+enum class RepeatMode { Restart, Reverse }
+
+@Immutable
+class InfiniteRepeatableSpec(val animation: TweenSpec, val repeatMode: RepeatMode) : AnimationSpec
+
+fun infiniteRepeatable(animation: TweenSpec, repeatMode: RepeatMode = RepeatMode.Restart): InfiniteRepeatableSpec =
+ InfiniteRepeatableSpec(animation, repeatMode)
+
+/// A float that animates to targets on the frame clock.
+class Animatable(initialValue: Float) {
+ private var current by mutableFloatStateOf(initialValue)
+ private var running by mutableStateOf(false)
+ var targetValue: Float = initialValue
+ private set
+
+ val value: Float get() = current
+ val isRunning: Boolean get() = running
+
+ suspend fun snapTo(targetValue: Float) {
+ this.targetValue = targetValue
+ current = targetValue
+ }
+
+ suspend fun animateTo(targetValue: Float, animationSpec: AnimationSpec = spring()) {
+ this.targetValue = targetValue
+ val spec = (animationSpec as? FloatSpec) ?: SpringSpec()
+ val from = current
+ if (from == targetValue) return
+ val state = floatArrayOf(0f, 0f, -1f)
+ running = true
+ try {
+ var start = -1L
+ while (true) {
+ val done = withFrameNanos { now ->
+ if (start < 0) start = now
+ val v = spec.valueAt(now - start, from, targetValue, state)
+ if (v == null) {
+ current = targetValue
+ true
+ } else {
+ current = v
+ false
+ }
+ }
+ if (done) break
+ }
+ } finally {
+ running = false
+ }
+ }
+
+ fun asState(): State = object : State {
+ override val value: Float get() = current
+ }
+}
+
+@Composable
+fun animateFloatAsState(targetValue: Float, animationSpec: AnimationSpec = spring(), label: String = "FloatAnimation",
+ finishedListener: ((Float) -> Unit)? = null): State {
+ val anim = remember { Animatable(targetValue) }
+ LaunchedEffect(targetValue) {
+ anim.animateTo(targetValue, animationSpec)
+ finishedListener?.invoke(targetValue)
+ }
+ return anim.asState()
+}
+
+@Composable
+fun animateDpAsState(targetValue: Dp, animationSpec: AnimationSpec = spring(), label: String = "DpAnimation"): State {
+ @Suppress("UNCHECKED_CAST")
+ val f = animateFloatAsState(targetValue.value, animationSpec as AnimationSpec, label)
+ return remember(f) { object : State { override val value: Dp get() = Dp(f.value) } }
+}
+
+/// Runs animations that never end: the values a [rememberInfiniteTransition]
+/// produces.
+class InfiniteTransition internal constructor() {
+ internal var startNanos by mutableStateOf(-1L)
+ internal var nowNanos by mutableStateOf(0L)
+}
+
+@Composable
+fun rememberInfiniteTransition(label: String = "InfiniteTransition"): InfiniteTransition {
+ val t = remember { InfiniteTransition() }
+ LaunchedEffect(t) {
+ while (true) {
+ withFrameNanos { now ->
+ if (t.startNanos < 0) t.startNanos = now
+ t.nowNanos = now - t.startNanos
+ }
+ }
+ }
+ return t
+}
+
+@Composable
+fun InfiniteTransition.animateFloat(initialValue: Float, targetValue: Float, animationSpec: InfiniteRepeatableSpec,
+ label: String = "FloatAnimation"): State {
+ val t = this
+ return remember(initialValue, targetValue, animationSpec) {
+ object : State {
+ override val value: Float
+ get() {
+ val spec = animationSpec.animation
+ val period = (spec.durationMillis + spec.delay).toLong() * 1_000_000L
+ if (period <= 0) return targetValue
+ val cycle = t.nowNanos / period
+ val inCycle = t.nowNanos % period
+ val reverse = animationSpec.repeatMode == RepeatMode.Reverse && cycle % 2 == 1L
+ val v = spec.valueAt(inCycle, initialValue, targetValue, floatArrayOf(0f, 0f, -1f)) ?: targetValue
+ return if (reverse) initialValue + targetValue - v else v
+ }
+ }
+ }
+}
+
+@Composable
+fun animateColorAsState(targetValue: Color, animationSpec: AnimationSpec = spring(), label: String = "ColorAnimation"): State {
+ val from = remember { mutableStateOf(targetValue) }
+ val to = remember { mutableStateOf(targetValue) }
+ if (to.value != targetValue) {
+ from.value = to.value
+ to.value = targetValue
+ }
+ val anim = remember { Animatable(1f) }
+ LaunchedEffect(targetValue) {
+ anim.snapTo(0f)
+ anim.animateTo(1f, animationSpec)
+ }
+ return remember {
+ object : State {
+ override val value: Color get() = androidx.compose.ui.graphics.lerp(from.value, to.value, anim.value)
+ }
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/Foundation.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/Foundation.kt
new file mode 100644
index 00000000000..0244a5e1bdf
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/Foundation.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.Dp
+import com.codename1.compose.internal.BackgroundElement
+import com.codename1.compose.internal.BorderElement
+import com.codename1.compose.internal.ClickableElement
+import com.codename1.compose.internal.DrawBehindElement
+import com.codename1.compose.internal.ImageNode
+import com.codename1.compose.internal.LeafNode
+import com.codename1.compose.internal.LeafPolicy
+import com.codename1.compose.internal.ScrollElement
+import com.codename1.compose.internal.ScrollStateHandle
+import com.codename1.compose.internal.UiNode
+import androidx.compose.runtime.ComposeNode
+import com.codename1.compose.internal.NodeApplier
+
+fun Modifier.background(color: Color, shape: Shape = RectangleShape): Modifier =
+ this.then(BackgroundElement(color, null, shape))
+
+fun Modifier.background(brush: Brush, shape: Shape = RectangleShape, alpha: Float = 1f): Modifier =
+ this.then(BackgroundElement(Color.Unspecified, brush, shape))
+
+fun Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape): Modifier =
+ this.then(BorderElement(width, color, shape))
+
+fun Modifier.border(border: BorderStroke, shape: Shape = RectangleShape): Modifier =
+ this.then(BorderElement(border.width, border.color, shape))
+
+class BorderStroke(val width: Dp, val color: Color)
+
+fun Modifier.clickable(enabled: Boolean = true, onClickLabel: String? = null, onClick: () -> Unit): Modifier =
+ this.then(ClickableElement(enabled, onClick, null, true))
+
+fun Modifier.combinedClickable(enabled: Boolean = true, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): Modifier =
+ this.then(ClickableElement(enabled, onClick, onLongClick, true))
+
+/// The scroll position of a [verticalScroll] or [horizontalScroll] modifier, in
+/// pixels.
+@Stable
+class ScrollState(initial: Int) {
+ private var node: UiNode? = null
+ private var vertical = true
+ private var stored by mutableIntStateOf(initial)
+
+ val value: Int
+ get() {
+ val n = node ?: return stored
+ return if (vertical) n.scrollY else n.scrollX
+ }
+
+ val maxValue: Int
+ get() {
+ val n = node ?: return 0
+ return if (vertical) Math.max(0, n.scrollDimension.height - n.height)
+ else Math.max(0, n.scrollDimension.width - n.width)
+ }
+
+ fun scrollTo(value: Int) {
+ stored = value
+ val n = node ?: return
+ if (vertical) n.cn1ScrollY(value) else n.cn1ScrollX(value)
+ }
+
+ suspend fun animateScrollTo(value: Int) {
+ scrollTo(value)
+ }
+
+ internal val handle: ScrollStateHandle = object : ScrollStateHandle {
+ override fun attach(node: UiNode, vertical: Boolean) {
+ this@ScrollState.node = node
+ this@ScrollState.vertical = vertical
+ if (stored != 0) {
+ node.cn1ScrollPending(stored, vertical)
+ }
+ }
+
+ override fun detach(node: UiNode) {
+ if (this@ScrollState.node === node) this@ScrollState.node = null
+ }
+ }
+}
+
+@Composable
+fun rememberScrollState(initial: Int = 0): ScrollState = remember { ScrollState(initial) }
+
+fun Modifier.verticalScroll(state: ScrollState, enabled: Boolean = true): Modifier =
+ this.then(ScrollElement(state.handle, true))
+
+fun Modifier.horizontalScroll(state: ScrollState, enabled: Boolean = true): Modifier =
+ this.then(ScrollElement(state.handle, false))
+
+/// A box that draws with [onDraw]; its size comes from `modifier`.
+@Composable
+fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) {
+ LeafNode(modifier.then(DrawBehindElement(onDraw)), LeafPolicy)
+}
+
+@Composable
+fun Image(
+ bitmap: ImageBitmap,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ alignment: Alignment = Alignment.Center,
+ contentScale: androidx.compose.ui.layout.ContentScale = androidx.compose.ui.layout.ContentScale.Fit,
+ alpha: Float = 1f
+) {
+ ComposeNode(
+ factory = { ImageNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(bitmap) { this.bitmap = it }
+ set(contentScale) { this.contentScale = it }
+ }
+ )
+}
+
+@Composable
+fun isSystemInDarkTheme(): Boolean = com.codename1.compose.internal.Theme.isDark()
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Arrangement.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Arrangement.kt
new file mode 100644
index 00000000000..afbdbd355d9
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Arrangement.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import com.codename1.compose.internal.Density
+
+/// How a Row or Column distributes its children along its main axis.
+object Arrangement {
+ @Immutable
+ interface Horizontal {
+ val spacing: Dp get() = Dp(0f)
+ fun arrange(totalSize: Int, sizes: IntArray, layoutDirection: LayoutDirection, outPositions: IntArray)
+ }
+
+ @Immutable
+ interface Vertical {
+ val spacing: Dp get() = Dp(0f)
+ fun arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray)
+ }
+
+ @Immutable
+ interface HorizontalOrVertical : Horizontal, Vertical {
+ override val spacing: Dp get() = Dp(0f)
+ }
+
+ private class Kind(val name: String, val gap: (Int, Int) -> Pair) : HorizontalOrVertical {
+ // gap(free, count) answers (leading, between).
+ override fun arrange(totalSize: Int, sizes: IntArray, layoutDirection: LayoutDirection, outPositions: IntArray) =
+ arrange(totalSize, sizes, outPositions)
+
+ override fun arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
+ var used = 0
+ for (s in sizes) used += s
+ val (lead, between) = gap(Math.max(0, totalSize - used), sizes.size)
+ var p = lead
+ for (i in sizes.indices) {
+ outPositions[i] = p
+ p += sizes[i] + between
+ }
+ }
+
+ override fun toString(): String = "Arrangement#$name"
+ }
+
+ val Start: Horizontal = Kind("Start") { _, _ -> 0 to 0 }
+ val End: Horizontal = Kind("End") { f, _ -> f to 0 }
+ val Top: Vertical = Kind("Top") { _, _ -> 0 to 0 }
+ val Bottom: Vertical = Kind("Bottom") { f, _ -> f to 0 }
+ val Center: HorizontalOrVertical = Kind("Center") { f, _ -> f / 2 to 0 }
+ val SpaceBetween: HorizontalOrVertical = Kind("SpaceBetween") { f, n -> 0 to if (n > 1) f / (n - 1) else 0 }
+ val SpaceEvenly: HorizontalOrVertical = Kind("SpaceEvenly") { f, n -> f / (n + 1) to f / (n + 1) }
+ val SpaceAround: HorizontalOrVertical = Kind("SpaceAround") { f, n ->
+ if (n == 0) 0 to 0 else f / (2 * n) to f / n
+ }
+
+ /// Places children `space` apart, from the start.
+ fun spacedBy(space: Dp): HorizontalOrVertical = SpacedAligned(space, null, null)
+
+ fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = SpacedAligned(space, alignment, null)
+
+ fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = SpacedAligned(space, null, alignment)
+
+ private data class SpacedAligned(val space: Dp, val h: Alignment.Horizontal?, val v: Alignment.Vertical?) :
+ HorizontalOrVertical {
+ override val spacing: Dp get() = space
+
+ override fun arrange(totalSize: Int, sizes: IntArray, layoutDirection: LayoutDirection, outPositions: IntArray) {
+ place(totalSize, sizes, outPositions) { used -> h?.align(used, totalSize, layoutDirection) ?: 0 }
+ }
+
+ override fun arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
+ place(totalSize, sizes, outPositions) { used -> v?.align(used, totalSize) ?: 0 }
+ }
+
+ private inline fun place(totalSize: Int, sizes: IntArray, out: IntArray, lead: (Int) -> Int) {
+ val gap = Density.px(space)
+ var used = 0
+ for (s in sizes) used += s
+ if (sizes.size > 1) used += gap * (sizes.size - 1)
+ var p = lead(used)
+ for (i in sizes.indices) {
+ out[i] = p
+ p += sizes[i] + gap
+ }
+ }
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Layout.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Layout.kt
new file mode 100644
index 00000000000..c775163725d
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/layout/Layout.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation.layout
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import com.codename1.compose.internal.AlignSelfElement
+import com.codename1.compose.internal.AspectRatioElement
+import com.codename1.compose.internal.BoxPolicy
+import com.codename1.compose.internal.FillElement
+import com.codename1.compose.internal.LayoutNode
+import com.codename1.compose.internal.LeafNode
+import com.codename1.compose.internal.LeafPolicy
+import com.codename1.compose.internal.LinearPolicy
+import com.codename1.compose.internal.MatchParentSizeElement
+import com.codename1.compose.internal.OffsetElement
+import com.codename1.compose.internal.PaddingElement
+import com.codename1.compose.internal.SizeElement
+import com.codename1.compose.internal.WeightElement
+import com.codename1.compose.internal.WrapElement
+
+// --- containers -------------------------------------------------------------
+
+@Stable
+interface BoxScope {
+ fun Modifier.align(alignment: Alignment): Modifier = this.then(AlignSelfElement(null, null, alignment))
+ fun Modifier.matchParentSize(): Modifier = this.then(MatchParentSizeElement)
+}
+
+@Stable
+interface RowScope {
+ fun Modifier.weight(weight: Float, fill: Boolean = true): Modifier = this.then(WeightElement(weight, fill))
+ fun Modifier.align(alignment: Alignment.Vertical): Modifier = this.then(AlignSelfElement(null, alignment, null))
+}
+
+@Stable
+interface ColumnScope {
+ fun Modifier.weight(weight: Float, fill: Boolean = true): Modifier = this.then(WeightElement(weight, fill))
+ fun Modifier.align(alignment: Alignment.Horizontal): Modifier = this.then(AlignSelfElement(alignment, null, null))
+}
+
+internal object BoxScopeInstance : BoxScope
+internal object RowScopeInstance : RowScope
+internal object ColumnScopeInstance : ColumnScope
+
+@Composable
+fun Box(
+ modifier: Modifier = Modifier,
+ contentAlignment: Alignment = Alignment.TopStart,
+ propagateMinConstraints: Boolean = false,
+ content: @Composable BoxScope.() -> Unit
+) {
+ val policy = remember(contentAlignment, propagateMinConstraints) { BoxPolicy(contentAlignment, propagateMinConstraints) }
+ LayoutNode(modifier, policy) { BoxScopeInstance.content() }
+}
+
+@Composable
+fun Box(modifier: Modifier) {
+ LeafNode(modifier, BoxPolicy.TopStart)
+}
+
+@Composable
+fun Row(
+ modifier: Modifier = Modifier,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ content: @Composable RowScope.() -> Unit
+) {
+ val policy = remember(horizontalArrangement, verticalAlignment) {
+ LinearPolicy(false, horizontalArrangement, null, null, verticalAlignment)
+ }
+ LayoutNode(modifier, policy) { RowScopeInstance.content() }
+}
+
+@Composable
+fun Column(
+ modifier: Modifier = Modifier,
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ val policy = remember(verticalArrangement, horizontalAlignment) {
+ LinearPolicy(true, null, verticalArrangement, horizontalAlignment, null)
+ }
+ LayoutNode(modifier, policy) { ColumnScopeInstance.content() }
+}
+
+@Composable
+fun Spacer(modifier: Modifier) {
+ LeafNode(modifier, LeafPolicy)
+}
+
+// --- padding ----------------------------------------------------------------
+
+@Immutable
+interface PaddingValues {
+ fun calculateLeftPadding(layoutDirection: androidx.compose.ui.unit.LayoutDirection): Dp
+ fun calculateTopPadding(): Dp
+ fun calculateRightPadding(layoutDirection: androidx.compose.ui.unit.LayoutDirection): Dp
+ fun calculateBottomPadding(): Dp
+}
+
+@Immutable
+private data class PaddingValuesImpl(val start: Dp, val top: Dp, val end: Dp, val bottom: Dp) : PaddingValues {
+ override fun calculateLeftPadding(layoutDirection: androidx.compose.ui.unit.LayoutDirection): Dp = start
+ override fun calculateTopPadding(): Dp = top
+ override fun calculateRightPadding(layoutDirection: androidx.compose.ui.unit.LayoutDirection): Dp = end
+ override fun calculateBottomPadding(): Dp = bottom
+}
+
+fun PaddingValues(all: Dp): PaddingValues = PaddingValuesImpl(all, all, all, all)
+fun PaddingValues(horizontal: Dp = Dp(0f), vertical: Dp = Dp(0f)): PaddingValues =
+ PaddingValuesImpl(horizontal, vertical, horizontal, vertical)
+fun PaddingValues(start: Dp = Dp(0f), top: Dp = Dp(0f), end: Dp = Dp(0f), bottom: Dp = Dp(0f)): PaddingValues =
+ PaddingValuesImpl(start, top, end, bottom)
+
+fun Modifier.padding(all: Dp): Modifier = this.then(PaddingElement(all, all, all, all))
+fun Modifier.padding(horizontal: Dp = Dp(0f), vertical: Dp = Dp(0f)): Modifier =
+ this.then(PaddingElement(horizontal, vertical, horizontal, vertical))
+fun Modifier.padding(start: Dp = Dp(0f), top: Dp = Dp(0f), end: Dp = Dp(0f), bottom: Dp = Dp(0f)): Modifier =
+ this.then(PaddingElement(start, top, end, bottom))
+fun Modifier.padding(paddingValues: PaddingValues): Modifier = this.then(PaddingElement(
+ paddingValues.calculateLeftPadding(androidx.compose.ui.unit.LayoutDirection.Ltr), paddingValues.calculateTopPadding(),
+ paddingValues.calculateRightPadding(androidx.compose.ui.unit.LayoutDirection.Ltr), paddingValues.calculateBottomPadding()))
+
+// --- size -------------------------------------------------------------------
+
+fun Modifier.size(size: Dp): Modifier = this.then(SizeElement(size, size, size, size))
+fun Modifier.size(width: Dp, height: Dp): Modifier = this.then(SizeElement(width, width, height, height))
+fun Modifier.width(width: Dp): Modifier = this.then(SizeElement(minWidth = width, maxWidth = width))
+fun Modifier.height(height: Dp): Modifier = this.then(SizeElement(minHeight = height, maxHeight = height))
+fun Modifier.requiredSize(size: Dp): Modifier = this.then(SizeElement(size, size, size, size, false))
+fun Modifier.requiredWidth(width: Dp): Modifier = this.then(SizeElement(minWidth = width, maxWidth = width, enforceIncoming = false))
+fun Modifier.requiredHeight(height: Dp): Modifier = this.then(SizeElement(minHeight = height, maxHeight = height, enforceIncoming = false))
+fun Modifier.sizeIn(minWidth: Dp = Dp.Unspecified, minHeight: Dp = Dp.Unspecified, maxWidth: Dp = Dp.Unspecified,
+ maxHeight: Dp = Dp.Unspecified): Modifier = this.then(SizeElement(minWidth, maxWidth, minHeight, maxHeight))
+fun Modifier.widthIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified): Modifier =
+ this.then(SizeElement(minWidth = min, maxWidth = max))
+fun Modifier.heightIn(min: Dp = Dp.Unspecified, max: Dp = Dp.Unspecified): Modifier =
+ this.then(SizeElement(minHeight = min, maxHeight = max))
+fun Modifier.defaultMinSize(minWidth: Dp = Dp.Unspecified, minHeight: Dp = Dp.Unspecified): Modifier =
+ this.then(SizeElement(minWidth = minWidth, minHeight = minHeight))
+
+fun Modifier.fillMaxWidth(fraction: Float = 1f): Modifier = this.then(FillElement(true, false, fraction))
+fun Modifier.fillMaxHeight(fraction: Float = 1f): Modifier = this.then(FillElement(false, true, fraction))
+fun Modifier.fillMaxSize(fraction: Float = 1f): Modifier = this.then(FillElement(true, true, fraction))
+
+fun Modifier.wrapContentWidth(align: Alignment.Horizontal = Alignment.CenterHorizontally, unbounded: Boolean = false): Modifier =
+ this.then(WrapElement(true, false, Alignment { s, sp, d -> androidx.compose.ui.unit.IntOffset(align.align(s.width, sp.width, d), 0) }, unbounded))
+fun Modifier.wrapContentHeight(align: Alignment.Vertical = Alignment.CenterVertically, unbounded: Boolean = false): Modifier =
+ this.then(WrapElement(false, true, Alignment { s, sp, _ -> androidx.compose.ui.unit.IntOffset(0, align.align(s.height, sp.height)) }, unbounded))
+fun Modifier.wrapContentSize(align: Alignment = Alignment.Center, unbounded: Boolean = false): Modifier =
+ this.then(WrapElement(true, true, align, unbounded))
+
+fun Modifier.offset(x: Dp = Dp(0f), y: Dp = Dp(0f)): Modifier = this.then(OffsetElement(x, y))
+
+fun Modifier.aspectRatio(ratio: Float): Modifier = this.then(AspectRatioElement(ratio))
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/lazy/LazyList.kt
new file mode 100644
index 00000000000..bb04c56b2bd
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -0,0 +1,657 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation.lazy
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.CompositionContext
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.codename1.compose.internal.BoxPolicy
+import com.codename1.compose.internal.Constraints
+import com.codename1.compose.internal.Density
+import com.codename1.compose.internal.LTR
+import com.codename1.compose.internal.NodeApplier
+import com.codename1.compose.internal.UiNode
+import com.codename1.compose.internal.h
+import com.codename1.compose.internal.measureChild
+import com.codename1.compose.internal.size
+import com.codename1.compose.internal.w
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.fillMaxHeight
+
+/// The receiver of an item's content.
+@Stable
+interface LazyItemScope {
+ fun Modifier.fillParentMaxWidth(fraction: Float = 1f): Modifier = this.fillMaxWidth(fraction)
+ fun Modifier.fillParentMaxHeight(fraction: Float = 1f): Modifier = this.fillMaxHeight(fraction)
+ fun Modifier.animateItem(): Modifier = this
+}
+
+private object LazyItemScopeInstance : LazyItemScope
+
+/// What a [LazyColumn] or [LazyRow] content block declares.
+interface LazyListScope {
+ fun item(key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit)
+ fun items(count: Int, key: ((Int) -> Any)? = null, contentType: (Int) -> Any? = { null },
+ itemContent: @Composable LazyItemScope.(Int) -> Unit)
+ fun stickyHeader(key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit) =
+ item(key, contentType, content)
+}
+
+inline fun LazyListScope.items(items: List, noinline key: ((T) -> Any)? = null,
+ crossinline itemContent: @Composable LazyItemScope.(T) -> Unit) {
+ items(items.size, if (key != null) { i: Int -> key(items[i]) } else null) { itemContent(items[it]) }
+}
+
+inline fun LazyListScope.itemsIndexed(items: List, noinline key: ((Int, T) -> Any)? = null,
+ crossinline itemContent: @Composable LazyItemScope.(Int, T) -> Unit) {
+ items(items.size, if (key != null) { i: Int -> key(i, items[i]) } else null) { itemContent(it, items[it]) }
+}
+
+private class Entry(val start: Int, val count: Int, val key: ((Int) -> Any)?,
+ val content: @Composable LazyItemScope.(Int) -> Unit)
+
+/// What one run of a lazy list's content block declared. Only the items in view
+/// are ever composed from it.
+internal class LazyItems : LazyListScope {
+ private val entries = ArrayList(2)
+ var count = 0
+ private set
+
+ override fun item(key: Any?, contentType: Any?, content: @Composable LazyItemScope.() -> Unit) {
+ entries.add(Entry(count, 1, if (key != null) { _: Int -> key } else null) { content() })
+ count++
+ }
+
+ override fun items(count: Int, key: ((Int) -> Any)?, contentType: (Int) -> Any?,
+ itemContent: @Composable LazyItemScope.(Int) -> Unit) {
+ entries.add(Entry(this.count, count, key, itemContent))
+ this.count += count
+ }
+
+ private fun entryAt(index: Int): Entry {
+ var e = entries[0]
+ for (k in 1 until entries.size) {
+ val n = entries[k]
+ if (n.start > index) break
+ e = n
+ }
+ return e
+ }
+
+ /// The item's key: the one the content block gave it, or its position.
+ fun keyAt(index: Int): Any {
+ val e = entryAt(index)
+ val k = e.key ?: return IndexKey(index)
+ return k(index - e.start)
+ }
+
+ @Composable
+ fun Content(index: Int) {
+ val e = entryAt(index)
+ e.content(LazyItemScopeInstance, index - e.start)
+ }
+}
+
+private data class IndexKey(val index: Int)
+
+/// The scroll position of a lazy list.
+@Stable
+class LazyListState internal constructor(internal val scroll: ScrollState) {
+ internal var node: LazyListNode? = null
+
+ val firstVisibleItemIndex: Int get() = node?.cn1FirstVisible() ?: 0
+ val firstVisibleItemScrollOffset: Int get() = scroll.value
+ val canScrollForward: Boolean get() = scroll.value < scroll.maxValue
+ val canScrollBackward: Boolean get() = scroll.value > 0
+
+ suspend fun scrollToItem(index: Int, scrollOffset: Int = 0) {
+ val n = node
+ if (n != null) n.cn1ScrollToItem(index, scrollOffset) else scroll.scrollTo(scrollOffset)
+ }
+
+ suspend fun animateScrollToItem(index: Int, scrollOffset: Int = 0) = scrollToItem(index, scrollOffset)
+}
+
+@Composable
+fun rememberLazyListState(): LazyListState = remember { LazyListState(ScrollState(0)) }
+
+/// A vertically scrolling list that composes only the items in view, and
+/// disposes of an item's composition once it has scrolled well out of it.
+@Composable
+fun LazyColumn(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(),
+ reverseLayout: Boolean = false,
+ verticalArrangement: Arrangement.Vertical = Arrangement.Top,
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ userScrollEnabled: Boolean = true,
+ content: LazyListScope.() -> Unit
+) {
+ LazyList(true, modifier, state, contentPadding, verticalArrangement.spacing, horizontalAlignment, null,
+ reverseLayout, userScrollEnabled, content)
+}
+
+@Composable
+fun LazyRow(
+ modifier: Modifier = Modifier,
+ state: LazyListState = rememberLazyListState(),
+ contentPadding: PaddingValues = PaddingValues(),
+ reverseLayout: Boolean = false,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
+ verticalAlignment: Alignment.Vertical = Alignment.Top,
+ userScrollEnabled: Boolean = true,
+ content: LazyListScope.() -> Unit
+) {
+ LazyList(false, modifier, state, contentPadding, horizontalArrangement.spacing, null, verticalAlignment,
+ reverseLayout, userScrollEnabled, content)
+}
+
+@Composable
+private fun LazyList(vertical: Boolean, modifier: Modifier, state: LazyListState, padding: PaddingValues,
+ spacing: androidx.compose.ui.unit.Dp, horizontal: Alignment.Horizontal?,
+ verticalAlign: Alignment.Vertical?, reverseLayout: Boolean, userScrollEnabled: Boolean,
+ content: LazyListScope.() -> Unit) {
+ // Laying the items out from the end is not implemented. Refusing is better than drawing a
+ // chat-style list upside down and letting it look like it works.
+ require(!reverseLayout) { "reverseLayout = true is not supported by LazyColumn/LazyRow" }
+ val context = rememberCompositionContext()
+ val holder = remember { NodeHolder() }
+ DisposableEffect(holder) {
+ onDispose { holder.node?.cn1Dispose() }
+ }
+ // The declared items are rebuilt only when the content block changes or a state it read does.
+ // A parent recomposition that hands back the same block (the Compose compiler memoizes it when
+ // its captures are stable) reuses the same LazyItems, so `set(items)` below does not fire: no
+ // remap, and no item composition is given new content.
+ val items = remember(content) { derivedStateOf { LazyItems().also(content) } }.value
+ ComposeNode(
+ factory = { LazyListNode(vertical).also { holder.node = it } },
+ update = {
+ set(context) { this.context = it }
+ set(state) { this.state = it }
+ set(padding) { this.padding = it }
+ set(spacing) { this.spacing = it }
+ set(horizontal) { this.horizontalAlignment = it }
+ set(verticalAlign) { this.verticalAlignment = it }
+ set(userScrollEnabled) { this.userScroll = it }
+ set(modifier) { this.modifier = it }
+ set(items) { this.items = it }
+ }
+ )
+}
+
+/// Lets the list's item compositions be disposed of with the list.
+private class NodeHolder {
+ var node: LazyListNode? = null
+}
+
+/// The node behind [LazyColumn] and [LazyRow]. It is its own scroll container,
+/// and every item in view is a composition of its own under the list's, rooted
+/// at a node that is this one's child. Item sizes are learnt as items are
+/// measured; the extent of the ones never seen is estimated from the average.
+///
+/// A pass costs the window, not the list: measured extents and their count are
+/// kept in two Fenwick trees, so where any item starts is O(log n) and the item
+/// at a position O(log^2 n), and the pass walks only the items within reach of
+/// the view. Only a change of content is O(n), to carry sizes over by key.
+internal class LazyListNode(private val vertical: Boolean) : UiNode() {
+ private class Slot(val key: Any, val root: UiNode, val composition: Composition) {
+ var index = -1
+ var items: LazyItems? = null
+ var live = false
+ var start = 0
+ }
+
+ var context: CompositionContext? = null
+ var state: LazyListState? = null
+ set(v) {
+ if (field !== v) {
+ field?.node = null
+ field?.scroll?.handle?.detach(this)
+ field = v
+ v?.node = this
+ v?.scroll?.handle?.attach(this, vertical)
+ }
+ }
+ var padding: PaddingValues = PaddingValues()
+ set(v) { if (field != v) { field = v; invalidateMeasure() } }
+ var spacing: androidx.compose.ui.unit.Dp = androidx.compose.ui.unit.Dp(0f)
+ set(v) { if (field != v) { field = v; invalidateMeasure() } }
+ var userScroll = true
+ set(v) {
+ if (field != v) {
+ field = v
+ if (vertical) isScrollableY = v else isScrollableX = v
+ }
+ }
+ var horizontalAlignment: Alignment.Horizontal? = null
+ var verticalAlignment: Alignment.Vertical? = null
+ var items: LazyItems? = null
+ set(v) {
+ val old = field
+ field = v
+ if (v != null) remap(old, v)
+ invalidateMeasure()
+ }
+
+ // Composed items, by key and as a list; the list is what a pass walks, so that
+ // nothing allocates an iterator.
+ private val slots = HashMap()
+ private val slotList = ArrayList()
+ // Per item: its key, and its extent along the scroll axis once measured (-1 before).
+ private var keys = arrayOfNulls(0)
+ private var extents = IntArray(0)
+ // Fenwick trees over the measured extents and over how many are measured.
+ private var sumTree = IntArray(1)
+ private var countTree = IntArray(1)
+ private var knownCount = 0
+ private var knownSum = 0
+ // The geometry of the last pass, which positions are computed with until the next.
+ private var lead = 0
+ private var gap = 0
+ private var estimate = 0
+ private var total = 0
+ private var crossExtent = 0
+ private var windowAt = Int.MIN_VALUE
+ private var viewport = 0
+ private var measuring = false
+ // The item the view is anchored on across a pass that learns sizes: the first one in view and
+ // how far into it the view starts. Without it an item above the view measured larger than its
+ // estimate pushes everything in view down, and the list jumps under a still finger.
+ private var anchorKey: Any? = null
+ private var anchorOffset = 0
+ private var anchorIndex = -1
+ // Set when the anchor was taken by a remap or by scrollToItem and must be used as it is;
+ // otherwise every pass takes it afresh from where the view is now.
+ private var anchorFixed = false
+
+ init {
+ policy = BoxPolicy.TopStart
+ if (vertical) isScrollableY = true else isScrollableX = true
+ }
+
+ override fun lazyAxis(): Int = if (!userScroll) 0 else if (vertical) 1 else 2
+
+ private fun treeAdd(t: IntArray, index: Int, v: Int) {
+ var i = index + 1
+ while (i < t.size) {
+ t[i] += v
+ i += i and -i
+ }
+ }
+
+ /// The sum over items [0, index).
+ private fun treeSum(t: IntArray, index: Int): Int {
+ var i = index
+ var s = 0
+ while (i > 0) {
+ s += t[i]
+ i -= i and -i
+ }
+ return s
+ }
+
+ private fun setExtent(i: Int, e: Int) {
+ val old = extents[i]
+ if (old == e) return
+ extents[i] = e
+ if (old < 0) {
+ treeAdd(countTree, i, 1)
+ knownCount++
+ treeAdd(sumTree, i, e)
+ knownSum += e
+ } else {
+ treeAdd(sumTree, i, e - old)
+ knownSum += e - old
+ }
+ }
+
+ /// Where item `i` starts (or, for `i == n`, where the gap after the last one ends).
+ private fun startOf(i: Int): Int {
+ val kc = treeSum(countTree, i)
+ // In Long and clamped: a list of millions of estimated items passes Int.MAX_VALUE pixels.
+ val v = lead.toLong() + treeSum(sumTree, i) + (i - kc).toLong() * estimate + i.toLong() * gap
+ return if (v > MAX_EXTENT) MAX_EXTENT.toInt() else v.toInt()
+ }
+
+ private companion object {
+ // Below Constraints.INF, so a clamped extent never reads as "unbounded".
+ const val MAX_EXTENT = (Int.MAX_VALUE / 2).toLong()
+ }
+
+ private fun extentOf(i: Int): Int = if (extents[i] >= 0) extents[i] else estimate
+
+ /// The first item that ends past `at`, or -1 for an empty list.
+ private fun firstAt(at: Int): Int {
+ val n = keys.size
+ var lo = 0
+ var hi = n - 1
+ var found = -1
+ while (lo <= hi) {
+ val mid = (lo + hi) ushr 1
+ if (startOf(mid) + extentOf(mid) > at) {
+ found = mid
+ hi = mid - 1
+ } else {
+ lo = mid + 1
+ }
+ }
+ return found
+ }
+
+ /// Carries what is known of each item over to a new content block, by key.
+ private fun remap(old: LazyItems?, now: LazyItems) {
+ captureAnchor()
+ anchorIndex = -1
+ anchorFixed = true
+ val n = now.count
+ val oldKeys = keys
+ val oldExtents = extents
+ val newKeys = arrayOfNulls(n)
+ val newExtents = IntArray(n)
+ var byKey: HashMap? = null
+ val ak = anchorKey
+ for (i in 0 until n) {
+ val k = now.keyAt(i)
+ newKeys[i] = k
+ if (anchorIndex < 0 && ak != null && k == ak) anchorIndex = i
+ var e = -1
+ if (i < oldKeys.size && oldKeys[i] == k) {
+ e = oldExtents[i]
+ } else if (old != null && oldKeys.isNotEmpty()) {
+ if (byKey == null) {
+ byKey = HashMap(oldKeys.size * 2)
+ for (j in oldKeys.indices) {
+ // The first of a repeated key wins; a repeat is refused once composed.
+ val ok = oldKeys[j]!!
+ if (!byKey.containsKey(ok)) byKey[ok] = j
+ }
+ }
+ val j = byKey[k]
+ if (j != null) e = oldExtents[j]
+ }
+ newExtents[i] = e
+ }
+ keys = newKeys
+ extents = IntArray(n) { -1 }
+ sumTree = IntArray(n + 1)
+ countTree = IntArray(n + 1)
+ knownCount = 0
+ knownSum = 0
+ for (i in 0 until n) {
+ if (newExtents[i] >= 0) setExtent(i, newExtents[i])
+ }
+ }
+
+ override fun measureContent(c: Constraints): Long {
+ val list = items
+ val ctx = context
+ val n = list?.count ?: 0
+ if (list == null || ctx == null || n == 0) {
+ disposeAll()
+ total = 0
+ return size(c.minWidth, c.minHeight)
+ }
+ measuring = true
+ try {
+ return measureItems(list, ctx, c, n)
+ } finally {
+ measuring = false
+ }
+ }
+
+ private fun measureItems(list: LazyItems, ctx: CompositionContext, c: Constraints, n: Int): Long {
+ // The anchor is taken with the geometry the view was drawn with, before this pass
+ // changes the estimate.
+ if (!anchorFixed) {
+ captureAnchor()
+ }
+ val d = Density
+ val p = padding
+ lead = if (vertical) d.px(p.calculateTopPadding()) else d.px(p.calculateLeftPadding(LTR))
+ val trail = if (vertical) d.px(p.calculateBottomPadding()) else d.px(p.calculateRightPadding(LTR))
+ val crossLead = if (vertical) d.px(p.calculateLeftPadding(LTR)) else d.px(p.calculateTopPadding())
+ val crossTrail = if (vertical) d.px(p.calculateRightPadding(LTR)) else d.px(p.calculateBottomPadding())
+ gap = d.px(spacing)
+ // Fixed for the pass, so that every position in it is computed the same way.
+ estimate = if (knownCount > 0) knownSum / knownCount else d.px(androidx.compose.ui.unit.Dp(72f))
+ val viewMain = if (vertical) c.maxHeight else c.maxWidth
+ val crossMax = if (vertical) c.maxWidth else c.maxHeight
+ val crossAvail = if (crossMax == Constraints.INF) Constraints.INF else Math.max(0, crossMax - crossLead - crossTrail)
+ val itemC = if (vertical) Constraints(0, crossAvail, 0, Constraints.INF)
+ else Constraints(0, Constraints.INF, 0, crossAvail)
+ val unbounded = viewMain == Constraints.INF
+ var at = if (vertical) scrollY else scrollX
+ val ai0 = anchorIndex
+ if (!unbounded && at > 0 && ai0 in 0 until n) {
+ // Where the anchor is under this pass's estimate. The estimate moves as sizes are
+ // learnt, and a window chosen from the old position can miss every item.
+ val want = Math.max(0, startOf(ai0) + anchorOffset)
+ if (want != at) {
+ at = want
+ if (vertical) scrollY = at else scrollX = at
+ }
+ }
+ // Compose what is in view and a quarter of a view either side; keep what was
+ // composed until it is half a view away, so a slow scroll does not churn.
+ val margin = if (unbounded) 0 else viewMain / 4
+ val composeFrom = if (unbounded) Int.MIN_VALUE else at - margin
+ val composeTo = if (unbounded) Int.MAX_VALUE else at + viewMain + margin
+ val keepFrom = if (unbounded) Int.MIN_VALUE else at - 2 * margin
+ val keepTo = if (unbounded) Int.MAX_VALUE else at + viewMain + 2 * margin
+ for (k in 0 until slotList.size) slotList[k].live = false
+ var i = if (unbounded) 0 else Math.max(0, firstAt(keepFrom))
+ var pos = startOf(i)
+ var cross = 0
+ while (i < n && pos < keepTo) {
+ var e = extents[i]
+ val guess = if (e >= 0) e else estimate
+ val key = keys[i]!!
+ var slot = slots[key]
+ val overlaps = pos + guess > composeFrom && pos < composeTo
+ val kept = slot != null && pos + guess > keepFrom
+ if (overlaps || kept) {
+ if (slot == null) {
+ slot = compose(key, ctx)
+ } else if (slot.live) {
+ // Two items share a slot; the second would silently take the first's
+ // composition. Like Compose's own, the check covers the items composed.
+ throw IllegalArgumentException("Key " + key + " was already used. Every item of a "
+ + "lazy list must have a unique key.")
+ }
+ if (slot.items !== list || slot.index != i) {
+ slot.items = list
+ slot.index = i
+ val l = list
+ val idx = i
+ slot.composition.setContent { l.Content(idx) }
+ }
+ val m = measureChild(slot.root, itemC)
+ e = if (vertical) m.h() else m.w()
+ setExtent(i, e)
+ cross = Math.max(cross, if (vertical) m.w() else m.h())
+ slot.live = true
+ slot.start = pos
+ } else {
+ e = guess
+ }
+ pos += e + gap
+ i++
+ }
+ val end = startOf(n) - gap + trail
+ if (total != end) {
+ total = end
+ // Component caches the scroll extent and clamps setScrollY against it; without this
+ // the anchor below is clamped to the extent the estimate gave last pass.
+ setScrollSize(null)
+ }
+ disposeDead()
+ crossExtent = cross + crossLead + crossTrail
+ val crossSize = crossExtent
+ // Place what is composed; positions are along the whole scrolled extent.
+ for (k in 0 until slotList.size) {
+ val s = slotList[k]
+ val ci = getComponentIndex(s.root)
+ if (ci < 0) continue
+ val iw = s.root.measuredWidth
+ val ih = s.root.measuredHeight
+ if (vertical) {
+ val a = horizontalAlignment
+ val x = crossLead + (if (a == null || crossAvail == Constraints.INF) 0 else a.align(iw, crossAvail, LTR))
+ place(ci, x, s.start, iw, ih)
+ } else {
+ val a = verticalAlignment
+ val y = crossLead + (if (a == null || crossAvail == Constraints.INF) 0 else a.align(ih, crossAvail))
+ place(ci, s.start, y, iw, ih)
+ }
+ }
+ viewport = if (unbounded) total else viewMain
+ var now = at
+ val ai = anchorIndex
+ if (!unbounded && at > 0 && ai in 0 until n) {
+ val want = Math.max(0, Math.min(startOf(ai) + anchorOffset, total - viewport))
+ if (want != at) {
+ now = want
+ if (vertical) scrollY = want else scrollX = want
+ }
+ }
+ windowAt = now
+ anchorFixed = false
+ return if (vertical) size(c.constrainWidth(crossSize), c.constrainHeight(total))
+ else size(c.constrainWidth(total), c.constrainHeight(crossSize))
+ }
+
+ private fun compose(key: Any, ctx: CompositionContext): Slot {
+ val root = UiNode()
+ addComponent(root)
+ val s = Slot(key, root, Composition(NodeApplier(root), ctx))
+ slots[key] = s
+ slotList.add(s)
+ return s
+ }
+
+ private fun disposeDead() {
+ var w = 0
+ for (k in 0 until slotList.size) {
+ val s = slotList[k]
+ if (s.live) {
+ slotList[w++] = s
+ } else {
+ slots.remove(s.key)
+ s.composition.dispose()
+ removeComponent(s.root)
+ }
+ }
+ while (slotList.size > w) slotList.removeAt(slotList.size - 1)
+ }
+
+ private fun disposeAll() {
+ for (k in 0 until slotList.size) slotList[k].live = false
+ disposeDead()
+ }
+
+ override fun calcScrollSize(): com.codename1.ui.geom.Dimension =
+ if (vertical) com.codename1.ui.geom.Dimension(Math.max(width, crossExtent), Math.max(height, total))
+ else com.codename1.ui.geom.Dimension(Math.max(width, total), Math.max(height, crossExtent))
+
+ override fun onScrollY(scrollY: Int) {
+ super.onScrollY(scrollY)
+ if (vertical) scrolled(scrollY)
+ }
+
+ override fun onScrollX(scrollX: Int) {
+ super.onScrollX(scrollX)
+ if (!vertical) scrolled(scrollX)
+ }
+
+ /// Composes what scrolled into view before it is painted: a quarter of a view
+ /// of travel since the last pass.
+ private fun scrolled(at: Int) {
+ if (measuring || windowAt == Int.MIN_VALUE) return
+ if (Math.abs(at - windowAt) * 4 >= viewport) {
+ remeasureInPlace()
+ }
+ }
+
+ private fun captureAnchor() {
+ val at = if (vertical) scrollY else scrollX
+ val i = firstAt(at)
+ if (i < 0) {
+ anchorKey = null
+ anchorIndex = -1
+ return
+ }
+ anchorKey = keys[i]
+ anchorIndex = i
+ anchorOffset = at - startOf(i)
+ }
+
+ internal fun cn1FirstVisible(): Int = Math.max(0, firstAt(if (vertical) scrollY else scrollX))
+
+ /// Scrolls so that `index` starts `offset` into the view. The item is composed and measured
+ /// first, so the position is its real one and not the estimate it had while out of view.
+ internal fun cn1ScrollToItem(index: Int, offset: Int) {
+ // A pass composes around where the estimate put the item; measuring its neighbours can
+ // move it, or the end of the list, so repeat until the position holds. Each pass leaves
+ // fewer estimates behind it, and two are the usual case.
+ for (pass in 0 until 4) {
+ if (index !in keys.indices) return
+ val t = Math.max(0, Math.min(startOf(index) + offset, total - viewport))
+ anchorKey = keys[index]
+ anchorIndex = index
+ anchorOffset = offset
+ anchorFixed = true
+ if ((if (vertical) scrollY else scrollX) == t && pass > 0) return
+ if (vertical) scrollY = t else scrollX = t
+ measuring = true
+ try {
+ remeasureInPlace()
+ } finally {
+ measuring = false
+ }
+ }
+ }
+
+ /// How many items the list has, and how many of them are composed.
+ fun cn1ItemCount(): Int = items?.count ?: 0
+
+ fun cn1ComposedCount(): Int = slots.size
+
+ /// Disposes every item composition; the list is leaving the composition.
+ internal fun cn1Dispose() {
+ disposeAll()
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/shape/Shapes.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/shape/Shapes.kt
new file mode 100644
index 00000000000..e4bea72456e
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/shape/Shapes.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation.shape
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+
+/// A corner's size, resolved against the shape's size.
+@Immutable
+interface CornerSize {
+ fun toPx(shapeSize: Size, density: Density): Float
+}
+
+private data class DpCornerSize(val size: Dp) : CornerSize {
+ override fun toPx(shapeSize: Size, density: Density): Float = size.value * density.density
+}
+
+private data class PercentCornerSize(val percent: Float) : CornerSize {
+ override fun toPx(shapeSize: Size, density: Density): Float = shapeSize.minDimension * percent / 100f
+}
+
+private data class PxCornerSize(val px: Float) : CornerSize {
+ override fun toPx(shapeSize: Size, density: Density): Float = px
+}
+
+fun CornerSize(size: Dp): CornerSize = DpCornerSize(size)
+fun CornerSize(percent: Int): CornerSize = PercentCornerSize(percent.toFloat())
+fun CornerSize(size: Float): CornerSize = PxCornerSize(size)
+
+@Immutable
+open class RoundedCornerShape(
+ val topStart: CornerSize, val topEnd: CornerSize, val bottomEnd: CornerSize, val bottomStart: CornerSize
+) : Shape {
+ override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
+ val ts = topStart.toPx(size, density)
+ val te = topEnd.toPx(size, density)
+ val be = bottomEnd.toPx(size, density)
+ val bs = bottomStart.toPx(size, density)
+ if (ts + te + be + bs == 0f) return Outline.Rectangle(size.width, size.height)
+ return Outline.Rounded(size.width, size.height, ts, te, be, bs)
+ }
+
+ override fun equals(other: Any?): Boolean = other is RoundedCornerShape && other.topStart == topStart &&
+ other.topEnd == topEnd && other.bottomEnd == bottomEnd && other.bottomStart == bottomStart
+
+ override fun hashCode(): Int = ((topStart.hashCode() * 31 + topEnd.hashCode()) * 31 + bottomEnd.hashCode()) * 31 +
+ bottomStart.hashCode()
+
+ fun copy(topStart: CornerSize = this.topStart, topEnd: CornerSize = this.topEnd, bottomEnd: CornerSize = this.bottomEnd,
+ bottomStart: CornerSize = this.bottomStart): RoundedCornerShape =
+ RoundedCornerShape(topStart, topEnd, bottomEnd, bottomStart)
+}
+
+fun RoundedCornerShape(corner: CornerSize): RoundedCornerShape = RoundedCornerShape(corner, corner, corner, corner)
+fun RoundedCornerShape(size: Dp): RoundedCornerShape = RoundedCornerShape(CornerSize(size))
+fun RoundedCornerShape(percent: Int): RoundedCornerShape = RoundedCornerShape(CornerSize(percent))
+fun RoundedCornerShape(topStart: Dp = Dp(0f), topEnd: Dp = Dp(0f), bottomEnd: Dp = Dp(0f), bottomStart: Dp = Dp(0f)): RoundedCornerShape =
+ RoundedCornerShape(CornerSize(topStart), CornerSize(topEnd), CornerSize(bottomEnd), CornerSize(bottomStart))
+
+val CircleShape: RoundedCornerShape = RoundedCornerShape(50)
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/text/BasicText.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/text/BasicText.kt
new file mode 100644
index 00000000000..9283388ea8e
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.foundation.text
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextOverflow
+import com.codename1.compose.internal.NodeApplier
+import com.codename1.compose.internal.TextNode
+
+@Composable
+fun BasicText(
+ text: String,
+ modifier: Modifier = Modifier,
+ style: TextStyle = TextStyle.Default,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ ComposeNode(
+ factory = { TextNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(text) { this.text = it }
+ set(style) { this.style = it }
+ set(overflow) { this.overflow = it }
+ set(softWrap) { this.softWrap = it }
+ set(maxLines) { this.maxLines = it }
+ }
+ )
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/Icons.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/Icons.kt
new file mode 100644
index 00000000000..b98bf58ff17
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/Icons.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material.icons
+
+/// The material icon sets. Each icon is an extension property in the set's
+/// package (`androidx.compose.material.icons.filled.Favorite`), as in Compose.
+object Icons {
+ object Filled
+ object Outlined
+ object Rounded
+ object AutoMirrored {
+ object Filled
+ object Outlined
+ }
+
+ val Default: Filled = Filled
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/automirrored/filled/AutoMirrored.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/automirrored/filled/AutoMirrored.kt
new file mode 100644
index 00000000000..3e8891b4453
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/automirrored/filled/AutoMirrored.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material.icons.automirrored.filled
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.ui.graphics.vector.ImageVector
+
+private val mirroredArrowBack: ImageVector = ImageVector.cn1Material("Filled.ArrowBack", '\uE5C4')
+val Icons.AutoMirrored.Filled.ArrowBack: ImageVector get() = mirroredArrowBack
+
+private val mirroredKeyboardArrowRight: ImageVector = ImageVector.cn1Material("Filled.KeyboardArrowRight", '\uE315')
+val Icons.AutoMirrored.Filled.KeyboardArrowRight: ImageVector get() = mirroredKeyboardArrowRight
+
+private val mirroredList: ImageVector = ImageVector.cn1Material("Filled.List", '\uE896')
+val Icons.AutoMirrored.Filled.List: ImageVector get() = mirroredList
+
+private val mirroredSend: ImageVector = ImageVector.cn1Material("Filled.Send", '\uE163')
+val Icons.AutoMirrored.Filled.Send: ImageVector get() = mirroredSend
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/filled/Filled.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/filled/Filled.kt
new file mode 100644
index 00000000000..8250e8dbb32
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/filled/Filled.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material.icons.filled
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.ui.graphics.vector.ImageVector
+
+private val filledFavorite: ImageVector = ImageVector.cn1Material("Filled.Favorite", '\uE87D')
+val Icons.Filled.Favorite: ImageVector get() = filledFavorite
+
+private val filledFavoriteBorder: ImageVector = ImageVector.cn1Material("Filled.FavoriteBorder", '\uE87E')
+val Icons.Filled.FavoriteBorder: ImageVector get() = filledFavoriteBorder
+
+private val filledSearch: ImageVector = ImageVector.cn1Material("Filled.Search", '\uE8B6')
+val Icons.Filled.Search: ImageVector get() = filledSearch
+
+private val filledClose: ImageVector = ImageVector.cn1Material("Filled.Close", '\uE5CD')
+val Icons.Filled.Close: ImageVector get() = filledClose
+
+private val filledArrowBack: ImageVector = ImageVector.cn1Material("Filled.ArrowBack", '\uE5C4')
+val Icons.Filled.ArrowBack: ImageVector get() = filledArrowBack
+
+private val filledAdd: ImageVector = ImageVector.cn1Material("Filled.Add", '\uE145')
+val Icons.Filled.Add: ImageVector get() = filledAdd
+
+private val filledHome: ImageVector = ImageVector.cn1Material("Filled.Home", '\uE88A')
+val Icons.Filled.Home: ImageVector get() = filledHome
+
+private val filledPerson: ImageVector = ImageVector.cn1Material("Filled.Person", '\uE7FD')
+val Icons.Filled.Person: ImageVector get() = filledPerson
+
+private val filledSettings: ImageVector = ImageVector.cn1Material("Filled.Settings", '\uE8B8')
+val Icons.Filled.Settings: ImageVector get() = filledSettings
+
+private val filledStar: ImageVector = ImageVector.cn1Material("Filled.Star", '\uE838')
+val Icons.Filled.Star: ImageVector get() = filledStar
+
+private val filledStarBorder: ImageVector = ImageVector.cn1Material("Filled.StarBorder", '\uE83A')
+val Icons.Filled.StarBorder: ImageVector get() = filledStarBorder
+
+private val filledMenu: ImageVector = ImageVector.cn1Material("Filled.Menu", '\uE5D2')
+val Icons.Filled.Menu: ImageVector get() = filledMenu
+
+private val filledMoreVert: ImageVector = ImageVector.cn1Material("Filled.MoreVert", '\uE5D4')
+val Icons.Filled.MoreVert: ImageVector get() = filledMoreVert
+
+private val filledCheck: ImageVector = ImageVector.cn1Material("Filled.Check", '\uE5CA')
+val Icons.Filled.Check: ImageVector get() = filledCheck
+
+private val filledInfo: ImageVector = ImageVector.cn1Material("Filled.Info", '\uE88E')
+val Icons.Filled.Info: ImageVector get() = filledInfo
+
+private val filledSchedule: ImageVector = ImageVector.cn1Material("Filled.Schedule", '\uE8B5')
+val Icons.Filled.Schedule: ImageVector get() = filledSchedule
+
+private val filledPlace: ImageVector = ImageVector.cn1Material("Filled.Place", '\uE55F')
+val Icons.Filled.Place: ImageVector get() = filledPlace
+
+private val filledShare: ImageVector = ImageVector.cn1Material("Filled.Share", '\uE80D')
+val Icons.Filled.Share: ImageVector get() = filledShare
+
+private val filledDelete: ImageVector = ImageVector.cn1Material("Filled.Delete", '\uE872')
+val Icons.Filled.Delete: ImageVector get() = filledDelete
+
+private val filledEdit: ImageVector = ImageVector.cn1Material("Filled.Edit", '\uE3C9')
+val Icons.Filled.Edit: ImageVector get() = filledEdit
+
+private val filledRefresh: ImageVector = ImageVector.cn1Material("Filled.Refresh", '\uE5D5')
+val Icons.Filled.Refresh: ImageVector get() = filledRefresh
+
+private val filledNotifications: ImageVector = ImageVector.cn1Material("Filled.Notifications", '\uE7F4')
+val Icons.Filled.Notifications: ImageVector get() = filledNotifications
+
+private val filledDateRange: ImageVector = ImageVector.cn1Material("Filled.DateRange", '\uE916')
+val Icons.Filled.DateRange: ImageVector get() = filledDateRange
+
+private val filledList: ImageVector = ImageVector.cn1Material("Filled.List", '\uE896')
+val Icons.Filled.List: ImageVector get() = filledList
+
+private val filledThumbUp: ImageVector = ImageVector.cn1Material("Filled.ThumbUp", '\uE8DC')
+val Icons.Filled.ThumbUp: ImageVector get() = filledThumbUp
+
+private val filledPlayArrow: ImageVector = ImageVector.cn1Material("Filled.PlayArrow", '\uE037')
+val Icons.Filled.PlayArrow: ImageVector get() = filledPlayArrow
+
+private val filledKeyboardArrowRight: ImageVector = ImageVector.cn1Material("Filled.KeyboardArrowRight", '\uE315')
+val Icons.Filled.KeyboardArrowRight: ImageVector get() = filledKeyboardArrowRight
+
+private val filledKeyboardArrowDown: ImageVector = ImageVector.cn1Material("Filled.KeyboardArrowDown", '\uE313')
+val Icons.Filled.KeyboardArrowDown: ImageVector get() = filledKeyboardArrowDown
+
+private val filledAccountCircle: ImageVector = ImageVector.cn1Material("Filled.AccountCircle", '\uE853')
+val Icons.Filled.AccountCircle: ImageVector get() = filledAccountCircle
+
+private val filledCalendarToday: ImageVector = ImageVector.cn1Material("Filled.CalendarToday", '\uE935')
+val Icons.Filled.CalendarToday: ImageVector get() = filledCalendarToday
+
+private val filledEvent: ImageVector = ImageVector.cn1Material("Filled.Event", '\uE878')
+val Icons.Filled.Event: ImageVector get() = filledEvent
+
+private val filledMic: ImageVector = ImageVector.cn1Material("Filled.Mic", '\uE029')
+val Icons.Filled.Mic: ImageVector get() = filledMic
+
+private val filledBookmark: ImageVector = ImageVector.cn1Material("Filled.Bookmark", '\uE866')
+val Icons.Filled.Bookmark: ImageVector get() = filledBookmark
+
+private val filledBookmarkBorder: ImageVector = ImageVector.cn1Material("Filled.BookmarkBorder", '\uE867')
+val Icons.Filled.BookmarkBorder: ImageVector get() = filledBookmarkBorder
+
+private val filledFilterList: ImageVector = ImageVector.cn1Material("Filled.FilterList", '\uE152')
+val Icons.Filled.FilterList: ImageVector get() = filledFilterList
+
+private val filledClear: ImageVector = ImageVector.cn1Material("Filled.Clear", '\uE14C')
+val Icons.Filled.Clear: ImageVector get() = filledClear
+
+private val filledSend: ImageVector = ImageVector.cn1Material("Filled.Send", '\uE163')
+val Icons.Filled.Send: ImageVector get() = filledSend
+
+private val filledLocationOn: ImageVector = ImageVector.cn1Material("Filled.LocationOn", '\uE0C8')
+val Icons.Filled.LocationOn: ImageVector get() = filledLocationOn
+
+private val filledEmail: ImageVector = ImageVector.cn1Material("Filled.Email", '\uE0BE')
+val Icons.Filled.Email: ImageVector get() = filledEmail
+
+private val filledPhone: ImageVector = ImageVector.cn1Material("Filled.Phone", '\uE0CD')
+val Icons.Filled.Phone: ImageVector get() = filledPhone
+
+private val filledWarning: ImageVector = ImageVector.cn1Material("Filled.Warning", '\uE002')
+val Icons.Filled.Warning: ImageVector get() = filledWarning
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/outlined/Outlined.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/outlined/Outlined.kt
new file mode 100644
index 00000000000..b8a6c885d53
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material/icons/outlined/Outlined.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material.icons.outlined
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.ui.graphics.vector.ImageVector
+
+private val outlinedFavorite: ImageVector = ImageVector.cn1Material("Outlined.Favorite", '\uE87E')
+val Icons.Outlined.Favorite: ImageVector get() = outlinedFavorite
+
+private val outlinedFavoriteBorder: ImageVector = ImageVector.cn1Material("Outlined.FavoriteBorder", '\uE87E')
+val Icons.Outlined.FavoriteBorder: ImageVector get() = outlinedFavoriteBorder
+
+private val outlinedSearch: ImageVector = ImageVector.cn1Material("Outlined.Search", '\uE8B6')
+val Icons.Outlined.Search: ImageVector get() = outlinedSearch
+
+private val outlinedClose: ImageVector = ImageVector.cn1Material("Outlined.Close", '\uE5CD')
+val Icons.Outlined.Close: ImageVector get() = outlinedClose
+
+private val outlinedArrowBack: ImageVector = ImageVector.cn1Material("Outlined.ArrowBack", '\uE5C4')
+val Icons.Outlined.ArrowBack: ImageVector get() = outlinedArrowBack
+
+private val outlinedAdd: ImageVector = ImageVector.cn1Material("Outlined.Add", '\uE145')
+val Icons.Outlined.Add: ImageVector get() = outlinedAdd
+
+private val outlinedHome: ImageVector = ImageVector.cn1Material("Outlined.Home", '\uE88A')
+val Icons.Outlined.Home: ImageVector get() = outlinedHome
+
+private val outlinedPerson: ImageVector = ImageVector.cn1Material("Outlined.Person", '\uE7FD')
+val Icons.Outlined.Person: ImageVector get() = outlinedPerson
+
+private val outlinedSettings: ImageVector = ImageVector.cn1Material("Outlined.Settings", '\uE8B8')
+val Icons.Outlined.Settings: ImageVector get() = outlinedSettings
+
+private val outlinedStar: ImageVector = ImageVector.cn1Material("Outlined.Star", '\uE83A')
+val Icons.Outlined.Star: ImageVector get() = outlinedStar
+
+private val outlinedStarBorder: ImageVector = ImageVector.cn1Material("Outlined.StarBorder", '\uE83A')
+val Icons.Outlined.StarBorder: ImageVector get() = outlinedStarBorder
+
+private val outlinedMenu: ImageVector = ImageVector.cn1Material("Outlined.Menu", '\uE5D2')
+val Icons.Outlined.Menu: ImageVector get() = outlinedMenu
+
+private val outlinedMoreVert: ImageVector = ImageVector.cn1Material("Outlined.MoreVert", '\uE5D4')
+val Icons.Outlined.MoreVert: ImageVector get() = outlinedMoreVert
+
+private val outlinedCheck: ImageVector = ImageVector.cn1Material("Outlined.Check", '\uE5CA')
+val Icons.Outlined.Check: ImageVector get() = outlinedCheck
+
+private val outlinedInfo: ImageVector = ImageVector.cn1Material("Outlined.Info", '\uE88E')
+val Icons.Outlined.Info: ImageVector get() = outlinedInfo
+
+private val outlinedSchedule: ImageVector = ImageVector.cn1Material("Outlined.Schedule", '\uE8B5')
+val Icons.Outlined.Schedule: ImageVector get() = outlinedSchedule
+
+private val outlinedPlace: ImageVector = ImageVector.cn1Material("Outlined.Place", '\uE55F')
+val Icons.Outlined.Place: ImageVector get() = outlinedPlace
+
+private val outlinedShare: ImageVector = ImageVector.cn1Material("Outlined.Share", '\uE80D')
+val Icons.Outlined.Share: ImageVector get() = outlinedShare
+
+private val outlinedDelete: ImageVector = ImageVector.cn1Material("Outlined.Delete", '\uE872')
+val Icons.Outlined.Delete: ImageVector get() = outlinedDelete
+
+private val outlinedEdit: ImageVector = ImageVector.cn1Material("Outlined.Edit", '\uE3C9')
+val Icons.Outlined.Edit: ImageVector get() = outlinedEdit
+
+private val outlinedRefresh: ImageVector = ImageVector.cn1Material("Outlined.Refresh", '\uE5D5')
+val Icons.Outlined.Refresh: ImageVector get() = outlinedRefresh
+
+private val outlinedNotifications: ImageVector = ImageVector.cn1Material("Outlined.Notifications", '\uE7F4')
+val Icons.Outlined.Notifications: ImageVector get() = outlinedNotifications
+
+private val outlinedDateRange: ImageVector = ImageVector.cn1Material("Outlined.DateRange", '\uE916')
+val Icons.Outlined.DateRange: ImageVector get() = outlinedDateRange
+
+private val outlinedList: ImageVector = ImageVector.cn1Material("Outlined.List", '\uE896')
+val Icons.Outlined.List: ImageVector get() = outlinedList
+
+private val outlinedThumbUp: ImageVector = ImageVector.cn1Material("Outlined.ThumbUp", '\uE8DC')
+val Icons.Outlined.ThumbUp: ImageVector get() = outlinedThumbUp
+
+private val outlinedPlayArrow: ImageVector = ImageVector.cn1Material("Outlined.PlayArrow", '\uE037')
+val Icons.Outlined.PlayArrow: ImageVector get() = outlinedPlayArrow
+
+private val outlinedKeyboardArrowRight: ImageVector = ImageVector.cn1Material("Outlined.KeyboardArrowRight", '\uE315')
+val Icons.Outlined.KeyboardArrowRight: ImageVector get() = outlinedKeyboardArrowRight
+
+private val outlinedKeyboardArrowDown: ImageVector = ImageVector.cn1Material("Outlined.KeyboardArrowDown", '\uE313')
+val Icons.Outlined.KeyboardArrowDown: ImageVector get() = outlinedKeyboardArrowDown
+
+private val outlinedAccountCircle: ImageVector = ImageVector.cn1Material("Outlined.AccountCircle", '\uE853')
+val Icons.Outlined.AccountCircle: ImageVector get() = outlinedAccountCircle
+
+private val outlinedCalendarToday: ImageVector = ImageVector.cn1Material("Outlined.CalendarToday", '\uE935')
+val Icons.Outlined.CalendarToday: ImageVector get() = outlinedCalendarToday
+
+private val outlinedEvent: ImageVector = ImageVector.cn1Material("Outlined.Event", '\uE878')
+val Icons.Outlined.Event: ImageVector get() = outlinedEvent
+
+private val outlinedMic: ImageVector = ImageVector.cn1Material("Outlined.Mic", '\uE029')
+val Icons.Outlined.Mic: ImageVector get() = outlinedMic
+
+private val outlinedBookmark: ImageVector = ImageVector.cn1Material("Outlined.Bookmark", '\uE867')
+val Icons.Outlined.Bookmark: ImageVector get() = outlinedBookmark
+
+private val outlinedBookmarkBorder: ImageVector = ImageVector.cn1Material("Outlined.BookmarkBorder", '\uE867')
+val Icons.Outlined.BookmarkBorder: ImageVector get() = outlinedBookmarkBorder
+
+private val outlinedFilterList: ImageVector = ImageVector.cn1Material("Outlined.FilterList", '\uE152')
+val Icons.Outlined.FilterList: ImageVector get() = outlinedFilterList
+
+private val outlinedClear: ImageVector = ImageVector.cn1Material("Outlined.Clear", '\uE14C')
+val Icons.Outlined.Clear: ImageVector get() = outlinedClear
+
+private val outlinedSend: ImageVector = ImageVector.cn1Material("Outlined.Send", '\uE163')
+val Icons.Outlined.Send: ImageVector get() = outlinedSend
+
+private val outlinedLocationOn: ImageVector = ImageVector.cn1Material("Outlined.LocationOn", '\uE0C8')
+val Icons.Outlined.LocationOn: ImageVector get() = outlinedLocationOn
+
+private val outlinedEmail: ImageVector = ImageVector.cn1Material("Outlined.Email", '\uE0BE')
+val Icons.Outlined.Email: ImageVector get() = outlinedEmail
+
+private val outlinedPhone: ImageVector = ImageVector.cn1Material("Outlined.Phone", '\uE0CD')
+val Icons.Outlined.Phone: ImageVector get() = outlinedPhone
+
+private val outlinedWarning: ImageVector = ImageVector.cn1Material("Outlined.Warning", '\uE002')
+val Icons.Outlined.Warning: ImageVector get() = outlinedWarning
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material3/ColorScheme.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/ColorScheme.kt
new file mode 100644
index 00000000000..cd040b70809
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material3
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+/// The Material 3 color roles. The defaults are the baseline scheme.
+@Immutable
+data class ColorScheme( val primary: Color,
+ val onPrimary: Color,
+ val primaryContainer: Color,
+ val onPrimaryContainer: Color,
+ val inversePrimary: Color,
+ val secondary: Color,
+ val onSecondary: Color,
+ val secondaryContainer: Color,
+ val onSecondaryContainer: Color,
+ val tertiary: Color,
+ val onTertiary: Color,
+ val tertiaryContainer: Color,
+ val onTertiaryContainer: Color,
+ val background: Color,
+ val onBackground: Color,
+ val surface: Color,
+ val onSurface: Color,
+ val surfaceVariant: Color,
+ val onSurfaceVariant: Color,
+ val surfaceTint: Color,
+ val inverseSurface: Color,
+ val inverseOnSurface: Color,
+ val error: Color,
+ val onError: Color,
+ val errorContainer: Color,
+ val onErrorContainer: Color,
+ val outline: Color,
+ val outlineVariant: Color,
+ val scrim: Color,
+ val surfaceBright: Color,
+ val surfaceDim: Color,
+ val surfaceContainer: Color,
+ val surfaceContainerHigh: Color,
+ val surfaceContainerHighest: Color,
+ val surfaceContainerLow: Color,
+ val surfaceContainerLowest: Color)
+
+fun lightColorScheme( primary: Color = Color(0xFF6750A4),
+ onPrimary: Color = Color(0xFFFFFFFF),
+ primaryContainer: Color = Color(0xFFEADDFF),
+ onPrimaryContainer: Color = Color(0xFF21005D),
+ inversePrimary: Color = Color(0xFFD0BCFF),
+ secondary: Color = Color(0xFF625B71),
+ onSecondary: Color = Color(0xFFFFFFFF),
+ secondaryContainer: Color = Color(0xFFE8DEF8),
+ onSecondaryContainer: Color = Color(0xFF1D192B),
+ tertiary: Color = Color(0xFF7D5260),
+ onTertiary: Color = Color(0xFFFFFFFF),
+ tertiaryContainer: Color = Color(0xFFFFD8E4),
+ onTertiaryContainer: Color = Color(0xFF31111D),
+ background: Color = Color(0xFFFEF7FF),
+ onBackground: Color = Color(0xFF1D1B20),
+ surface: Color = Color(0xFFFEF7FF),
+ onSurface: Color = Color(0xFF1D1B20),
+ surfaceVariant: Color = Color(0xFFE7E0EC),
+ onSurfaceVariant: Color = Color(0xFF49454F),
+ surfaceTint: Color = Color(0xFF6750A4),
+ inverseSurface: Color = Color(0xFF322F35),
+ inverseOnSurface: Color = Color(0xFFF5EFF7),
+ error: Color = Color(0xFFB3261E),
+ onError: Color = Color(0xFFFFFFFF),
+ errorContainer: Color = Color(0xFFF9DEDC),
+ onErrorContainer: Color = Color(0xFF410E0B),
+ outline: Color = Color(0xFF79747E),
+ outlineVariant: Color = Color(0xFFCAC4D0),
+ scrim: Color = Color(0xFF000000),
+ surfaceBright: Color = Color(0xFFFEF7FF),
+ surfaceDim: Color = Color(0xFFDED8E1),
+ surfaceContainer: Color = Color(0xFFF3EDF7),
+ surfaceContainerHigh: Color = Color(0xFFECE6F0),
+ surfaceContainerHighest: Color = Color(0xFFE6E0E9),
+ surfaceContainerLow: Color = Color(0xFFF7F2FA),
+ surfaceContainerLowest: Color = Color(0xFFFFFFFF)): ColorScheme = ColorScheme(primary, onPrimary, primaryContainer, onPrimaryContainer, inversePrimary, secondary, onSecondary, secondaryContainer, onSecondaryContainer, tertiary, onTertiary, tertiaryContainer, onTertiaryContainer, background, onBackground, surface, onSurface, surfaceVariant, onSurfaceVariant, surfaceTint, inverseSurface, inverseOnSurface, error, onError, errorContainer, onErrorContainer, outline, outlineVariant, scrim, surfaceBright, surfaceDim, surfaceContainer, surfaceContainerHigh, surfaceContainerHighest, surfaceContainerLow, surfaceContainerLowest)
+
+fun darkColorScheme( primary: Color = Color(0xFFD0BCFF),
+ onPrimary: Color = Color(0xFF381E72),
+ primaryContainer: Color = Color(0xFF4F378B),
+ onPrimaryContainer: Color = Color(0xFFEADDFF),
+ inversePrimary: Color = Color(0xFF6750A4),
+ secondary: Color = Color(0xFFCCC2DC),
+ onSecondary: Color = Color(0xFF332D41),
+ secondaryContainer: Color = Color(0xFF4A4458),
+ onSecondaryContainer: Color = Color(0xFFE8DEF8),
+ tertiary: Color = Color(0xFFEFB8C8),
+ onTertiary: Color = Color(0xFF492532),
+ tertiaryContainer: Color = Color(0xFF633B48),
+ onTertiaryContainer: Color = Color(0xFFFFD8E4),
+ background: Color = Color(0xFF141218),
+ onBackground: Color = Color(0xFFE6E0E9),
+ surface: Color = Color(0xFF141218),
+ onSurface: Color = Color(0xFFE6E0E9),
+ surfaceVariant: Color = Color(0xFF49454F),
+ onSurfaceVariant: Color = Color(0xFFCAC4D0),
+ surfaceTint: Color = Color(0xFFD0BCFF),
+ inverseSurface: Color = Color(0xFFE6E0E9),
+ inverseOnSurface: Color = Color(0xFF322F35),
+ error: Color = Color(0xFFF2B8B5),
+ onError: Color = Color(0xFF601410),
+ errorContainer: Color = Color(0xFF8C1D18),
+ onErrorContainer: Color = Color(0xFFF9DEDC),
+ outline: Color = Color(0xFF938F99),
+ outlineVariant: Color = Color(0xFF49454F),
+ scrim: Color = Color(0xFF000000),
+ surfaceBright: Color = Color(0xFF3B383E),
+ surfaceDim: Color = Color(0xFF141218),
+ surfaceContainer: Color = Color(0xFF211F26),
+ surfaceContainerHigh: Color = Color(0xFF2B2930),
+ surfaceContainerHighest: Color = Color(0xFF36343B),
+ surfaceContainerLow: Color = Color(0xFF1D1B20),
+ surfaceContainerLowest: Color = Color(0xFF0F0D13)): ColorScheme = ColorScheme(primary, onPrimary, primaryContainer, onPrimaryContainer, inversePrimary, secondary, onSecondary, secondaryContainer, onSecondaryContainer, tertiary, onTertiary, tertiaryContainer, onTertiaryContainer, background, onBackground, surface, onSurface, surfaceVariant, onSurfaceVariant, surfaceTint, inverseSurface, inverseOnSurface, error, onError, errorContainer, onErrorContainer, outline, outlineVariant, scrim, surfaceBright, surfaceDim, surfaceContainer, surfaceContainerHigh, surfaceContainerHighest, surfaceContainerLow, surfaceContainerLowest)
+
+/// The color that goes on `background`: its `on` role, or the content color in
+/// effect when the color is not one of the scheme's.
+fun ColorScheme.contentColorFor(backgroundColor: Color): Color = when (backgroundColor) {
+ primary -> onPrimary
+ secondary -> onSecondary
+ tertiary -> onTertiary
+ background -> onBackground
+ error -> onError
+ primaryContainer -> onPrimaryContainer
+ secondaryContainer -> onSecondaryContainer
+ tertiaryContainer -> onTertiaryContainer
+ errorContainer -> onErrorContainer
+ inverseSurface -> inverseOnSurface
+ surface, surfaceContainer, surfaceContainerHigh, surfaceContainerHighest, surfaceContainerLow,
+ surfaceContainerLowest, surfaceBright, surfaceDim -> onSurface
+ surfaceVariant -> onSurfaceVariant
+ else -> Color.Unspecified
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Components.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Components.kt
new file mode 100644
index 00000000000..b8ae21b434d
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Components.kt
@@ -0,0 +1,411 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material3
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.animation.core.animateFloat
+import com.codename1.compose.internal.IconNode
+import com.codename1.compose.internal.NodeApplier
+
+// --- text and icons ---------------------------------------------------------
+
+@Composable
+fun Text(
+ text: String,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ fontSize: TextUnit = TextUnit.Unspecified,
+ fontWeight: FontWeight? = null,
+ textAlign: TextAlign = TextAlign.Unspecified,
+ lineHeight: TextUnit = TextUnit.Unspecified,
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ style: TextStyle = LocalTextStyle.current
+) {
+ val c = if (color != Color.Unspecified) color else if (style.color != Color.Unspecified) style.color else LocalContentColor.current
+ val merged = style.merge(TextStyle(color = c, fontSize = fontSize, fontWeight = fontWeight, textAlign = textAlign,
+ lineHeight = lineHeight))
+ BasicText(text, modifier, merged, overflow, softWrap, maxLines)
+}
+
+@Composable
+fun Icon(imageVector: ImageVector, contentDescription: String?, modifier: Modifier = Modifier,
+ tint: Color = LocalContentColor.current) {
+ ComposeNode(
+ factory = { IconNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(imageVector) { this.glyph = com.codename1.compose.internal.Glyphs.of(it) }
+ set(tint) { this.tint = it }
+ }
+ )
+}
+
+// --- surfaces ---------------------------------------------------------------
+
+@Composable
+fun Surface(
+ modifier: Modifier = Modifier,
+ shape: Shape = RectangleShape,
+ color: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(color),
+ tonalElevation: Dp = 0.dp,
+ shadowElevation: Dp = 0.dp,
+ border: BorderStroke? = null,
+ content: @Composable () -> Unit
+) {
+ val m = modifier.surface(shape, tint(color, tonalElevation), border)
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ Box(m, propagateMinConstraints = true) { content() }
+ }
+}
+
+@Composable
+fun Surface(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = RectangleShape,
+ color: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(color),
+ tonalElevation: Dp = 0.dp,
+ shadowElevation: Dp = 0.dp,
+ border: BorderStroke? = null,
+ content: @Composable () -> Unit
+) {
+ val m = modifier.surface(shape, tint(color, tonalElevation), border).clickable(enabled, onClick = onClick)
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ Box(m, propagateMinConstraints = true) { content() }
+ }
+}
+
+private fun Modifier.surface(shape: Shape, color: Color, border: BorderStroke?): Modifier {
+ var m = this.clip(shape).background(color, shape)
+ if (border != null) m = m.border(border.width, border.color, shape)
+ return m
+}
+
+@Composable
+private fun tint(color: Color, elevation: Dp): Color {
+ val cs = MaterialTheme.colorScheme
+ if (color != cs.surface || elevation.value <= 0f) return color
+ val f = Math.min(0.14f, elevation.value * 0.02f)
+ return androidx.compose.ui.graphics.lerp(color, cs.surfaceTint, f)
+}
+
+@Immutable
+class CardColors(val containerColor: Color, val contentColor: Color)
+
+object CardDefaults {
+ val shape: Shape @Composable get() = MaterialTheme.shapes.medium
+
+ @Composable
+ fun cardColors(containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ contentColor: Color = contentColorFor(containerColor)): CardColors = CardColors(containerColor, contentColor)
+
+ @Composable
+ fun elevatedCardColors(containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
+ contentColor: Color = contentColorFor(containerColor)): CardColors = CardColors(containerColor, contentColor)
+
+ @Composable
+ fun outlinedCardColors(containerColor: Color = MaterialTheme.colorScheme.surface,
+ contentColor: Color = contentColorFor(containerColor)): CardColors = CardColors(containerColor, contentColor)
+
+ @Composable
+ fun cardElevation(defaultElevation: Dp = 0.dp): Dp = defaultElevation
+
+ @Composable
+ fun elevatedCardElevation(defaultElevation: Dp = 1.dp): Dp = defaultElevation
+}
+
+@Composable
+fun Card(modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape, colors: CardColors = CardDefaults.cardColors(),
+ elevation: Dp = 0.dp, border: BorderStroke? = null, content: @Composable ColumnScope.() -> Unit) {
+ Surface(modifier, shape, colors.containerColor, colors.contentColor, elevation, border = border) {
+ Column(content = content)
+ }
+}
+
+@Composable
+fun Card(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = CardDefaults.shape,
+ colors: CardColors = CardDefaults.cardColors(), elevation: Dp = 0.dp, border: BorderStroke? = null,
+ content: @Composable ColumnScope.() -> Unit) {
+ Surface(onClick, modifier, enabled, shape, colors.containerColor, colors.contentColor, elevation, border = border) {
+ Column(content = content)
+ }
+}
+
+@Composable
+fun ElevatedCard(modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape,
+ colors: CardColors = CardDefaults.elevatedCardColors(), content: @Composable ColumnScope.() -> Unit) =
+ Card(modifier, shape, colors, 1.dp, null, content)
+
+@Composable
+fun OutlinedCard(modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape,
+ colors: CardColors = CardDefaults.outlinedCardColors(), content: @Composable ColumnScope.() -> Unit) =
+ Card(modifier, shape, colors, 0.dp, BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), content)
+
+// --- buttons ----------------------------------------------------------------
+
+@Immutable
+class ButtonColors(val containerColor: Color, val contentColor: Color, val disabledContainerColor: Color,
+ val disabledContentColor: Color) {
+ internal fun container(enabled: Boolean) = if (enabled) containerColor else disabledContainerColor
+ internal fun content(enabled: Boolean) = if (enabled) contentColor else disabledContentColor
+}
+
+object ButtonDefaults {
+ val ContentPadding: PaddingValues = PaddingValues(start = 24.dp, top = 8.dp, end = 24.dp, bottom = 8.dp)
+ val TextButtonContentPadding: PaddingValues = PaddingValues(start = 12.dp, top = 8.dp, end = 12.dp, bottom = 8.dp)
+ val MinWidth: Dp = 58.dp
+ val MinHeight: Dp = 40.dp
+ val IconSize: Dp = 18.dp
+ val IconSpacing: Dp = 8.dp
+ val shape: Shape @Composable get() = CircleShape
+
+ @Composable
+ fun buttonColors(containerColor: Color = MaterialTheme.colorScheme.primary,
+ contentColor: Color = MaterialTheme.colorScheme.onPrimary,
+ disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
+ disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)): ButtonColors =
+ ButtonColors(containerColor, contentColor, disabledContainerColor, disabledContentColor)
+
+ @Composable
+ fun filledTonalButtonColors(containerColor: Color = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor: Color = MaterialTheme.colorScheme.onSecondaryContainer): ButtonColors =
+ buttonColors(containerColor, contentColor)
+
+ @Composable
+ fun outlinedButtonColors(containerColor: Color = Color.Transparent,
+ contentColor: Color = MaterialTheme.colorScheme.primary): ButtonColors =
+ ButtonColors(containerColor, contentColor, Color.Transparent, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f))
+
+ @Composable
+ fun textButtonColors(containerColor: Color = Color.Transparent,
+ contentColor: Color = MaterialTheme.colorScheme.primary): ButtonColors =
+ ButtonColors(containerColor, contentColor, Color.Transparent, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f))
+
+ @Composable
+ fun elevatedButtonColors(containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
+ contentColor: Color = MaterialTheme.colorScheme.primary): ButtonColors =
+ buttonColors(containerColor, contentColor)
+}
+
+@Composable
+fun Button(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ shape: Shape = ButtonDefaults.shape,
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
+ border: BorderStroke? = null,
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+ content: @Composable RowScope.() -> Unit
+) {
+ Surface(onClick, modifier, enabled, shape, colors.container(enabled), colors.content(enabled), border = border) {
+ ProvideTextStyle(MaterialTheme.typography.labelLarge) {
+ Row(Modifier.defaultMinSize(ButtonDefaults.MinWidth, ButtonDefaults.MinHeight).padding(contentPadding),
+ horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content)
+ }
+ }
+}
+
+@Composable
+fun FilledTonalButton(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit) =
+ Button(onClick, modifier, enabled, shape, colors, null, contentPadding, content)
+
+@Composable
+fun ElevatedButton(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.elevatedButtonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit) =
+ Button(onClick, modifier, enabled, shape, colors, null, contentPadding, content)
+
+@Composable
+fun OutlinedButton(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.outlinedButtonColors(),
+ border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit) =
+ Button(onClick, modifier, enabled, shape, colors, border, contentPadding, content)
+
+@Composable
+fun TextButton(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.textButtonColors(),
+ contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, content: @Composable RowScope.() -> Unit) =
+ Button(onClick, modifier, enabled, shape, colors, null, contentPadding, content)
+
+@Composable
+fun IconButton(onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable () -> Unit) {
+ val color = if (enabled) LocalContentColor.current else LocalContentColor.current.copy(alpha = 0.38f)
+ Box(modifier.size(40.dp).clip(CircleShape).clickable(enabled, onClick = onClick), contentAlignment = Alignment.Center) {
+ CompositionLocalProvider(LocalContentColor provides color) { content() }
+ }
+}
+
+@Composable
+fun FloatingActionButton(onClick: () -> Unit, modifier: Modifier = Modifier,
+ shape: Shape = MaterialTheme.shapes.large,
+ containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
+ contentColor: Color = contentColorFor(containerColor),
+ content: @Composable () -> Unit) {
+ Surface(onClick, modifier, true, shape, containerColor, contentColor, 6.dp) {
+ Box(Modifier.defaultMinSize(56.dp, 56.dp), contentAlignment = Alignment.Center) { content() }
+ }
+}
+
+@Composable
+fun ExtendedFloatingActionButton(text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit,
+ modifier: Modifier = Modifier, expanded: Boolean = true,
+ containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
+ contentColor: Color = contentColorFor(containerColor)) {
+ Surface(onClick, modifier, true, MaterialTheme.shapes.large, containerColor, contentColor, 6.dp) {
+ Row(Modifier.defaultMinSize(80.dp, 56.dp).padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
+ icon()
+ if (expanded) {
+ Spacer(Modifier.width(12.dp))
+ ProvideTextStyle(MaterialTheme.typography.labelLarge) { text() }
+ }
+ }
+ }
+}
+
+// --- chips ------------------------------------------------------------------
+
+@Composable
+fun FilterChip(selected: Boolean, onClick: () -> Unit, label: @Composable () -> Unit, modifier: Modifier = Modifier,
+ enabled: Boolean = true, leadingIcon: (@Composable () -> Unit)? = null,
+ trailingIcon: (@Composable () -> Unit)? = null) {
+ val cs = MaterialTheme.colorScheme
+ val container = if (selected) cs.secondaryContainer else Color.Transparent
+ val content = if (selected) cs.onSecondaryContainer else cs.onSurfaceVariant
+ Surface(onClick, modifier, enabled, MaterialTheme.shapes.small, container, content,
+ border = if (selected) null else BorderStroke(1.dp, cs.outline)) {
+ Row(Modifier.height(32.dp).padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (leadingIcon != null) Box(Modifier.size(18.dp)) { leadingIcon() }
+ ProvideTextStyle(MaterialTheme.typography.labelLarge) { label() }
+ if (trailingIcon != null) Box(Modifier.size(18.dp)) { trailingIcon() }
+ }
+ }
+}
+
+@Composable
+fun AssistChip(onClick: () -> Unit, label: @Composable () -> Unit, modifier: Modifier = Modifier,
+ leadingIcon: (@Composable () -> Unit)? = null) =
+ FilterChip(false, onClick, label, modifier, true, leadingIcon)
+
+// --- dividers and progress --------------------------------------------------
+
+@Composable
+fun HorizontalDivider(modifier: Modifier = Modifier, thickness: Dp = 1.dp,
+ color: Color = MaterialTheme.colorScheme.outlineVariant) {
+ Box(modifier.fillMaxWidth().height(thickness).background(color))
+}
+
+@Composable
+fun VerticalDivider(modifier: Modifier = Modifier, thickness: Dp = 1.dp,
+ color: Color = MaterialTheme.colorScheme.outlineVariant) {
+ Box(modifier.width(thickness).background(color))
+}
+
+@Composable
+fun LinearProgressIndicator(progress: () -> Float, modifier: Modifier = Modifier,
+ color: Color = MaterialTheme.colorScheme.primary,
+ trackColor: Color = MaterialTheme.colorScheme.surfaceContainerHighest) {
+ androidx.compose.foundation.Canvas(modifier.fillMaxWidth().height(4.dp)) {
+ drawRoundRect(trackColor, cornerRadius = androidx.compose.ui.geometry.CornerRadius(size.height / 2))
+ val p = Math.max(0f, Math.min(1f, progress()))
+ drawRoundRect(color, size = androidx.compose.ui.geometry.Size(size.width * p, size.height),
+ cornerRadius = androidx.compose.ui.geometry.CornerRadius(size.height / 2))
+ }
+}
+
+@Composable
+fun CircularProgressIndicator(modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.primary,
+ strokeWidth: Dp = 4.dp) {
+ val t = androidx.compose.animation.core.rememberInfiniteTransition()
+ val angle = t.animateFloat(0f, 360f, androidx.compose.animation.core.infiniteRepeatable(
+ androidx.compose.animation.core.tween(1100, easing = androidx.compose.animation.core.LinearEasing)))
+ androidx.compose.foundation.Canvas(modifier.size(40.dp)) {
+ val w = strokeWidth.toPx()
+ drawArc(color, angle.value, 270f, false, androidx.compose.ui.geometry.Offset(w / 2, w / 2),
+ androidx.compose.ui.geometry.Size(size.width - w, size.height - w),
+ style = androidx.compose.ui.graphics.drawscope.Stroke(w, androidx.compose.ui.graphics.StrokeCap.Round))
+ }
+}
+
+@Composable
+fun CircularProgressIndicator(progress: () -> Float, modifier: Modifier = Modifier,
+ color: Color = MaterialTheme.colorScheme.primary, strokeWidth: Dp = 4.dp,
+ trackColor: Color = MaterialTheme.colorScheme.surfaceContainerHighest) {
+ androidx.compose.foundation.Canvas(modifier.size(40.dp)) {
+ val w = strokeWidth.toPx()
+ val o = androidx.compose.ui.geometry.Offset(w / 2, w / 2)
+ val s = androidx.compose.ui.geometry.Size(size.width - w, size.height - w)
+ val st = androidx.compose.ui.graphics.drawscope.Stroke(w, androidx.compose.ui.graphics.StrokeCap.Round)
+ drawArc(trackColor, 0f, 360f, false, o, s, style = st)
+ drawArc(color, -90f, 360f * Math.max(0f, Math.min(1f, progress())), false, o, s, style = st)
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Structure.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Structure.kt
new file mode 100644
index 00000000000..9d21293e313
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Structure.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material3
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.animation.core.animateColorAsState
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.runtime.getValue
+import com.codename1.compose.internal.Fonts
+import com.codename1.compose.internal.SafeInsetsElement
+import com.codename1.compose.internal.NodeApplier
+import com.codename1.compose.internal.TextInputNode
+
+// --- scaffold ---------------------------------------------------------------
+
+@Composable
+fun Scaffold(
+ modifier: Modifier = Modifier,
+ topBar: @Composable () -> Unit = {},
+ bottomBar: @Composable () -> Unit = {},
+ snackbarHost: @Composable () -> Unit = {},
+ floatingActionButton: @Composable () -> Unit = {},
+ containerColor: Color = MaterialTheme.colorScheme.background,
+ contentColor: Color = contentColorFor(containerColor),
+ content: @Composable (PaddingValues) -> Unit
+) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ Column(modifier.fillMaxSize().background(containerColor)) {
+ topBar()
+ Box(Modifier.weight(1f).fillMaxWidth()) {
+ // The bars are laid out above and below the content, not over
+ // it, so the content needs no padding for them; upstream hands
+ // their heights and the window insets here because its bars
+ // overlap the content. A Scaffold with no top bar in a host under
+ // the status bar therefore starts its content there; the top bar
+ // is what pads for it.
+ Box(Modifier.fillMaxSize()) { content(PaddingValues(0.dp)) }
+ Box(Modifier.align(Alignment.BottomEnd).padding(16.dp)) { floatingActionButton() }
+ Box(Modifier.align(Alignment.BottomCenter)) { snackbarHost() }
+ }
+ bottomBar()
+ }
+ }
+}
+
+// --- app bars ---------------------------------------------------------------
+
+@Immutable
+class TopAppBarColors(val containerColor: Color, val titleContentColor: Color, val navigationIconContentColor: Color,
+ val actionIconContentColor: Color)
+
+object TopAppBarDefaults {
+ @Composable
+ fun topAppBarColors(containerColor: Color = MaterialTheme.colorScheme.surface,
+ titleContentColor: Color = MaterialTheme.colorScheme.onSurface,
+ navigationIconContentColor: Color = MaterialTheme.colorScheme.onSurface,
+ actionIconContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant): TopAppBarColors =
+ TopAppBarColors(containerColor, titleContentColor, navigationIconContentColor, actionIconContentColor)
+
+ @Composable
+ fun centerAlignedTopAppBarColors(containerColor: Color = MaterialTheme.colorScheme.surface,
+ titleContentColor: Color = MaterialTheme.colorScheme.onSurface): TopAppBarColors =
+ topAppBarColors(containerColor, titleContentColor)
+}
+
+@Composable
+fun TopAppBar(
+ title: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors()
+) = AppBar(title, modifier, navigationIcon, actions, colors, false)
+
+@Composable
+fun CenterAlignedTopAppBar(
+ title: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ navigationIcon: @Composable () -> Unit = {},
+ actions: @Composable RowScope.() -> Unit = {},
+ colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors()
+) = AppBar(title, modifier, navigationIcon, actions, colors, true)
+
+@Composable
+private fun AppBar(title: @Composable () -> Unit, modifier: Modifier, navigationIcon: @Composable () -> Unit,
+ actions: @Composable RowScope.() -> Unit, colors: TopAppBarColors, centered: Boolean) {
+ Row(modifier.fillMaxWidth().background(colors.containerColor).then(SafeInsetsElement(top = true, bottom = false)).height(64.dp)
+ .padding(horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically) {
+ CompositionLocalProvider(LocalContentColor provides colors.navigationIconContentColor) { navigationIcon() }
+ Box(Modifier.weight(1f).padding(horizontal = 12.dp),
+ contentAlignment = if (centered) Alignment.Center else Alignment.CenterStart) {
+ CompositionLocalProvider(LocalContentColor provides colors.titleContentColor) {
+ ProvideTextStyle(MaterialTheme.typography.titleLarge) { title() }
+ }
+ }
+ CompositionLocalProvider(LocalContentColor provides colors.actionIconContentColor) {
+ Row(verticalAlignment = Alignment.CenterVertically, content = actions)
+ }
+ }
+}
+
+// --- navigation bar ---------------------------------------------------------
+
+@Composable
+fun NavigationBar(modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
+ contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, content: @Composable RowScope.() -> Unit) {
+ CompositionLocalProvider(LocalContentColor provides contentColor) {
+ Row(modifier.fillMaxWidth().background(containerColor).then(SafeInsetsElement(top = false, bottom = true)).height(80.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, content = content)
+ }
+}
+
+@Composable
+fun RowScope.NavigationBarItem(selected: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit,
+ modifier: Modifier = Modifier, enabled: Boolean = true,
+ label: (@Composable () -> Unit)? = null, alwaysShowLabel: Boolean = true) {
+ val cs = MaterialTheme.colorScheme
+ val indicator by animateColorAsState(if (selected) cs.secondaryContainer else cs.secondaryContainer.copy(alpha = 0f))
+ Column(modifier.weight(1f).fillMaxSize().clickable(enabled, onClick = onClick),
+ horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ Box(Modifier.size(64.dp, 32.dp).background(indicator, CircleShape), contentAlignment = Alignment.Center) {
+ CompositionLocalProvider(LocalContentColor provides if (selected) cs.onSecondaryContainer else cs.onSurfaceVariant) {
+ icon()
+ }
+ }
+ if (label != null && (alwaysShowLabel || selected)) {
+ Spacer(Modifier.height(4.dp))
+ CompositionLocalProvider(LocalContentColor provides if (selected) cs.onSurface else cs.onSurfaceVariant) {
+ ProvideTextStyle(MaterialTheme.typography.labelMedium) { label() }
+ }
+ }
+ }
+}
+
+// --- selection controls -----------------------------------------------------
+
+@Composable
+fun Switch(checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true) {
+ val cs = MaterialTheme.colorScheme
+ val track by animateColorAsState(if (checked) cs.primary else cs.surfaceContainerHighest)
+ val thumb by animateColorAsState(if (checked) cs.onPrimary else cs.outline)
+ val offset by animateDpAsState(if (checked) 24.dp else 4.dp)
+ val thumbSize by animateDpAsState(if (checked) 24.dp else 16.dp)
+ var m = modifier.size(52.dp, 32.dp).clip(CircleShape).background(track, CircleShape)
+ if (!checked) m = m.border(2.dp, cs.outline, CircleShape)
+ if (onCheckedChange != null) m = m.clickable(enabled) { onCheckedChange(!checked) }
+ Box(m, contentAlignment = Alignment.CenterStart) {
+ Box(Modifier.offset(x = offset + (24.dp - thumbSize) / 2).size(thumbSize).background(thumb, CircleShape))
+ }
+}
+
+@Composable
+fun Checkbox(checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true) {
+ val cs = MaterialTheme.colorScheme
+ var m = modifier.size(40.dp)
+ if (onCheckedChange != null) m = m.clip(CircleShape).clickable(enabled) { onCheckedChange(!checked) }
+ Box(m, contentAlignment = Alignment.Center) {
+ val box = Modifier.size(18.dp).clip(RoundedCornerShape(2.dp))
+ if (checked) {
+ Box(box.background(cs.primary), contentAlignment = Alignment.Center) {
+ Icon(androidx.compose.ui.graphics.vector.ImageVector.cn1Material("Check", '\ue5ca'), null,
+ Modifier.size(16.dp), cs.onPrimary)
+ }
+ } else {
+ Box(box.border(2.dp, cs.onSurfaceVariant, RoundedCornerShape(2.dp)))
+ }
+ }
+}
+
+// --- text fields ------------------------------------------------------------
+
+@Composable
+private fun TextInput(value: String, onValueChange: (String) -> Unit, modifier: Modifier, enabled: Boolean,
+ singleLine: Boolean, hint: String, style: TextStyle, color: Color) {
+ val font = Fonts.get(Math.max(1, Math.round((if (style.fontSize.isSpecified) style.fontSize.value else 16f) *
+ com.codename1.compose.internal.Density.density)), style.fontWeight)
+ ComposeNode(
+ factory = { TextInputNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(value) { this.setValue(it) }
+ set(onValueChange) { this.onValueChange = it }
+ set(enabled) { this.enabledInput = it }
+ set(singleLine) { this.singleLine = it }
+ set(hint) { this.hint = it }
+ set(color) { this.textColor = it }
+ set(font) { this.font = it }
+ }
+ )
+}
+
+@Composable
+fun BasicTextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ textStyle: TextStyle = TextStyle.Default, singleLine: Boolean = false) {
+ TextInput(value, onValueChange, modifier, enabled, singleLine, "", textStyle,
+ if (textStyle.color != Color.Unspecified) textStyle.color else LocalContentColor.current)
+}
+
+@Composable
+private fun DecoratedField(value: String, onValueChange: (String) -> Unit, modifier: Modifier, enabled: Boolean,
+ label: (@Composable () -> Unit)?, placeholder: (@Composable () -> Unit)?, leadingIcon: (@Composable () -> Unit)?,
+ trailingIcon: (@Composable () -> Unit)?, singleLine: Boolean, shape: Shape,
+ container: Color, outline: Color?) {
+ val cs = MaterialTheme.colorScheme
+ var m = modifier.clip(shape).background(container, shape)
+ if (outline != null) m = m.border(1.dp, outline, shape)
+ Row(m.height(if (singleLine) 56.dp else 112.dp).padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+ if (leadingIcon != null) {
+ CompositionLocalProvider(LocalContentColor provides cs.onSurfaceVariant) { leadingIcon() }
+ Spacer(Modifier.width(12.dp))
+ }
+ Column(Modifier.weight(1f)) {
+ if (label != null && value.isNotEmpty()) {
+ CompositionLocalProvider(LocalContentColor provides cs.primary) {
+ ProvideTextStyle(MaterialTheme.typography.bodySmall) { label() }
+ }
+ }
+ Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {
+ TextInput(value, onValueChange, Modifier.fillMaxWidth(), enabled, singleLine, "",
+ MaterialTheme.typography.bodyLarge, cs.onSurface)
+ // The placeholder, or the label while there is no value, paints over the
+ // empty field; text takes no touches, so the field still gets them.
+ val hint = placeholder ?: label
+ if (value.isEmpty() && hint != null) {
+ CompositionLocalProvider(LocalContentColor provides cs.onSurfaceVariant) {
+ ProvideTextStyle(MaterialTheme.typography.bodyLarge) { hint() }
+ }
+ }
+ }
+ }
+ if (trailingIcon != null) {
+ Spacer(Modifier.width(12.dp))
+ CompositionLocalProvider(LocalContentColor provides cs.onSurfaceVariant) { trailingIcon() }
+ }
+ }
+}
+
+/// A filled text field. The placeholder (or the label) shows while the field is empty.
+/// Editing is the platform's own: the field is a Codename One `TextField`.
+@Composable
+fun TextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ label: (@Composable () -> Unit)? = null, placeholder: (@Composable () -> Unit)? = null,
+ leadingIcon: (@Composable () -> Unit)? = null, trailingIcon: (@Composable () -> Unit)? = null,
+ singleLine: Boolean = false, shape: Shape = RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) {
+ DecoratedField(value, onValueChange, modifier, enabled, label, placeholder, leadingIcon, trailingIcon, singleLine,
+ shape, MaterialTheme.colorScheme.surfaceContainerHighest, null)
+}
+
+@Composable
+fun OutlinedTextField(value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true,
+ label: (@Composable () -> Unit)? = null, placeholder: (@Composable () -> Unit)? = null,
+ leadingIcon: (@Composable () -> Unit)? = null, trailingIcon: (@Composable () -> Unit)? = null,
+ singleLine: Boolean = false, shape: Shape = RoundedCornerShape(4.dp)) {
+ DecoratedField(value, onValueChange, modifier, enabled, label, placeholder, leadingIcon, trailingIcon, singleLine,
+ shape, Color.Transparent, MaterialTheme.colorScheme.outline)
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Theme.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Theme.kt
new file mode 100644
index 00000000000..6becd25c2bb
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Theme.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material3
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+
+/// The Material 3 shape scale.
+@Immutable
+data class Shapes(
+ val extraSmall: RoundedCornerShape = RoundedCornerShape(4.dp),
+ val small: RoundedCornerShape = RoundedCornerShape(8.dp),
+ val medium: RoundedCornerShape = RoundedCornerShape(12.dp),
+ val large: RoundedCornerShape = RoundedCornerShape(16.dp),
+ val extraLarge: RoundedCornerShape = RoundedCornerShape(28.dp)
+)
+
+internal val LocalColorScheme = staticCompositionLocalOf { lightColorScheme() }
+internal val LocalTypography = staticCompositionLocalOf { Typography() }
+internal val LocalShapes = staticCompositionLocalOf { Shapes() }
+
+/// The color content is drawn in: text and icons read it when no color is given.
+val LocalContentColor = compositionLocalOf { Color.Black }
+
+/// The style [Text] starts from.
+val LocalTextStyle = compositionLocalOf { TextStyle.Default }
+
+@Composable
+fun MaterialTheme(
+ colorScheme: ColorScheme = MaterialTheme.colorScheme,
+ shapes: Shapes = MaterialTheme.shapes,
+ typography: Typography = MaterialTheme.typography,
+ content: @Composable () -> Unit
+) {
+ CompositionLocalProvider(
+ LocalColorScheme provides colorScheme,
+ LocalTypography provides typography,
+ LocalShapes provides shapes,
+ LocalContentColor provides colorScheme.onSurface,
+ LocalTextStyle provides typography.bodyLarge
+ ) {
+ content()
+ }
+}
+
+object MaterialTheme {
+ val colorScheme: ColorScheme
+ @Composable @ReadOnlyComposable get() = LocalColorScheme.current
+ val typography: Typography
+ @Composable @ReadOnlyComposable get() = LocalTypography.current
+ val shapes: Shapes
+ @Composable @ReadOnlyComposable get() = LocalShapes.current
+}
+
+@Composable
+fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
+ val merged = LocalTextStyle.current.merge(value)
+ CompositionLocalProvider(LocalTextStyle provides merged, content = content)
+}
+
+@Composable
+@ReadOnlyComposable
+fun contentColorFor(backgroundColor: Color): Color {
+ val c = MaterialTheme.colorScheme.contentColorFor(backgroundColor)
+ return if (c == Color.Unspecified) LocalContentColor.current else c
+}
+
+internal val CornerFull = RoundedCornerShape(CornerSize(50))
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Typography.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Typography.kt
new file mode 100644
index 00000000000..446c824a7fd
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/material3/Typography.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.material3
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+/// The Material 3 type scale; the defaults are the baseline sizes.
+@Immutable
+data class Typography(
+ val displayLarge: TextStyle = TextStyle(fontSize = 57.sp, lineHeight = 64.sp, fontWeight = FontWeight(400)),
+ val displayMedium: TextStyle = TextStyle(fontSize = 45.sp, lineHeight = 52.sp, fontWeight = FontWeight(400)),
+ val displaySmall: TextStyle = TextStyle(fontSize = 36.sp, lineHeight = 44.sp, fontWeight = FontWeight(400)),
+ val headlineLarge: TextStyle = TextStyle(fontSize = 32.sp, lineHeight = 40.sp, fontWeight = FontWeight(400)),
+ val headlineMedium: TextStyle = TextStyle(fontSize = 28.sp, lineHeight = 36.sp, fontWeight = FontWeight(400)),
+ val headlineSmall: TextStyle = TextStyle(fontSize = 24.sp, lineHeight = 32.sp, fontWeight = FontWeight(400)),
+ val titleLarge: TextStyle = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight(400)),
+ val titleMedium: TextStyle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight(500)),
+ val titleSmall: TextStyle = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight(500)),
+ val bodyLarge: TextStyle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight(400)),
+ val bodyMedium: TextStyle = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight(400)),
+ val bodySmall: TextStyle = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight(400)),
+ val labelLarge: TextStyle = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight(500)),
+ val labelMedium: TextStyle = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight(500)),
+ val labelSmall: TextStyle = TextStyle(fontSize = 11.sp, lineHeight = 16.sp, fontWeight = FontWeight(500))
+)
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Alignment.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Alignment.kt
new file mode 100644
index 00000000000..e8048675804
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Alignment.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+
+/// Where a child of a given size goes in the space it has.
+@Immutable
+fun interface Alignment {
+ fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset
+
+ @Immutable
+ fun interface Horizontal {
+ fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int
+ }
+
+ @Immutable
+ fun interface Vertical {
+ fun align(size: Int, space: Int): Int
+ }
+
+ companion object {
+ val TopStart: Alignment = BiasAlignment(-1f, -1f)
+ val TopCenter: Alignment = BiasAlignment(0f, -1f)
+ val TopEnd: Alignment = BiasAlignment(1f, -1f)
+ val CenterStart: Alignment = BiasAlignment(-1f, 0f)
+ val Center: Alignment = BiasAlignment(0f, 0f)
+ val CenterEnd: Alignment = BiasAlignment(1f, 0f)
+ val BottomStart: Alignment = BiasAlignment(-1f, 1f)
+ val BottomCenter: Alignment = BiasAlignment(0f, 1f)
+ val BottomEnd: Alignment = BiasAlignment(1f, 1f)
+
+ val Top: Vertical = BiasAlignment.Vertical(-1f)
+ val CenterVertically: Vertical = BiasAlignment.Vertical(0f)
+ val Bottom: Vertical = BiasAlignment.Vertical(1f)
+
+ val Start: Horizontal = BiasAlignment.Horizontal(-1f)
+ val CenterHorizontally: Horizontal = BiasAlignment.Horizontal(0f)
+ val End: Horizontal = BiasAlignment.Horizontal(1f)
+ }
+}
+
+/// An alignment expressed as a bias: -1 is the start or top, 0 the center, 1 the
+/// end or bottom.
+@Immutable
+data class BiasAlignment(val horizontalBias: Float, val verticalBias: Float) : Alignment {
+ override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset {
+ val h = Horizontal(horizontalBias).align(size.width, space.width, layoutDirection)
+ val v = Vertical(verticalBias).align(size.height, space.height)
+ return IntOffset(h, v)
+ }
+
+ @Immutable
+ data class Horizontal(val bias: Float) : Alignment.Horizontal {
+ override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int {
+ val b = if (layoutDirection == LayoutDirection.Ltr) bias else -bias
+ return Math.round((space - size) / 2f * (1 + b))
+ }
+ }
+
+ @Immutable
+ data class Vertical(val bias: Float) : Alignment.Vertical {
+ override fun align(size: Int, space: Int): Int = Math.round((space - size) / 2f * (1 + bias))
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Modifier.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Modifier.kt
new file mode 100644
index 00000000000..61ca8377ed4
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/Modifier.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui
+
+import androidx.compose.runtime.Stable
+
+/// An ordered chain of elements that decorate or configure a layout: sizes,
+/// padding, backgrounds, click handling. Elements apply outermost first, so
+/// `Modifier.padding(8.dp).background(c)` paints the background inside the
+/// padding and `Modifier.background(c).padding(8.dp)` paints it under it.
+@Stable
+interface Modifier {
+ fun foldIn(initial: R, operation: (R, Element) -> R): R
+ fun foldOut(initial: R, operation: (Element, R) -> R): R
+ fun any(predicate: (Element) -> Boolean): Boolean
+ fun all(predicate: (Element) -> Boolean): Boolean
+
+ infix fun then(other: Modifier): Modifier =
+ if (other === Modifier) this else CombinedModifier(this, other)
+
+ interface Element : Modifier {
+ override fun foldIn(initial: R, operation: (R, Element) -> R): R = operation(initial, this)
+ override fun foldOut(initial: R, operation: (Element, R) -> R): R = operation(this, initial)
+ override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)
+ override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
+ }
+
+ /// The empty modifier, and the receiver every chain starts from.
+ companion object : Modifier {
+ override fun foldIn(initial: R, operation: (R, Element) -> R): R = initial
+ override fun foldOut(initial: R, operation: (Element, R) -> R): R = initial
+ override fun any(predicate: (Element) -> Boolean): Boolean = false
+ override fun all(predicate: (Element) -> Boolean): Boolean = true
+ override infix fun then(other: Modifier): Modifier = other
+ override fun toString(): String = "Modifier"
+ }
+}
+
+class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier {
+ override fun foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
+ inner.foldIn(outer.foldIn(initial, operation), operation)
+
+ override fun foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
+ outer.foldOut(inner.foldOut(initial, operation), operation)
+
+ override fun any(predicate: (Modifier.Element) -> Boolean): Boolean = outer.any(predicate) || inner.any(predicate)
+ override fun all(predicate: (Modifier.Element) -> Boolean): Boolean = outer.all(predicate) && inner.all(predicate)
+
+ override fun equals(other: Any?): Boolean =
+ other is CombinedModifier && outer == other.outer && inner == other.inner
+
+ override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
+
+ override fun toString(): String = "[" + foldIn("") { acc, e -> if (acc.isEmpty()) e.toString() else "$acc, $e" } + "]"
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/draw/Draw.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/draw/Draw.kt
new file mode 100644
index 00000000000..5e1c86aef9f
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/draw/Draw.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.draw
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import com.codename1.compose.internal.AlphaElement
+import com.codename1.compose.internal.ClipElement
+import com.codename1.compose.internal.DrawBehindElement
+
+fun Modifier.clip(shape: Shape): Modifier = this.then(ClipElement(shape))
+fun Modifier.alpha(alpha: Float): Modifier = this.then(AlphaElement(alpha))
+fun Modifier.drawBehind(onDraw: DrawScope.() -> Unit): Modifier = this.then(DrawBehindElement(onDraw))
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/geometry/Geometry.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/geometry/Geometry.kt
new file mode 100644
index 00000000000..42e2d9e8f25
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/geometry/Geometry.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.geometry
+
+/// A point in pixels, relative to the drawing area's top left corner.
+data class Offset(val x: Float, val y: Float) {
+ operator fun plus(other: Offset): Offset = Offset(x + other.x, y + other.y)
+ operator fun minus(other: Offset): Offset = Offset(x - other.x, y - other.y)
+ operator fun times(operand: Float): Offset = Offset(x * operand, y * operand)
+ operator fun div(operand: Float): Offset = Offset(x / operand, y / operand)
+ operator fun unaryMinus(): Offset = Offset(-x, -y)
+
+ companion object {
+ val Zero: Offset = Offset(0f, 0f)
+ }
+}
+
+/// A width and a height in pixels.
+data class Size(val width: Float, val height: Float) {
+ val minDimension: Float get() = Math.min(width, height)
+ val maxDimension: Float get() = Math.max(width, height)
+ val center: Offset get() = Offset(width / 2f, height / 2f)
+ operator fun times(operand: Float): Size = Size(width * operand, height * operand)
+ operator fun div(operand: Float): Size = Size(width / operand, height / operand)
+
+ companion object {
+ val Zero: Size = Size(0f, 0f)
+ }
+}
+
+/// A rectangle in pixels.
+data class Rect(val left: Float, val top: Float, val right: Float, val bottom: Float) {
+ constructor(offset: Offset, size: Size) : this(offset.x, offset.y, offset.x + size.width, offset.y + size.height)
+
+ val width: Float get() = right - left
+ val height: Float get() = bottom - top
+ val size: Size get() = Size(width, height)
+ val topLeft: Offset get() = Offset(left, top)
+ val center: Offset get() = Offset(left + width / 2f, top + height / 2f)
+}
+
+/// A corner radius in pixels.
+data class CornerRadius(val x: Float, val y: Float = x) {
+ companion object {
+ val Zero: CornerRadius = CornerRadius(0f, 0f)
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/Graphics.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/Graphics.kt
new file mode 100644
index 00000000000..7bebd1e0539
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/Graphics.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.graphics
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/// An sRGB color. The value is packed as Compose packs an sRGB color: the ARGB
+/// integer in the upper 32 bits.
+@JvmInline
+value class Color(val value: ULong) {
+ val alpha: Float get() = ((argb ushr 24) and 0xff) / 255f
+ val red: Float get() = ((argb ushr 16) and 0xff) / 255f
+ val green: Float get() = ((argb ushr 8) and 0xff) / 255f
+ val blue: Float get() = (argb and 0xff) / 255f
+
+ internal val argb: Int get() = (value shr 32).toInt()
+
+ fun copy(alpha: Float = this.alpha, red: Float = this.red, green: Float = this.green, blue: Float = this.blue): Color =
+ Color(red, green, blue, alpha)
+
+ override fun toString(): String =
+ if (this == Unspecified) "Color.Unspecified" else "Color(#" + Integer.toHexString(argb) + ")"
+
+ companion object {
+ val Black: Color = Color(0xFF000000)
+ val DarkGray: Color = Color(0xFF444444)
+ val Gray: Color = Color(0xFF888888)
+ val LightGray: Color = Color(0xFFCCCCCC)
+ val White: Color = Color(0xFFFFFFFF)
+ val Red: Color = Color(0xFFFF0000)
+ val Green: Color = Color(0xFF00FF00)
+ val Blue: Color = Color(0xFF0000FF)
+ val Yellow: Color = Color(0xFFFFFF00)
+ val Cyan: Color = Color(0xFF00FFFF)
+ val Magenta: Color = Color(0xFFFF00FF)
+ val Transparent: Color = Color(0x00000000)
+
+ /// Distinct from every real color: the default of a parameter that means
+ /// "use the theme's".
+ val Unspecified: Color = Color(0x10UL)
+ }
+}
+
+fun Color(color: Long): Color = Color(((color and 0xffffffffL).toULong()) shl 32)
+
+fun Color(color: Int): Color = Color((color.toLong() and 0xffffffffL).toULong() shl 32)
+
+fun Color(red: Float, green: Float, blue: Float, alpha: Float = 1f): Color {
+ fun c(v: Float): Int = Math.round(Math.max(0f, Math.min(1f, v)) * 255f)
+ return Color((c(alpha) shl 24) or (c(red) shl 16) or (c(green) shl 8) or c(blue))
+}
+
+fun Color(red: Int, green: Int, blue: Int, alpha: Int = 0xff): Color =
+ Color(((alpha and 0xff) shl 24) or ((red and 0xff) shl 16) or ((green and 0xff) shl 8) or (blue and 0xff))
+
+val Color.isSpecified: Boolean get() = this != Color.Unspecified
+val Color.isUnspecified: Boolean get() = this == Color.Unspecified
+
+inline fun Color.takeOrElse(block: () -> Color): Color = if (isSpecified) this else block()
+
+fun Color.toArgb(): Int = argb
+
+fun lerp(start: Color, stop: Color, fraction: Float): Color {
+ fun mix(a: Float, b: Float): Float = a + (b - a) * fraction
+ return Color(mix(start.red, stop.red), mix(start.green, stop.green), mix(start.blue, stop.blue),
+ mix(start.alpha, stop.alpha))
+}
+
+/// What fills a shape: one color, or a gradient sampled along an axis.
+sealed class Brush {
+ companion object {
+ fun verticalGradient(colors: List, startY: Float = 0f, endY: Float = Float.POSITIVE_INFINITY): Brush =
+ LinearGradient(colors, true)
+
+ fun horizontalGradient(colors: List, startX: Float = 0f, endX: Float = Float.POSITIVE_INFINITY): Brush =
+ LinearGradient(colors, false)
+
+ fun linearGradient(colors: List): Brush = LinearGradient(colors, true)
+ }
+}
+
+class SolidColor(val value: Color) : Brush()
+
+internal class LinearGradient(val colors: List, val vertical: Boolean) : Brush()
+
+/// The outline a shape resolves to at a given size.
+sealed class Outline {
+ class Rectangle(val width: Float, val height: Float) : Outline()
+
+ /// A rounded rectangle; each radius is in pixels.
+ class Rounded(
+ val width: Float, val height: Float,
+ val topLeft: Float, val topRight: Float, val bottomRight: Float, val bottomLeft: Float
+ ) : Outline()
+}
+
+interface Shape {
+ fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline
+}
+
+val RectangleShape: Shape = object : Shape {
+ override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline =
+ Outline.Rectangle(size.width, size.height)
+
+ override fun toString(): String = "RectangleShape"
+}
+
+/// A decoded bitmap. Wraps a Codename One image.
+class ImageBitmap internal constructor(internal val image: com.codename1.ui.Image) {
+ val width: Int get() = image.width
+ val height: Int get() = image.height
+
+ companion object {
+ /// Wraps an image the application already has, for [androidx.compose.foundation.Image].
+ fun fromCodenameOne(image: com.codename1.ui.Image): ImageBitmap = ImageBitmap(image)
+ }
+}
+
+fun com.codename1.ui.Image.asImageBitmap(): ImageBitmap = ImageBitmap(this)
+
+enum class StrokeCap { Butt, Round, Square }
+
+object DefaultAlpha {
+ const val value: Float = 1f
+}
+
+/// How a drawing combines with what is under it. Only normal source-over drawing
+/// is supported; the other modes draw as [SrcOver].
+enum class BlendMode { SrcOver, Src, Multiply, Screen }
+
+class ColorFilter private constructor(internal val tint: Color) {
+ companion object {
+ fun tint(color: Color, blendMode: BlendMode = BlendMode.SrcOver): ColorFilter = ColorFilter(color)
+ }
+}
+
+internal fun Offset.isFinite(): Boolean = !x.isInfinite() && !y.isInfinite()
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
new file mode 100644
index 00000000000..501c64ebdde
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/drawscope/DrawScope.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.graphics.drawscope
+
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+
+/// How a shape is drawn: filled, or stroked with a width in pixels.
+sealed class DrawStyle
+
+object Fill : DrawStyle()
+
+class Stroke(val width: Float = 0f, val cap: StrokeCap = StrokeCap.Butt) : DrawStyle()
+
+/// The surface a [androidx.compose.foundation.Canvas] or `drawBehind` draws on.
+/// Coordinates are pixels, from the top left corner of the drawing's box.
+interface DrawScope : Density {
+ val size: Size
+ val layoutDirection: LayoutDirection get() = LayoutDirection.Ltr
+ val center: Offset get() = Offset(size.width / 2f, size.height / 2f)
+
+ fun drawRect(color: Color, topLeft: Offset = Offset.Zero, size: Size = this.size, alpha: Float = 1f,
+ style: DrawStyle = Fill)
+
+ fun drawRoundRect(color: Color, topLeft: Offset = Offset.Zero, size: Size = this.size,
+ cornerRadius: CornerRadius = CornerRadius.Zero, alpha: Float = 1f, style: DrawStyle = Fill)
+
+ fun drawCircle(color: Color, radius: Float = size.minDimension / 2f, center: Offset = this.center,
+ alpha: Float = 1f, style: DrawStyle = Fill)
+
+ fun drawArc(color: Color, startAngle: Float, sweepAngle: Float, useCenter: Boolean,
+ topLeft: Offset = Offset.Zero, size: Size = this.size, alpha: Float = 1f, style: DrawStyle = Fill)
+
+ fun drawLine(color: Color, start: Offset, end: Offset, strokeWidth: Float = 0f,
+ cap: StrokeCap = StrokeCap.Butt, alpha: Float = 1f)
+
+ fun drawImage(image: ImageBitmap, topLeft: Offset = Offset.Zero, alpha: Float = 1f)
+
+ /// Draws `points` joined by straight segments.
+ fun drawPolyline(color: Color, points: List, strokeWidth: Float, alpha: Float = 1f)
+
+ /// Fills the polygon `points` describe.
+ fun drawPolygon(color: Color, points: List, alpha: Float = 1f)
+
+ fun translate(left: Float, top: Float, block: DrawScope.() -> Unit)
+}
+
+fun DrawScope.inset(horizontal: Float, vertical: Float, block: DrawScope.() -> Unit) {
+ translate(horizontal, vertical, block)
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
new file mode 100644
index 00000000000..7b820145851
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.graphics.vector
+
+import androidx.compose.runtime.Immutable
+
+/// A vector icon. The icons shipped here are glyphs of the material design font
+/// every Codename One application already carries, drawn at any size.
+@Immutable
+class ImageVector internal constructor(val name: String, internal val glyph: Char) {
+ override fun equals(other: Any?): Boolean = other is ImageVector && other.glyph == glyph
+
+ override fun hashCode(): Int = glyph.code
+
+ override fun toString(): String = "ImageVector($name)"
+
+ companion object {
+ /// An icon from any glyph of the material design font.
+ fun cn1Material(name: String, glyph: Char): ImageVector = ImageVector(name, glyph)
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/layout/ContentScale.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/layout/ContentScale.kt
new file mode 100644
index 00000000000..70ff46358f4
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/layout/ContentScale.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.layout
+
+/// How an image is scaled into the box it is given.
+class ContentScale private constructor(internal val name: String) {
+ override fun toString(): String = "ContentScale.$name"
+
+ companion object {
+ val Fit: ContentScale = ContentScale("Fit")
+ val Crop: ContentScale = ContentScale("Crop")
+ val FillBounds: ContentScale = ContentScale("FillBounds")
+ val FillWidth: ContentScale = ContentScale("FillWidth")
+ val Inside: ContentScale = ContentScale("Inside")
+ val None: ContentScale = ContentScale("None")
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/platform/Platform.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/platform/Platform.kt
new file mode 100644
index 00000000000..0b67bac43ad
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/platform/Platform.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.platform
+
+import androidx.compose.ui.Modifier
+import com.codename1.compose.internal.TestTagElement
+
+/// Names the node, for tests: the Codename One component's name is the tag.
+fun Modifier.testTag(tag: String): Modifier = this.then(TestTagElement(tag))
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/TextStyle.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/TextStyle.kt
new file mode 100644
index 00000000000..9b81453e08c
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/TextStyle.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.text
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.TextUnit
+
+/// How text is drawn. Unspecified values fall back to the style it is merged
+/// into, as in Compose.
+@Immutable
+data class TextStyle(
+ val color: Color = Color.Unspecified,
+ val fontSize: TextUnit = TextUnit.Unspecified,
+ val fontWeight: FontWeight? = null,
+ val fontStyle: FontStyle? = null,
+ val fontFamily: FontFamily? = null,
+ val letterSpacing: TextUnit = TextUnit.Unspecified,
+ val textDecoration: TextDecoration? = null,
+ val textAlign: TextAlign = TextAlign.Unspecified,
+ val lineHeight: TextUnit = TextUnit.Unspecified
+) {
+ fun merge(other: TextStyle?): TextStyle {
+ if (other == null) return this
+ return TextStyle(
+ color = if (other.color != Color.Unspecified) other.color else color,
+ fontSize = if (other.fontSize.isSpecified) other.fontSize else fontSize,
+ fontWeight = other.fontWeight ?: fontWeight,
+ fontStyle = other.fontStyle ?: fontStyle,
+ fontFamily = other.fontFamily ?: fontFamily,
+ letterSpacing = if (other.letterSpacing.isSpecified) other.letterSpacing else letterSpacing,
+ textDecoration = other.textDecoration ?: textDecoration,
+ textAlign = if (other.textAlign != TextAlign.Unspecified) other.textAlign else textAlign,
+ lineHeight = if (other.lineHeight.isSpecified) other.lineHeight else lineHeight
+ )
+ }
+
+ operator fun plus(other: TextStyle): TextStyle = merge(other)
+
+ companion object {
+ val Default: TextStyle = TextStyle()
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/font/FontWeight.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/font/FontWeight.kt
new file mode 100644
index 00000000000..002543c7785
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/font/FontWeight.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.text.font
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class FontWeight(val weight: Int) : Comparable {
+ override fun compareTo(other: FontWeight): Int = weight.compareTo(other.weight)
+
+ companion object {
+ val Thin: FontWeight = FontWeight(100)
+ val ExtraLight: FontWeight = FontWeight(200)
+ val Light: FontWeight = FontWeight(300)
+ val Normal: FontWeight = FontWeight(400)
+ val Medium: FontWeight = FontWeight(500)
+ val SemiBold: FontWeight = FontWeight(600)
+ val Bold: FontWeight = FontWeight(700)
+ val ExtraBold: FontWeight = FontWeight(800)
+ val Black: FontWeight = FontWeight(900)
+ }
+}
+
+@JvmInline
+value class FontStyle(val value: Int) {
+ companion object {
+ val Normal: FontStyle = FontStyle(0)
+ val Italic: FontStyle = FontStyle(1)
+ }
+}
+
+/// A font family. Only the platform's own families exist here: text is drawn
+/// with the device's native font.
+@Immutable
+class FontFamily private constructor(internal val name: String) {
+ override fun toString(): String = "FontFamily.$name"
+
+ companion object {
+ val Default: FontFamily = FontFamily("Default")
+ val SansSerif: FontFamily = FontFamily("SansSerif")
+ val Serif: FontFamily = FontFamily("Serif")
+ val Monospace: FontFamily = FontFamily("Monospace")
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/style/TextStyles.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/style/TextStyles.kt
new file mode 100644
index 00000000000..7cf2c5c1e3d
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/text/style/TextStyles.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.text.style
+
+@JvmInline
+value class TextAlign(val value: Int) {
+ companion object {
+ val Left: TextAlign = TextAlign(1)
+ val Right: TextAlign = TextAlign(2)
+ val Center: TextAlign = TextAlign(3)
+ val Justify: TextAlign = TextAlign(4)
+ val Start: TextAlign = TextAlign(5)
+ val End: TextAlign = TextAlign(6)
+ val Unspecified: TextAlign = TextAlign(0)
+ }
+}
+
+@JvmInline
+value class TextOverflow(val value: Int) {
+ companion object {
+ val Clip: TextOverflow = TextOverflow(1)
+ val Ellipsis: TextOverflow = TextOverflow(2)
+ val Visible: TextOverflow = TextOverflow(3)
+ }
+}
+
+@JvmInline
+value class TextDecoration(val mask: Int) {
+ companion object {
+ val None: TextDecoration = TextDecoration(0)
+ val Underline: TextDecoration = TextDecoration(1)
+ val LineThrough: TextDecoration = TextDecoration(2)
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/androidx/compose/ui/unit/Units.kt b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/unit/Units.kt
new file mode 100644
index 00000000000..a675221bf20
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/androidx/compose/ui/unit/Units.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package androidx.compose.ui.unit
+
+import com.codename1.compose.internal.Density as Cn1Density
+
+/// A density-independent length: 1/160 of an inch, the unit every size in a
+/// Compose layout is written in.
+@JvmInline
+value class Dp(val value: Float) : Comparable {
+ operator fun plus(other: Dp): Dp = Dp(value + other.value)
+ operator fun minus(other: Dp): Dp = Dp(value - other.value)
+ operator fun unaryMinus(): Dp = Dp(-value)
+ operator fun div(other: Float): Dp = Dp(value / other)
+ operator fun div(other: Int): Dp = Dp(value / other)
+ operator fun div(other: Dp): Float = value / other.value
+ operator fun times(other: Float): Dp = Dp(value * other)
+ operator fun times(other: Int): Dp = Dp(value * other)
+ override fun compareTo(other: Dp): Int = value.compareTo(other.value)
+ override fun toString(): String = if (value.isNaN()) "Dp.Unspecified" else "$value.dp"
+
+ companion object {
+ val Hairline: Dp = Dp(0f)
+ val Infinity: Dp = Dp(Float.POSITIVE_INFINITY)
+ val Unspecified: Dp = Dp(Float.NaN)
+ }
+}
+
+inline val Int.dp: Dp get() = Dp(this.toFloat())
+inline val Float.dp: Dp get() = Dp(this)
+inline val Double.dp: Dp get() = Dp(this.toFloat())
+
+operator fun Int.times(other: Dp): Dp = Dp(this * other.value)
+operator fun Float.times(other: Dp): Dp = Dp(this * other.value)
+
+fun min(a: Dp, b: Dp): Dp = if (a.value <= b.value) a else b
+fun max(a: Dp, b: Dp): Dp = if (a.value >= b.value) a else b
+
+val Dp.isSpecified: Boolean get() = !value.isNaN()
+val Dp.isUnspecified: Boolean get() = value.isNaN()
+
+/// A width and a height in [Dp].
+data class DpSize(val width: Dp, val height: Dp)
+
+/// A text size. Only scaled pixels are supported; `em` is treated as a multiple of
+/// the default body size.
+@JvmInline
+value class TextUnit internal constructor(internal val packed: Long) {
+ val value: Float get() = Float.fromBits((packed and 0xffffffffL).toInt())
+ val isSp: Boolean get() = (packed ushr 32) == 1L
+ val isEm: Boolean get() = (packed ushr 32) == 2L
+ val isUnspecified: Boolean get() = (packed ushr 32) == 0L
+ val isSpecified: Boolean get() = !isUnspecified
+ operator fun times(other: Float): TextUnit = textUnit(packed ushr 32, value * other)
+ override fun toString(): String = when {
+ isSp -> "$value.sp"
+ isEm -> "$value.em"
+ else -> "Unspecified"
+ }
+
+ companion object {
+ val Unspecified: TextUnit = TextUnit(0L)
+ }
+}
+
+internal fun textUnit(type: Long, value: Float): TextUnit =
+ TextUnit((type shl 32) or (value.toRawBits().toLong() and 0xffffffffL))
+
+val Int.sp: TextUnit get() = textUnit(1L, this.toFloat())
+val Float.sp: TextUnit get() = textUnit(1L, this)
+val Double.sp: TextUnit get() = textUnit(1L, this.toFloat())
+val Int.em: TextUnit get() = textUnit(2L, this.toFloat())
+val Float.em: TextUnit get() = textUnit(2L, this)
+val Double.em: TextUnit get() = textUnit(2L, this.toFloat())
+
+/// A size in pixels.
+data class IntSize(val width: Int, val height: Int) {
+ companion object {
+ val Zero: IntSize = IntSize(0, 0)
+ }
+}
+
+/// A position in pixels.
+data class IntOffset(val x: Int, val y: Int) {
+ companion object {
+ val Zero: IntOffset = IntOffset(0, 0)
+ }
+}
+
+enum class LayoutDirection { Ltr, Rtl }
+
+/// Converts between [Dp] and pixels on the current display.
+interface Density {
+ val density: Float
+ val fontScale: Float
+ fun Dp.toPx(): Float = value * density
+ fun Dp.roundToPx(): Int = Math.round(toPx())
+ fun TextUnit.toPx(): Float = value * density * fontScale
+ fun Int.toDp(): Dp = Dp(this / density)
+ fun Float.toDp(): Dp = Dp(this / density)
+}
+
+fun Density(density: Float, fontScale: Float = 1f): Density = object : Density {
+ override val density: Float = density
+ override val fontScale: Float = fontScale
+}
+
+/// The display's density, for code that converts by hand.
+val currentDisplayDensity: Density get() = Cn1Density.current
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/Compose.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/Compose.kt
new file mode 100644
index 00000000000..6bd73cf9acf
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/Compose.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose
+
+import androidx.compose.runtime.Composable
+import com.codename1.compose.internal.ComposeHost
+import com.codename1.ui.Container
+import com.codename1.ui.Form
+import com.codename1.ui.layouts.BorderLayout
+
+/// Embeds Compose content in a Codename One user interface. The content's nodes
+/// are real Codename One components, so a composition can sit beside any other
+/// component, and recomposes on the EDT.
+///
+/// ```kotlin
+/// val view = ComposeView { App() }
+/// form.add(BorderLayout.CENTER, view)
+/// ```
+class ComposeView(content: @Composable () -> Unit) : Container(BorderLayout()) {
+ private val host = ComposeHost()
+
+ init {
+ allStyles.setPadding(0, 0, 0, 0)
+ allStyles.setMargin(0, 0, 0, 0)
+ add(BorderLayout.CENTER, host)
+ host.setContent(content)
+ }
+
+ /// Replaces the content.
+ fun setContent(content: @Composable () -> Unit) {
+ host.setContent(content)
+ }
+
+ /// Ends the composition: its effects are cancelled and its nodes removed.
+ fun dispose() {
+ host.dispose()
+ }
+
+ /// Whether a recomposition or an animation frame is still pending; tests wait
+ /// for this to become false.
+ fun hasPendingWork(): Boolean = host.cn1HasPendingWork()
+
+ /// Delivers an animation frame now, for tests that do not wait for the EDT's
+ /// animation loop.
+ fun cn1Frame() {
+ host.sendFrame()
+ }
+}
+
+/// A form whose whole content is a composition: the usual way to start a
+/// Compose application on Codename One.
+///
+/// ```kotlin
+/// override fun runApp() {
+/// ComposeForm { App() }.show()
+/// }
+/// ```
+class ComposeForm(title: String = "", content: @Composable () -> Unit) : Form(title, BorderLayout()) {
+ val view: ComposeView = ComposeView(content)
+
+ init {
+ contentPane.allStyles.setPadding(0, 0, 0, 0)
+ if (title.isEmpty()) {
+ toolbar?.isHidden = true
+ titleArea.isVisible = false
+ }
+ add(BorderLayout.CENTER, view)
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/ComposeHost.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/ComposeHost.kt
new file mode 100644
index 00000000000..c2e686e75fe
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/ComposeHost.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.runtime.AbstractApplier
+import androidx.compose.runtime.BroadcastFrameClock
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.Recomposer
+import com.codename1.ui.Component
+import com.codename1.ui.Container
+import com.codename1.ui.geom.Dimension
+import com.codename1.ui.layouts.Layout
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+
+/// Inserts, moves and removes the components a composition emits.
+internal class NodeApplier(root: Container) : AbstractApplier(root) {
+ override fun insertTopDown(index: Int, instance: Component) {
+ // Children are attached bottom up, so a parent is complete when it is added.
+ }
+
+ override fun insertBottomUp(index: Int, instance: Component) {
+ val parent = current as? Container ?: return
+ if (index >= parent.componentCount) {
+ parent.addComponent(instance)
+ } else {
+ parent.addComponent(index, instance)
+ }
+ invalidate(parent)
+ }
+
+ override fun remove(index: Int, count: Int) {
+ val parent = current as? Container ?: return
+ for (i in 0 until count) {
+ parent.removeComponent(parent.getComponentAt(index))
+ }
+ invalidate(parent)
+ }
+
+ override fun move(from: Int, to: Int, count: Int) {
+ val parent = current as? Container ?: return
+ val moved = ArrayList(count)
+ for (i in 0 until count) {
+ moved.add(parent.getComponentAt(from))
+ parent.removeComponent(parent.getComponentAt(from))
+ }
+ val dest = if (from > to) to else to - count
+ for (i in 0 until count) {
+ parent.addComponent(dest + i, moved[i])
+ }
+ invalidate(parent)
+ }
+
+ override fun onClear() {
+ (root as? Container)?.removeAll()
+ }
+
+ override fun onEndChanges() {
+ val r = root
+ if (r is ComposeHost) {
+ r.requestRemeasure()
+ }
+ }
+
+ private fun invalidate(c: Container) {
+ if (c is UiNode) {
+ c.invalidateMeasure()
+ }
+ }
+}
+
+/// The Codename One component a composition lives in. Its children are the
+/// composition's top level nodes, stacked as a `Box` would stack them.
+internal class ComposeHost : Container(HostLayout) {
+ private val clock = BroadcastFrameClock { scheduleFrame() }
+ // Immediate, so a frame resumes its awaiters inside sendFrame. Dispatched,
+ // every resumption waited for the end of the EDT cycle, and so did the
+ // state it wrote: a change reached the screen three cycles after it was
+ // made. Now the recomposer applies what a frame callback writes in that
+ // same frame (see scheduleFrame for when the frame runs).
+ private val scope = CoroutineScope(EdtDispatcher.Immediate + clock + Job())
+ private val recomposer = Recomposer(scope.coroutineContext)
+ private var composition: Composition? = null
+ private var inFrame = false
+ private var sentInLayout = false
+ private var animatedOn: com.codename1.ui.Form? = null
+ private var frameRequested = false
+ private var remeasureInFrame = false
+
+ init {
+ cn1StripStyle(this)
+ GlobalSnapshotManager.ensureStarted()
+ scope.launch(start = CoroutineStart.UNDISPATCHED) {
+ recomposer.runRecomposeAndApplyChanges()
+ }
+ }
+
+ fun setContent(content: @Composable () -> Unit) {
+ val c = composition ?: Composition(NodeApplier(this), recomposer).also { composition = it }
+ c.setContent(content)
+ requestRemeasure()
+ }
+
+ fun dispose() {
+ composition?.dispose()
+ composition = null
+ recomposer.cancel()
+ scope.cancel()
+ animatedOn?.deregisterAnimated(this)
+ animatedOn = null
+ }
+
+ /// Whether a composition change, a state change or a frame is pending.
+ fun cn1HasPendingWork(): Boolean = recomposer.hasPendingWork || clock.hasAwaiters
+
+ // --- frames -------------------------------------------------------------
+
+ /// A frame runs at the start of the next EDT cycle's layout pass, as
+ /// Android's Choreographer runs a frame's callbacks before traversal: the
+ /// frame, recomposition, layout and paint then share one cycle. Sent from
+ /// `animate()`, which runs after the cycle has painted, a change waited a
+ /// whole cycle more. The cycle's own wait paces continuous animation:
+ /// `revalidateLater` does not wake the EDT. `animate()`, after the paint,
+ /// is the fallback for a cycle whose layout pass did not reach this host
+ /// (the form defers revalidation while it animates). A host that is not
+ /// showing gets no frames: its request waits, and `initComponent` sends it
+ /// when the host is shown again, so a hidden infinite animation costs
+ /// nothing.
+ private fun scheduleFrame() {
+ frameRequested = true
+ val f = componentForm
+ if (f != null && f.isVisible) {
+ if (animatedOn !== f) {
+ animatedOn?.deregisterAnimated(this)
+ f.registerAnimated(this)
+ animatedOn = f
+ }
+ if (!inFrame) {
+ revalidateLater()
+ }
+ }
+ }
+
+ internal fun sendFrame() {
+ if (frameRequested) {
+ frameRequested = false
+ clock.sendFrame(System.nanoTime())
+ }
+ }
+
+
+ override fun animate(): Boolean {
+ if (sentInLayout) {
+ sentInLayout = false
+ } else {
+ sendFrame()
+ }
+ return false
+ }
+
+ override fun deinitialize() {
+ animatedOn?.deregisterAnimated(this)
+ animatedOn = null
+ super.deinitialize()
+ }
+
+ override fun paint(g: com.codename1.ui.Graphics) {
+ super.paint(g)
+ }
+
+ override fun initComponent() {
+ super.initComponent()
+ if (frameRequested) {
+ scheduleFrame()
+ }
+ }
+
+ // --- measuring ----------------------------------------------------------
+
+ private var remeasurePending = false
+
+ fun requestRemeasure() {
+ if (inFrame) {
+ // This pass lays out what the frame applies, but the parent sized
+ // the host before it; layoutChildren asks the parent again.
+ remeasureInFrame = true
+ return
+ }
+ if (!remeasurePending) {
+ remeasurePending = true
+ setShouldCalcPreferredSize(true)
+ revalidateLater()
+ repaint()
+ }
+ }
+
+
+ /// The status bar and home indicator this host lies under, in pixels, as
+ /// of its last pass; read by its nodes' [SafeInsetsElement]s.
+ internal var insetTop = 0
+ internal var insetBottom = 0
+ private var insetsKnown = false
+
+ internal fun layoutChildren() {
+ if (frameRequested) {
+ // What the frame applies is measured by this pass, so it must not
+ // queue another; a frame asked for during it goes to the next cycle.
+ inFrame = true
+ try {
+ sendFrame()
+ } finally {
+ inFrame = false
+ }
+ sentInLayout = true
+ if (remeasureInFrame) {
+ // Next cycle, never inline: the parent is mid-layout.
+ // Only when the size really moved: most frames change content,
+ // not size, and a parent pass per frame is what this avoids.
+ remeasureInFrame = false
+ val before = preferredSize
+ val w = before.width
+ val h = before.height
+ setShouldCalcPreferredSize(true)
+ val after = preferredSize
+ if (after.width != w || after.height != h) {
+ parent?.revalidateLater()
+ }
+ }
+ if (frameRequested) {
+ revalidateLater()
+ }
+ }
+ remeasurePending = false
+ val insets = SafeInsets.of(absoluteY, height)
+ val top = (insets ushr 32).toInt()
+ val bottom = insets.toInt()
+ val changed = !insetsKnown || top != insetTop || bottom != insetBottom
+ insetTop = top
+ insetBottom = bottom
+ insetsKnown = true
+ if (changed) {
+ // A node's measure is cached by constraints, and a safe area that
+ // arrives late (or a rotation) changes no constraint: forget the
+ // measures of the nodes that pad by it. Forgetting every node's
+ // rebuilt the lazy lists' items and cost a megabyte at start-up.
+ // This pass measures them; the nodes must not ask for another.
+ remeasurePending = true
+ forgetMeasures(this)
+ remeasurePending = false
+ }
+ val c = Constraints(0, width, 0, height)
+ for (k in 0 until componentCount) {
+ val child = getComponentAt(k)
+ val s = measureChild(child, c)
+ child.setX(if (child is UiNode) child.offsetX else 0)
+ child.setY(if (child is UiNode) child.offsetY else 0)
+ child.setWidth(s.w())
+ child.setHeight(s.h())
+ if (child is UiNode) {
+ child.applyPlacements()
+ }
+ }
+ }
+
+ private fun forgetMeasures(c: Container) {
+ for (k in 0 until c.componentCount) {
+ val child = c.getComponentAt(k)
+ if (child is UiNode && child.usesSafeInsets) {
+ child.invalidateMeasure()
+ }
+ if (child is Container) {
+ forgetMeasures(child)
+ }
+ }
+ }
+
+ internal fun preferred(): Dimension {
+ var w = 0
+ var h = 0
+ val insets = SafeInsets.of(absoluteY, cn1DisplayHeight())
+ insetTop = (insets ushr 32).toInt()
+ insetBottom = insets.toInt()
+ // Measured with a guessed height: the next pass measures again.
+ insetsKnown = false
+ val c = Constraints(0, cn1DisplayWidth(), 0, Constraints.INF)
+ for (k in 0 until componentCount) {
+ val s = measureChild(getComponentAt(k), c)
+ w = Math.max(w, s.w())
+ h = Math.max(h, s.h())
+ }
+ return Dimension(w, h)
+ }
+}
+
+internal object HostLayout : Layout() {
+ override fun layoutContainer(parent: Container) {
+ // Checked, never cast: a failed cast does not throw on iOS.
+ (parent as? ComposeHost)?.layoutChildren()
+ }
+
+ override fun getPreferredSize(parent: Container): Dimension =
+ (parent as? ComposeHost)?.preferred() ?: Dimension(0, 0)
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Constraints.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Constraints.kt
new file mode 100644
index 00000000000..5921f0239a0
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Constraints.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import com.codename1.ui.CN
+
+/// The display's density in Compose terms: pixels per dp (1/160 inch). Read from
+/// the port once a display exists; one pixel per dp before that, which only a
+/// test that never starts a display sees.
+internal object Density : androidx.compose.ui.unit.Density {
+ private var cached = 0f
+
+ override val density: Float
+ get() {
+ if (cached == 0f) {
+ val perHundredMm = try {
+ CN.convertToPixels(100f)
+ } catch (t: Throwable) {
+ 0
+ }
+ // 1 dp = 1/160 in = 0.15875 mm.
+ cached = if (perHundredMm > 0) perHundredMm / 100f * 0.15875f else 1f
+ }
+ return cached
+ }
+
+ override val fontScale: Float get() = 1f
+
+ val current: androidx.compose.ui.unit.Density get() = this
+
+ fun px(dp: Dp): Int = if (dp.value.isNaN()) 0 else Math.round(dp.value * density)
+
+ fun pxF(dp: Dp): Float = if (dp.value.isNaN()) 0f else dp.value * density
+
+ /// For tests that measure in a known unit.
+ fun cn1Override(pixelsPerDp: Float) {
+ cached = pixelsPerDp
+ }
+}
+
+internal val LTR: LayoutDirection = LayoutDirection.Ltr
+
+/// What a parent allows a child to be, in pixels. [INF] marks an unbounded axis.
+internal class Constraints(val minWidth: Int, val maxWidth: Int, val minHeight: Int, val maxHeight: Int) {
+ val hasBoundedWidth: Boolean get() = maxWidth != INF
+ val hasBoundedHeight: Boolean get() = maxHeight != INF
+ val hasFixedWidth: Boolean get() = minWidth == maxWidth
+ val hasFixedHeight: Boolean get() = minHeight == maxHeight
+
+ fun constrainWidth(w: Int): Int = Math.max(minWidth, Math.min(maxWidth, w))
+ fun constrainHeight(h: Int): Int = Math.max(minHeight, Math.min(maxHeight, h))
+
+ fun copy(minWidth: Int = this.minWidth, maxWidth: Int = this.maxWidth,
+ minHeight: Int = this.minHeight, maxHeight: Int = this.maxHeight): Constraints =
+ Constraints(minWidth, Math.max(minWidth, maxWidth), minHeight, Math.max(minHeight, maxHeight))
+
+ /// Shrinks every bound by `horizontal` and `vertical` pixels (padding).
+ fun offset(horizontal: Int, vertical: Int): Constraints {
+ fun sub(v: Int, d: Int): Int = if (v == INF) INF else Math.max(0, v - d)
+ return Constraints(Math.max(0, minWidth - horizontal), sub(maxWidth, horizontal),
+ Math.max(0, minHeight - vertical), sub(maxHeight, vertical))
+ }
+
+ fun loose(): Constraints = Constraints(0, maxWidth, 0, maxHeight)
+
+ override fun toString(): String = "Constraints(w=$minWidth..${s(maxWidth)}, h=$minHeight..${s(maxHeight)})"
+
+ private fun s(v: Int): String = if (v == INF) "inf" else v.toString()
+
+ companion object {
+ const val INF: Int = Int.MAX_VALUE
+
+ fun fixed(w: Int, h: Int): Constraints = Constraints(w, w, h, h)
+ }
+}
+
+/// The parts of the display the system draws over (the status bar and the home
+/// indicator on a notched phone) that a composition's host actually lies under,
+/// in pixels. M3's top app bar and navigation bar pad themselves by these, as
+/// Compose's window insets make them do; each host keeps its own. Measured against the host, not the
+/// display: a host below a form's title area, in a dialog or mid-form is not
+/// under the status bar, and padding it by the display's inset would leave a
+/// band of empty colour.
+internal object SafeInsets {
+ /// The top and bottom insets of a host at `absY` that is `height` tall,
+ /// packed as `top shl 32 or bottom`.
+ fun of(absY: Int, height: Int): Long {
+ val r = try {
+ com.codename1.ui.Display.getInstance().getDisplaySafeArea(com.codename1.ui.geom.Rectangle())
+ } catch (e: Throwable) {
+ null
+ }
+ if (r == null || height <= 0) {
+ return 0L
+ }
+ val t = Math.max(0, Math.min(height, r.y - absY))
+ val b = Math.max(0, Math.min(height, absY + height - (r.y + r.height)))
+ return (t.toLong() shl 32) or b.toLong()
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Dispatch.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Dispatch.kt
new file mode 100644
index 00000000000..b756463879e
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Dispatch.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.runtime.snapshots.Snapshot
+import com.codename1.ui.CN
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.Delay
+import kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.MainCoroutineDispatcher
+import kotlinx.coroutines.internal.MainDispatcherFactory
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.CoroutineContext
+
+/// `Dispatchers.Main`: Codename One's event dispatch thread.
+@OptIn(InternalCoroutinesApi::class)
+internal open class EdtDispatcher(private val eager: Boolean) : MainCoroutineDispatcher(), Delay {
+ override val immediate: MainCoroutineDispatcher get() = if (eager) this else Immediate
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean = !eager || !isEdt()
+
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ CN.callSerially(block)
+ }
+
+ override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) {
+ val t = CN.setTimeout(Math.max(0L, Math.min(timeMillis, Int.MAX_VALUE.toLong())).toInt()) {
+ continuation.resumeWith(Result.success(Unit))
+ }
+ continuation.invokeOnCancellation { t.cancel() }
+ }
+
+ override fun toString(): String = if (eager) "Dispatchers.Main.immediate" else "Dispatchers.Main"
+
+ companion object {
+ val Main: EdtDispatcher = EdtDispatcher(false)
+ val Immediate: EdtDispatcher = EdtDispatcher(true)
+
+ fun isEdt(): Boolean = try {
+ CN.isEdt()
+ } catch (t: Throwable) {
+ false
+ }
+ }
+}
+
+/// Found by kotlinx.coroutines through `META-INF/services`, which the translator
+/// compiles into a registry; on the JVM the real service loader reads the file.
+@OptIn(InternalCoroutinesApi::class)
+internal class EdtDispatcherFactory : MainDispatcherFactory {
+ override val loadPriority: Int get() = Int.MAX_VALUE
+
+ override fun createDispatcher(allFactories: List): MainCoroutineDispatcher = EdtDispatcher.Main
+
+ override fun hintOnError(): String = "Codename One's main dispatcher"
+}
+
+/// Sends snapshot apply notifications on the EDT, so that a state written from
+/// any thread schedules a recomposition.
+internal object GlobalSnapshotManager {
+ private val started = AtomicBoolean(false)
+ private val pending = AtomicBoolean(false)
+
+ fun ensureStarted() {
+ if (started.compareAndSet(false, true)) {
+ Snapshot.registerGlobalWriteObserver {
+ if (pending.compareAndSet(false, true)) {
+ CN.callSerially {
+ pending.set(false)
+ Snapshot.sendApplyNotifications()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Elements.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Elements.kt
new file mode 100644
index 00000000000..ff7c27b1656
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Elements.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.unit.Dp
+
+// The modifier elements the public extension functions create. Each is a value
+// (data class) where it can be, so that an unchanged chain compares equal and a
+// recomposition that rebuilds it does not relayout.
+
+/// Changes how a node measures; applied in chain order.
+internal sealed interface LayoutElement : Modifier.Element
+
+internal data class PaddingElement(val start: Dp, val top: Dp, val end: Dp, val bottom: Dp) : LayoutElement
+
+/// Padding by the system bars, read when the node is measured rather than when
+/// it is composed: at the first composition the native view may not know its
+/// safe area yet, and a value captured then would stay. The host re-measures
+/// when the safe area changes (`ComposeHost.layoutChildren`).
+internal data class SafeInsetsElement(val top: Boolean, val bottom: Boolean) : LayoutElement
+
+/// A size request. An unspecified bound leaves the incoming one alone; with
+/// `enforceIncoming` the result still respects what the parent allows.
+internal data class SizeElement(
+ val minWidth: Dp = Dp.Unspecified, val maxWidth: Dp = Dp.Unspecified,
+ val minHeight: Dp = Dp.Unspecified, val maxHeight: Dp = Dp.Unspecified,
+ val enforceIncoming: Boolean = true
+) : LayoutElement
+
+internal data class FillElement(val horizontal: Boolean, val vertical: Boolean, val fraction: Float) : LayoutElement
+
+internal data class WrapElement(val horizontal: Boolean, val vertical: Boolean, val align: Alignment,
+ val unbounded: Boolean) : LayoutElement
+
+internal data class OffsetElement(val x: Dp, val y: Dp) : LayoutElement
+
+internal data class AspectRatioElement(val ratio: Float) : LayoutElement
+
+internal class ScrollElement(val state: ScrollStateHandle, val vertical: Boolean) : LayoutElement {
+ override fun equals(other: Any?): Boolean =
+ other is ScrollElement && other.state === state && other.vertical == vertical
+
+ override fun hashCode(): Int = System.identityHashCode(state) * 2 + if (vertical) 1 else 0
+}
+
+/// Read by the parent's measure policy, not by the node.
+internal sealed interface ParentDataElement : Modifier.Element
+
+internal data class WeightElement(val weight: Float, val fill: Boolean) : ParentDataElement
+
+internal data class AlignSelfElement(val horizontal: Alignment.Horizontal?, val vertical: Alignment.Vertical?,
+ val box: Alignment?) : ParentDataElement
+
+internal object MatchParentSizeElement : ParentDataElement
+
+/// Paints at the position it has in the chain.
+internal sealed interface DrawElement : Modifier.Element
+
+internal data class BackgroundElement(val color: Color, val brush: Brush?, val shape: Shape) : DrawElement
+
+internal data class BorderElement(val width: Dp, val color: Color, val shape: Shape) : DrawElement
+
+internal data class ClipElement(val shape: Shape) : DrawElement
+
+internal class DrawBehindElement(val onDraw: DrawScope.() -> Unit) : DrawElement
+
+internal data class AlphaElement(val alpha: Float) : Modifier.Element
+
+internal class ClickableElement(val enabled: Boolean, val onClick: () -> Unit, val onLongClick: (() -> Unit)?,
+ val indication: Boolean) : Modifier.Element
+
+internal class PointerTapElement(val onTap: (Float, Float) -> Unit) : Modifier.Element
+
+internal data class TestTagElement(val tag: String) : Modifier.Element
+
+internal data class SemanticsElement(val contentDescription: String?) : Modifier.Element
+
+/// What a scroll modifier binds to; implemented by
+/// [androidx.compose.foundation.ScrollState].
+internal interface ScrollStateHandle {
+ fun attach(node: UiNode, vertical: Boolean)
+ fun detach(node: UiNode)
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Emit.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Emit.kt
new file mode 100644
index 00000000000..2beca85a9f3
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Emit.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ComposeNode
+import androidx.compose.ui.Modifier
+
+/// Emits a layout node with children.
+@Composable
+internal inline fun LayoutNode(modifier: Modifier, policy: MeasurePolicy, content: @Composable () -> Unit) {
+ ComposeNode(
+ factory = { UiNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(policy) { this.policy = it }
+ },
+ content = content
+ )
+}
+
+/// Emits a node with no children.
+@Composable
+internal fun LeafNode(modifier: Modifier, policy: MeasurePolicy) {
+ ComposeNode(
+ factory = { UiNode() },
+ update = {
+ set(modifier) { this.modifier = it }
+ set(policy) { this.policy = it }
+ }
+ )
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Glyphs.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Glyphs.kt
new file mode 100644
index 00000000000..b94ea9e7d13
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Glyphs.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+internal object Glyphs {
+ fun of(v: ImageVector): Char = v.glyph
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Leaves.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Leaves.kt
new file mode 100644
index 00000000000..8b9b4e3aaee
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Leaves.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import com.codename1.ui.CN
+import com.codename1.ui.Font
+import com.codename1.ui.Graphics
+
+internal object Theme {
+ fun isDark(): Boolean = try {
+ CN.isDarkMode() == true
+ } catch (t: Throwable) {
+ false
+ }
+}
+
+/// Native fonts by weight and pixel size, cached.
+internal object Fonts {
+ private val cache = HashMap()
+
+ fun get(sizePx: Int, weight: FontWeight?): Font {
+ val w = weight?.weight ?: 400
+ val key = (sizePx.toLong() shl 16) or w.toLong()
+ cache[key]?.let { return it }
+ val f = create(sizePx, w)
+ cache[key] = f
+ return f
+ }
+
+ private fun create(sizePx: Int, w: Int): Font {
+ try {
+ if (Font.isNativeFontSchemeSupported()) {
+ val name = when {
+ w <= 200 -> "native:MainThin"
+ w <= 300 -> "native:MainLight"
+ w <= 500 -> "native:MainRegular"
+ w <= 700 -> "native:MainBold"
+ else -> "native:MainBlack"
+ }
+ val base = Font.createTrueTypeFont(name, name)
+ if (base != null) {
+ return base.derive(sizePx.toFloat(), if (w >= 600) Font.STYLE_BOLD else Font.STYLE_PLAIN)
+ }
+ }
+ } catch (t: Throwable) {
+ // Fall through to a system font.
+ }
+ val size = when {
+ sizePx < Density.px(androidx.compose.ui.unit.Dp(13f)) -> Font.SIZE_SMALL
+ sizePx > Density.px(androidx.compose.ui.unit.Dp(18f)) -> Font.SIZE_LARGE
+ else -> Font.SIZE_MEDIUM
+ }
+ return Font.createSystemFont(Font.FACE_SYSTEM, if (w >= 600) Font.STYLE_BOLD else Font.STYLE_PLAIN, size)
+ }
+}
+
+/// Text: wrapped at word boundaries to the width it is given, cut at
+/// `maxLines` with an ellipsis when asked.
+internal class TextNode : UiNode() {
+ var text: String = ""
+ set(v) { if (field != v) { field = v; invalidateMeasure(); repaint() } }
+ var style: TextStyle = TextStyle.Default
+ set(v) { if (field != v) { field = v; invalidateMeasure(); repaint() } }
+ var maxLines: Int = Int.MAX_VALUE
+ set(v) { if (field != v) { field = v; invalidateMeasure() } }
+ var softWrap: Boolean = true
+ set(v) { if (field != v) { field = v; invalidateMeasure() } }
+ var overflow: TextOverflow = TextOverflow.Clip
+ set(v) { if (field != v) { field = v; invalidateMeasure() } }
+
+ private var lines: List = emptyList()
+ private var font: Font? = null
+
+ init {
+ policy = LeafPolicy
+ }
+
+ private fun font(): Font {
+ val sp = if (style.fontSize.isSpecified) style.fontSize.value else 14f
+ val f = Fonts.get(Math.max(1, Math.round(sp * Density.density)), style.fontWeight)
+ font = f
+ return f
+ }
+
+ private fun lineHeight(f: Font): Int =
+ if (style.lineHeight.isSpecified && style.lineHeight.isSp) Math.max(f.height, Math.round(style.lineHeight.value * Density.density))
+ else f.height
+
+ override fun measureContent(c: Constraints): Long {
+ val f = font()
+ val maxW = c.maxWidth
+ val out = ArrayList()
+ for (para in text.split('\n')) {
+ if (!softWrap || maxW == Constraints.INF || f.stringWidth(para) <= maxW) {
+ out.add(para)
+ } else {
+ wrap(para, f, maxW, out)
+ }
+ if (out.size > maxLines) break
+ }
+ if (out.size > maxLines) {
+ while (out.size > maxLines) out.removeAt(out.size - 1)
+ if (overflow == TextOverflow.Ellipsis && out.isNotEmpty()) {
+ out[out.size - 1] = ellipsize(out[out.size - 1] + "\u2026", f, maxW)
+ }
+ } else if (overflow == TextOverflow.Ellipsis && maxW != Constraints.INF && out.size == 1 && f.stringWidth(out[0]) > maxW) {
+ out[0] = ellipsize(out[0], f, maxW)
+ }
+ lines = out
+ var w = 0
+ for (l in out) w = Math.max(w, f.stringWidth(l))
+ return size(Math.min(w, maxW), lineHeight(f) * out.size)
+ }
+
+ private fun wrap(para: String, f: Font, maxW: Int, out: MutableList) {
+ var line = StringBuilder()
+ for (word in para.split(' ')) {
+ val candidate = if (line.isEmpty()) word else "$line $word"
+ if (f.stringWidth(candidate) <= maxW || line.isEmpty()) {
+ line = StringBuilder(candidate)
+ } else {
+ out.add(line.toString())
+ line = StringBuilder(word)
+ }
+ }
+ out.add(line.toString())
+ }
+
+ private fun ellipsize(s: String, f: Font, maxW: Int): String {
+ val e = "\u2026"
+ var base = if (s.endsWith(e)) s.substring(0, s.length - 1) else s
+ if (maxW == Constraints.INF || f.stringWidth(base + e) <= maxW && s.endsWith(e)) return base + e
+ while (base.isNotEmpty() && f.stringWidth(base + e) > maxW) base = base.substring(0, base.length - 1)
+ return base.trimEnd() + e
+ }
+
+ override fun paintContent(g: Graphics) {
+ val f = font ?: font()
+ val color = if (style.color == Color.Unspecified) Color.Black else style.color
+ if (color.alpha <= 0f) return
+ val old = g.alpha
+ g.font = f
+ g.color = color.argb and 0xffffff
+ if (color.alpha < 1f) g.concatenateAlpha(Math.round(color.alpha * 255))
+ g.isAntiAliased = true
+ g.isAntiAliasedText = true
+ val lh = lineHeight(f)
+ var y = absY() + contentY + (lh - f.height) / 2
+ val x0 = absX() + contentX
+ for (l in lines) {
+ val lw = f.stringWidth(l)
+ val dx = when (style.textAlign) {
+ TextAlign.Center -> (contentW - lw) / 2
+ TextAlign.End, TextAlign.Right -> contentW - lw
+ else -> 0
+ }
+ g.drawString(l, x0 + dx, y)
+ if (style.textDecoration == TextDecoration.Underline) {
+ g.drawLine(x0 + dx, y + f.ascent + 1, x0 + dx + lw, y + f.ascent + 1)
+ } else if (style.textDecoration == TextDecoration.LineThrough) {
+ g.drawLine(x0 + dx, y + f.height / 2, x0 + dx + lw, y + f.height / 2)
+ }
+ y += lh
+ }
+ g.alpha = old
+ }
+
+ fun cn1Lines(): List = lines
+}
+
+internal fun UiNode.absX(): Int = x
+internal fun UiNode.absY(): Int = y
+
+/// A bitmap, scaled into its content box.
+internal class ImageNode : UiNode() {
+ var bitmap: ImageBitmap? = null
+ set(v) { if (field !== v) { field = v; invalidateMeasure(); repaint() } }
+ var contentScale: ContentScale = ContentScale.Fit
+ var tint: Color = Color.Unspecified
+
+ init {
+ policy = LeafPolicy
+ }
+
+ override fun measureContent(c: Constraints): Long {
+ val b = bitmap ?: return size(c.minWidth, c.minHeight)
+ val iw = b.width
+ val ih = b.height
+ if (c.hasFixedWidth && c.hasFixedHeight) return size(c.maxWidth, c.maxHeight)
+ var w = c.constrainWidth(iw)
+ var h = c.constrainHeight(ih)
+ if (iw > 0 && ih > 0) {
+ val sw = w.toFloat() / iw
+ val sh = h.toFloat() / ih
+ val s = Math.min(sw, sh)
+ if (!c.hasFixedWidth) w = c.constrainWidth(Math.round(iw * s))
+ if (!c.hasFixedHeight) h = c.constrainHeight(Math.round(ih * s))
+ }
+ return size(w, h)
+ }
+
+ override fun paintContent(g: Graphics) {
+ val b = bitmap ?: return
+ val bw = contentW
+ val bh = contentH
+ val iw = b.width.toFloat()
+ val ih = b.height.toFloat()
+ if (iw <= 0 || ih <= 0) return
+ val s = when (contentScale) {
+ ContentScale.Crop -> Math.max(bw / iw, bh / ih)
+ ContentScale.FillBounds -> -1f
+ ContentScale.None -> 1f
+ ContentScale.FillWidth -> bw / iw
+ else -> Math.min(bw / iw, bh / ih)
+ }
+ val x0 = x + contentX
+ val y0 = y + contentY
+ if (s < 0) {
+ g.drawImage(b.image, x0, y0, bw, bh)
+ return
+ }
+ val dw = Math.round(iw * s)
+ val dh = Math.round(ih * s)
+ g.pushClip()
+ g.clipRect(x0, y0, bw, bh)
+ g.drawImage(b.image, x0 + (bw - dw) / 2, y0 + (bh - dh) / 2, dw, dh)
+ g.popClip()
+ }
+}
+
+/// A material icon glyph from the material design font.
+internal class IconNode : UiNode() {
+ var glyph: Char = ' '
+ set(v) { if (field != v) { field = v; repaint() } }
+ var tint: Color = Color.Black
+ set(v) { if (field != v) { field = v; repaint() } }
+
+ init {
+ policy = LeafPolicy
+ }
+
+ override fun measureContent(c: Constraints): Long {
+ val d = Density.px(androidx.compose.ui.unit.Dp(24f))
+ return size(c.constrainWidth(d), c.constrainHeight(d))
+ }
+
+ override fun paintContent(g: Graphics) {
+ val s = Math.min(contentW, contentH)
+ if (s <= 0 || tint.alpha <= 0f) return
+ val f = com.codename1.ui.FontImage.getMaterialDesignFont().derive(s.toFloat(), Font.STYLE_PLAIN)
+ val old = g.alpha
+ g.font = f
+ g.color = tint.argb and 0xffffff
+ if (tint.alpha < 1f) g.concatenateAlpha(Math.round(tint.alpha * 255))
+ g.isAntiAliasedText = true
+ val str = glyph.toString()
+ val gw = f.stringWidth(str)
+ g.drawString(str, x + contentX + (contentW - gw) / 2, y + contentY + (contentH - f.height) / 2)
+ g.alpha = old
+ }
+}
+
+/// Hosts a Codename One component as a node's only child, sized to the node's
+/// content box: how text input gets the platform's own editing.
+internal class NativeNode(val component: T) : UiNode() {
+ init {
+ policy = BoxPolicy(androidx.compose.ui.Alignment.TopStart, true)
+ addComponent(component)
+ }
+}
+
+/// A single line or multi line text input over Codename One's `TextArea`.
+internal class TextInputNode : UiNode() {
+ val field: com.codename1.ui.TextArea
+ private var syncing = false
+ var onValueChange: (String) -> Unit = {}
+ var textColor: Color = Color.Black
+ set(v) { field = v; applyColors() }
+ var font: Font? = null
+ set(v) { field = v; if (v != null) this.field.allStyles.font = v; invalidateMeasure() }
+
+ init {
+ policy = BoxPolicy(androidx.compose.ui.Alignment.TopStart, true)
+ val input = com.codename1.ui.TextField()
+ field = input
+ cn1StripStyle(input, allStates = true)
+ input.addDataChangedListener { _, _ ->
+ if (!syncing) onValueChange(input.text)
+ }
+ addComponent(field)
+ }
+
+ fun setValue(v: String) {
+ if (field.text != v) {
+ syncing = true
+ try {
+ field.text = v
+ } finally {
+ syncing = false
+ }
+ }
+ }
+
+ var singleLine: Boolean = true
+ set(v) { field = v; this.field.isSingleLineTextArea = v; invalidateMeasure() }
+
+ var hint: String = ""
+ set(v) { field = v; this.field.hint = v }
+
+ var enabledInput: Boolean = true
+ set(v) { field = v; this.field.isEnabled = v }
+
+ private fun applyColors() {
+ field.allStyles.fgColor = textColor.argb and 0xffffff
+ field.hintLabel?.allStyles?.fgColor = textColor.argb and 0xffffff
+ field.hintLabel?.allStyles?.opacity = 140
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Painting.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Painting.kt
new file mode 100644
index 00000000000..e7b61c7dc09
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Painting.kt
@@ -0,0 +1,383 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.LinearGradient
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.DrawStyle
+import androidx.compose.ui.graphics.drawscope.Stroke
+import com.codename1.ui.Graphics
+import com.codename1.ui.geom.GeneralPath
+
+/// Fills and strokes shapes on Codename One graphics.
+internal object Painting {
+ fun outline(shape: Shape, w: Int, h: Int): Outline =
+ shape.createOutline(Size(w.toFloat(), h.toFloat()), LTR, Density)
+
+ fun fill(g: Graphics, shape: Shape, color: Color, brush: Brush?, x: Int, y: Int, w: Int, h: Int) {
+ if (w <= 0 || h <= 0) return
+ val o = outline(shape, w, h)
+ val b = brush
+ if (b is LinearGradient && b.colors.size >= 2 && o is Outline.Rectangle) {
+ val a = b.colors[0]
+ val e = b.colors[b.colors.size - 1]
+ val old = g.alpha
+ g.concatenateAlpha(Math.round(a.alpha * 255))
+ g.fillLinearGradient(a.argb and 0xffffff, e.argb and 0xffffff, x, y, w, h, !b.vertical)
+ g.alpha = old
+ return
+ }
+ val c = when (b) {
+ is SolidColor -> b.value
+ is LinearGradient -> if (b.colors.isEmpty()) color else b.colors[0]
+ else -> color
+ }
+ if (c.alpha <= 0f) return
+ val old = g.alpha
+ g.color = c.argb and 0xffffff
+ if (c.alpha < 1f) g.concatenateAlpha(Math.round(c.alpha * 255))
+ when (o) {
+ is Outline.Rounded -> {
+ if (uniform(o)) {
+ val r = Math.round(o.topLeft * 2)
+ g.fillRoundRect(x, y, w, h, r, r)
+ } else if (g.isShapeSupported) {
+ g.fillShape(roundedPath(o, x.toFloat(), y.toFloat()))
+ } else {
+ val r = o.cornerArc()
+ g.fillRoundRect(x, y, w, h, r, r)
+ }
+ }
+ else -> g.fillRect(x, y, w, h)
+ }
+ g.alpha = old
+ }
+
+ fun stroke(g: Graphics, shape: Shape, color: Color, width: Int, x: Int, y: Int, w: Int, h: Int) {
+ if (w <= 0 || h <= 0 || width <= 0 || color.alpha <= 0f) return
+ val old = g.alpha
+ g.color = color.argb and 0xffffff
+ if (color.alpha < 1f) g.concatenateAlpha(Math.round(color.alpha * 255))
+ val o = outline(shape, w, h)
+ val half = width / 2f
+ if (g.isShapeSupported) {
+ val path = when (o) {
+ is Outline.Rounded -> roundedPath(Outline.Rounded(w - width.toFloat(), h - width.toFloat(),
+ Math.max(0f, o.topLeft - half), Math.max(0f, o.topRight - half),
+ Math.max(0f, o.bottomRight - half), Math.max(0f, o.bottomLeft - half)), x + half, y + half)
+ else -> rectPath(x + half, y + half, w - width.toFloat(), h - width.toFloat())
+ }
+ g.drawShape(path, com.codename1.ui.Stroke(width.toFloat(), com.codename1.ui.Stroke.CAP_BUTT,
+ com.codename1.ui.Stroke.JOIN_MITER, 4f))
+ } else {
+ for (i in 0 until width) {
+ when (o) {
+ is Outline.Rounded -> {
+ val r = o.cornerArc()
+ g.drawRoundRect(x + i, y + i, w - 1 - 2 * i, h - 1 - 2 * i, r, r)
+ }
+ else -> g.drawRect(x + i, y + i, w - 1 - 2 * i, h - 1 - 2 * i)
+ }
+ }
+ }
+ g.alpha = old
+ }
+
+ private fun uniform(o: Outline.Rounded): Boolean =
+ o.topLeft == o.topRight && o.topRight == o.bottomRight && o.bottomRight == o.bottomLeft
+
+ fun rectPath(x: Float, y: Float, w: Float, h: Float): GeneralPath {
+ val p = GeneralPath()
+ p.moveTo(x, y)
+ p.lineTo(x + w, y)
+ p.lineTo(x + w, y + h)
+ p.lineTo(x, y + h)
+ p.closePath()
+ return p
+ }
+
+ fun roundedPath(o: Outline.Rounded, x: Float, y: Float): GeneralPath {
+ val w = o.width
+ val h = o.height
+ val max = Math.min(w, h) / 2f
+ val tl = Math.min(o.topLeft, max)
+ val tr = Math.min(o.topRight, max)
+ val br = Math.min(o.bottomRight, max)
+ val bl = Math.min(o.bottomLeft, max)
+ val p = GeneralPath()
+ p.moveTo(x + tl, y)
+ p.lineTo(x + w - tr, y)
+ if (tr > 0) p.arc(x + w - 2 * tr, y, 2 * tr, 2 * tr, HALF_PI, -HALF_PI, true)
+ p.lineTo(x + w, y + h - br)
+ if (br > 0) p.arc(x + w - 2 * br, y + h - 2 * br, 2 * br, 2 * br, 0f, -HALF_PI, true)
+ p.lineTo(x + bl, y + h)
+ if (bl > 0) p.arc(x, y + h - 2 * bl, 2 * bl, 2 * bl, -HALF_PI, -HALF_PI, true)
+ p.lineTo(x, y + tl)
+ if (tl > 0) p.arc(x, y, 2 * tl, 2 * tl, PI, -HALF_PI, true)
+ p.closePath()
+ return p
+ }
+
+ /// Clips `g` to the rounded rectangle `o` at (`x`, `y`), intersected with
+ /// the clip already in force, and answers false when nothing of it is
+ /// visible. `setClip(Shape)` replaces the clip, so the intersection is
+ /// ours to make. A rounded rectangle is convex: flattened to a polygon it
+ /// is cut by the clip rectangle edge by edge (Sutherland-Hodgman), with no
+ /// curve math and no allocation once the buffers have grown. Cutting the
+ /// curved path with `GeneralPath.intersect` was most of what a scrolling
+ /// list allocated; the iOS port flattens a curved clip to a polygon anyway.
+ fun clipRounded(g: Graphics, o: Outline.Rounded, x: Int, y: Int): Boolean {
+ var n = roundedPolygon(o, x.toFloat(), y.toFloat())
+ val left = g.clipX.toFloat()
+ val top = g.clipY.toFloat()
+ val right = left + g.clipWidth
+ val bottom = top + g.clipHeight
+ n = cut(n, 0, left, true)
+ n = cut(n, 0, right, false)
+ n = cut(n, 1, top, true)
+ n = cut(n, 1, bottom, false)
+ if (n < 3) {
+ return false
+ }
+ val p = clipPath
+ p.reset()
+ p.moveTo(poly[0], poly[1])
+ for (i in 1 until n) {
+ p.lineTo(poly[i * 2], poly[i * 2 + 1])
+ }
+ p.closePath()
+ g.setClip(p)
+ return true
+ }
+
+ private const val ARC_STEPS = 6
+ private var poly = FloatArray(64)
+ private var scratch = FloatArray(64)
+ private val clipPath = GeneralPath()
+
+ /// Writes `o` as a clockwise polygon into `poly`; answers its point count.
+ private fun roundedPolygon(o: Outline.Rounded, x: Float, y: Float): Int {
+ val max = Math.min(o.width, o.height) / 2f
+ val w = o.width
+ val h = o.height
+ ensure(4 * (ARC_STEPS + 1) + 8)
+ var n = 0
+ n = corner(n, x, y, Math.min(o.topLeft, max), 1f, 1f, PI)
+ n = corner(n, x + w, y, Math.min(o.topRight, max), -1f, 1f, PI + HALF_PI)
+ n = corner(n, x + w, y + h, Math.min(o.bottomRight, max), -1f, -1f, 0f)
+ n = corner(n, x, y + h, Math.min(o.bottomLeft, max), 1f, -1f, HALF_PI)
+ return n
+ }
+
+ private fun corner(n: Int, px: Float, py: Float, r: Float, sx: Float, sy: Float, start: Float): Int {
+ var k = n
+ if (r <= 0f) {
+ poly[k * 2] = px
+ poly[k * 2 + 1] = py
+ return k + 1
+ }
+ val cx = px + sx * r
+ val cy = py + sy * r
+ for (i in 0..ARC_STEPS) {
+ val a = start + HALF_PI * i / ARC_STEPS
+ poly[k * 2] = cx + r * Math.cos(a.toDouble()).toFloat()
+ poly[k * 2 + 1] = cy + r * Math.sin(a.toDouble()).toFloat()
+ k++
+ }
+ return k
+ }
+
+ /// Keeps the part of the polygon in `poly` on the inner side of the line
+ /// `axis` (0 is x, 1 is y) = `v`; `low` keeps values at or above it.
+ private fun cut(n: Int, axis: Int, v: Float, low: Boolean): Int {
+ if (n == 0) {
+ return 0
+ }
+ ensure(n + 4)
+ val src = poly
+ val dst = scratch
+ var m = 0
+ for (i in 0 until n) {
+ val j = (i + 1) % n
+ val a = src[i * 2 + axis]
+ val b = src[j * 2 + axis]
+ val aIn = if (low) a >= v else a <= v
+ val bIn = if (low) b >= v else b <= v
+ if (aIn) {
+ dst[m * 2] = src[i * 2]
+ dst[m * 2 + 1] = src[i * 2 + 1]
+ m++
+ }
+ if (aIn != bIn) {
+ val t = (v - a) / (b - a)
+ dst[m * 2] = src[i * 2] + t * (src[j * 2] - src[i * 2])
+ dst[m * 2 + 1] = src[i * 2 + 1] + t * (src[j * 2 + 1] - src[i * 2 + 1])
+ m++
+ }
+ }
+ poly = dst
+ scratch = src
+ return m
+ }
+
+ private fun ensure(points: Int) {
+ if (poly.size < points * 2) {
+ poly = poly.copyOf(points * 4)
+ }
+ if (scratch.size < points * 2 || scratch.size < poly.size) {
+ scratch = FloatArray(poly.size)
+ }
+ }
+
+ private const val PI = Math.PI.toFloat()
+ private const val HALF_PI = (Math.PI / 2).toFloat()
+}
+
+/// [DrawScope] over Codename One graphics, already translated to the box's
+/// origin.
+internal class Cn1DrawScope(private val g: Graphics, override val size: Size) : DrawScope {
+ override val density: Float get() = Density.density
+ override val fontScale: Float get() = 1f
+
+ private inline fun paint(color: Color, alpha: Float, block: () -> Unit) {
+ val a = color.alpha * alpha
+ if (a <= 0f) return
+ val old = g.alpha
+ g.color = color.argb and 0xffffff
+ if (a < 1f) g.concatenateAlpha(Math.round(a * 255))
+ g.isAntiAliased = true
+ try {
+ block()
+ } finally {
+ g.alpha = old
+ }
+ }
+
+ private fun stroke(width: Float, cap: StrokeCap): com.codename1.ui.Stroke =
+ com.codename1.ui.Stroke(Math.max(1f, width), when (cap) {
+ StrokeCap.Round -> com.codename1.ui.Stroke.CAP_ROUND
+ StrokeCap.Square -> com.codename1.ui.Stroke.CAP_SQUARE
+ else -> com.codename1.ui.Stroke.CAP_BUTT
+ }, com.codename1.ui.Stroke.JOIN_ROUND, 4f)
+
+ private fun r(v: Float): Int = Math.round(v)
+
+ override fun drawRect(color: Color, topLeft: Offset, size: Size, alpha: Float, style: DrawStyle) = paint(color, alpha) {
+ if (style is Stroke) {
+ g.drawShape(Painting.rectPath(topLeft.x, topLeft.y, size.width, size.height), stroke(style.width, style.cap))
+ } else {
+ g.fillRect(r(topLeft.x), r(topLeft.y), r(size.width), r(size.height))
+ }
+ }
+
+ override fun drawRoundRect(color: Color, topLeft: Offset, size: Size, cornerRadius: CornerRadius, alpha: Float,
+ style: DrawStyle) = paint(color, alpha) {
+ val rr = cornerRadius.x
+ if (style is Stroke) {
+ g.drawShape(Painting.roundedPath(Outline.Rounded(size.width, size.height, rr, rr, rr, rr), topLeft.x, topLeft.y),
+ stroke(style.width, style.cap))
+ } else {
+ g.fillRoundRect(r(topLeft.x), r(topLeft.y), r(size.width), r(size.height), r(rr * 2), r(cornerRadius.y * 2))
+ }
+ }
+
+ override fun drawCircle(color: Color, radius: Float, center: Offset, alpha: Float, style: DrawStyle) = paint(color, alpha) {
+ if (style is Stroke) {
+ val p = GeneralPath()
+ p.arc(center.x - radius, center.y - radius, radius * 2, radius * 2, 0f, (Math.PI * 2).toFloat())
+ g.drawShape(p, stroke(style.width, style.cap))
+ } else {
+ g.fillArc(r(center.x - radius), r(center.y - radius), r(radius * 2), r(radius * 2), 0, 360)
+ }
+ }
+
+ override fun drawArc(color: Color, startAngle: Float, sweepAngle: Float, useCenter: Boolean, topLeft: Offset,
+ size: Size, alpha: Float, style: DrawStyle) = paint(color, alpha) {
+ // Compose measures angles clockwise; Codename One counter-clockwise.
+ if (style is Stroke) {
+ val p = GeneralPath()
+ val rad = (Math.PI / 180).toFloat()
+ p.arc(topLeft.x, topLeft.y, size.width, size.height, -startAngle * rad, -sweepAngle * rad)
+ g.drawShape(p, stroke(style.width, style.cap))
+ } else {
+ g.fillArc(r(topLeft.x), r(topLeft.y), r(size.width), r(size.height), r(-startAngle), r(-sweepAngle))
+ }
+ }
+
+ override fun drawLine(color: Color, start: Offset, end: Offset, strokeWidth: Float, cap: StrokeCap, alpha: Float) =
+ paint(color, alpha) {
+ if (strokeWidth <= 1f) {
+ g.drawLine(r(start.x), r(start.y), r(end.x), r(end.y))
+ } else {
+ val p = GeneralPath()
+ p.moveTo(start.x, start.y)
+ p.lineTo(end.x, end.y)
+ g.drawShape(p, stroke(strokeWidth, cap))
+ }
+ }
+
+ override fun drawImage(image: ImageBitmap, topLeft: Offset, alpha: Float) = paint(Color.Black, alpha) {
+ g.drawImage(image.image, r(topLeft.x), r(topLeft.y))
+ }
+
+ override fun drawPolyline(color: Color, points: List, strokeWidth: Float, alpha: Float) = paint(color, alpha) {
+ if (points.size >= 2) {
+ val p = GeneralPath()
+ p.moveTo(points[0].x, points[0].y)
+ for (i in 1 until points.size) p.lineTo(points[i].x, points[i].y)
+ g.drawShape(p, stroke(strokeWidth, StrokeCap.Round))
+ }
+ }
+
+ override fun drawPolygon(color: Color, points: List, alpha: Float) = paint(color, alpha) {
+ if (points.size >= 3) {
+ val p = GeneralPath()
+ p.moveTo(points[0].x, points[0].y)
+ for (i in 1 until points.size) p.lineTo(points[i].x, points[i].y)
+ p.closePath()
+ g.fillShape(p)
+ }
+ }
+
+ override fun translate(left: Float, top: Float, block: DrawScope.() -> Unit) {
+ val dx = r(left)
+ val dy = r(top)
+ g.translate(dx, dy)
+ try {
+ block()
+ } finally {
+ g.translate(-dx, -dy)
+ }
+ }
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Policies.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Policies.kt
new file mode 100644
index 00000000000..de455f8258a
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/Policies.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.unit.IntSize
+import com.codename1.ui.Component
+
+private fun Component.weightOf(): WeightElement? = (this as? UiNode)?.weight
+private fun Component.alignOf(): AlignSelfElement? = (this as? UiNode)?.alignSelf
+
+/// Stacks children, each aligned in the box.
+internal class BoxPolicy(val alignment: Alignment, val propagateMinConstraints: Boolean) : MeasurePolicy {
+ override fun measure(node: UiNode, children: List, constraints: Constraints): Long {
+ val childC = if (propagateMinConstraints) constraints else constraints.loose()
+ var w = constraints.minWidth
+ var h = constraints.minHeight
+ val sizes = LongArray(children.size)
+ // Children that match the parent's size are measured once the others have
+ // decided it.
+ for (i in children.indices) {
+ val c = children[i]
+ if ((c as? UiNode)?.matchParentSize == true) continue
+ val s = measureChild(c, childC)
+ sizes[i] = s
+ w = Math.max(w, s.w())
+ h = Math.max(h, s.h())
+ }
+ w = constraints.constrainWidth(w)
+ h = constraints.constrainHeight(h)
+ for (i in children.indices) {
+ val c = children[i]
+ if ((c as? UiNode)?.matchParentSize == true) {
+ sizes[i] = measureChild(c, Constraints.fixed(w, h))
+ }
+ val s = sizes[i]
+ val a = c.alignOf()?.box ?: alignment
+ val o = a.align(IntSize(s.w(), s.h()), IntSize(w, h), LTR)
+ node.place(i, o.x, o.y, s.w(), s.h())
+ }
+ return size(w, h)
+ }
+
+ override fun equals(other: Any?): Boolean =
+ other is BoxPolicy && other.alignment == alignment && other.propagateMinConstraints == propagateMinConstraints
+
+ override fun hashCode(): Int = alignment.hashCode() * 2 + if (propagateMinConstraints) 1 else 0
+
+ companion object {
+ val TopStart: BoxPolicy = BoxPolicy(Alignment.TopStart, false)
+ }
+}
+
+/// Row and Column: children along the main axis, with weights and an
+/// arrangement, aligned on the cross axis.
+internal class LinearPolicy(
+ val vertical: Boolean,
+ val horizontalArrangement: Arrangement.Horizontal?,
+ val verticalArrangement: Arrangement.Vertical?,
+ val horizontalAlignment: Alignment.Horizontal?,
+ val verticalAlignment: Alignment.Vertical?
+) : MeasurePolicy {
+ override fun measure(node: UiNode, children: List, constraints: Constraints): Long {
+ val d = Density
+ val spacing = if (vertical) d.px(verticalArrangement!!.spacing) else d.px(horizontalArrangement!!.spacing)
+ val mainMax = if (vertical) constraints.maxHeight else constraints.maxWidth
+ val mainMin = if (vertical) constraints.minHeight else constraints.minWidth
+ val crossMax = if (vertical) constraints.maxWidth else constraints.maxHeight
+ val crossMin = if (vertical) constraints.minWidth else constraints.minHeight
+ val n = children.size
+ val mains = IntArray(n)
+ val crosses = IntArray(n)
+ var used = if (n > 1) spacing * (n - 1) else 0
+ var totalWeight = 0f
+ var cross = 0
+ for (i in 0 until n) {
+ val c = children[i]
+ val wt = c.weightOf()
+ if (wt != null && wt.weight > 0f) {
+ totalWeight += wt.weight
+ continue
+ }
+ val remaining = if (mainMax == Constraints.INF) Constraints.INF else Math.max(0, mainMax - used)
+ val cc = if (vertical) Constraints(0, crossMax, 0, remaining) else Constraints(0, remaining, 0, crossMax)
+ val s = measureChild(c, cc)
+ mains[i] = if (vertical) s.h() else s.w()
+ crosses[i] = if (vertical) s.w() else s.h()
+ used += mains[i]
+ cross = Math.max(cross, crosses[i])
+ }
+ if (totalWeight > 0f) {
+ val space = if (mainMax == Constraints.INF) 0 else Math.max(0, mainMax - used)
+ var left = space
+ var lastWeighted = -1
+ for (i in 0 until n) {
+ val wt = children[i].weightOf()
+ if (wt != null && wt.weight > 0f) lastWeighted = i
+ }
+ for (i in 0 until n) {
+ val c = children[i]
+ val wt = c.weightOf() ?: continue
+ if (wt.weight <= 0f) continue
+ val share = if (i == lastWeighted) left else Math.round(space * wt.weight / totalWeight)
+ left -= share
+ val min = if (wt.fill) share else 0
+ val cc = if (vertical) Constraints(0, crossMax, min, share) else Constraints(min, share, 0, crossMax)
+ val s = measureChild(c, cc)
+ mains[i] = if (vertical) s.h() else s.w()
+ crosses[i] = if (vertical) s.w() else s.h()
+ used += mains[i]
+ cross = Math.max(cross, crosses[i])
+ }
+ }
+ val mainSize = Math.max(mainMin, if (totalWeight > 0f && mainMax != Constraints.INF) mainMax else
+ Math.min(used, mainMax))
+ val crossSize = Math.max(crossMin, Math.min(cross, crossMax))
+ // Arrange along the main axis.
+ val positions = IntArray(n)
+ if (vertical) {
+ verticalArrangement!!.arrange(mainSize, mains, positions)
+ } else {
+ horizontalArrangement!!.arrange(mainSize, mains, LTR, positions)
+ }
+ for (i in 0 until n) {
+ val c = children[i]
+ val a = c.alignOf()
+ val crossPos = if (vertical) {
+ (a?.horizontal ?: horizontalAlignment!!).align(crosses[i], crossSize, LTR)
+ } else {
+ (a?.vertical ?: verticalAlignment!!).align(crosses[i], crossSize)
+ }
+ if (vertical) {
+ node.place(i, crossPos, positions[i], crosses[i], mains[i])
+ } else {
+ node.place(i, positions[i], crossPos, mains[i], crosses[i])
+ }
+ }
+ return if (vertical) size(crossSize, mainSize) else size(mainSize, crossSize)
+ }
+
+ override fun equals(other: Any?): Boolean = other is LinearPolicy && other.vertical == vertical &&
+ other.horizontalArrangement == horizontalArrangement && other.verticalArrangement == verticalArrangement &&
+ other.horizontalAlignment == horizontalAlignment && other.verticalAlignment == verticalAlignment
+
+ override fun hashCode(): Int = (if (vertical) 1 else 0) + 31 * (horizontalArrangement.hashCode() +
+ 31 * (verticalArrangement.hashCode() + 31 * (horizontalAlignment.hashCode() + 31 * verticalAlignment.hashCode())))
+}
+
+/// A node with no children whose content measures itself (text, an icon, a
+/// canvas).
+internal object LeafPolicy : MeasurePolicy {
+ override fun measure(node: UiNode, children: List, constraints: Constraints): Long =
+ size(constraints.minWidth, constraints.minHeight)
+}
diff --git a/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/UiNode.kt b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/UiNode.kt
new file mode 100644
index 00000000000..9a1250696c3
--- /dev/null
+++ b/maven/compose-compat/src/main/kotlin/com/codename1/compose/internal/UiNode.kt
@@ -0,0 +1,736 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose.internal
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Outline
+import androidx.compose.ui.graphics.Shape
+import com.codename1.ui.Component
+import com.codename1.ui.Container
+import com.codename1.ui.Graphics
+import com.codename1.ui.geom.Dimension
+import com.codename1.ui.layouts.Layout
+import com.codename1.ui.plaf.Border
+
+/// Lays out the children of a node. `measure` returns the content size and
+/// records, through [UiNode.place], where each child goes relative to the content
+/// box's top left corner.
+internal interface MeasurePolicy {
+ fun measure(node: UiNode, children: List, constraints: Constraints): Long
+}
+
+internal fun size(w: Int, h: Int): Long = (w.toLong() shl 32) or (h.toLong() and 0xffffffffL)
+internal fun Long.w(): Int = (this ushr 32).toInt()
+internal fun Long.h(): Int = this.toInt()
+
+/// A Compose layout node, which is also a Codename One container: the node's
+/// children are Codename One components, and so is everything a composable emits.
+///
+/// Compose's layout model is reproduced here rather than mapped onto Codename One
+/// layouts. A node is measured with [Constraints] and answers a size; its modifier
+/// chain is walked outermost first, each layout element transforming the
+/// constraints going in and the size coming out (padding shrinks one and grows the
+/// other); the innermost box is where [policy] measures the children. Drawing
+/// elements paint at the box their position in the chain gives them.
+///
+/// Measuring happens from the root of a composition ([ComposeHost]) in one pass,
+/// which also fixes every child's position. Codename One's own layout pass then
+/// only copies those positions out ([NodeLayout]); a node it lays out on its own
+/// re-measures at its current size.
+internal open class UiNode : Container(NodeLayout) {
+ var policy: MeasurePolicy = BoxPolicy.TopStart
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateMeasure()
+ }
+ }
+
+ var modifier: Modifier = Modifier
+ set(value) {
+ if (field != value) {
+ val old = field
+ field = value
+ resolveModifier(old)
+ invalidateMeasure()
+ repaint()
+ }
+ }
+
+ // The resolved chain.
+ internal var chain: Array = EMPTY
+ internal var weight: WeightElement? = null
+ internal var alignSelf: AlignSelfElement? = null
+ internal var matchParentSize: Boolean = false
+ internal var alpha: Float = 1f
+ internal var clickable: ClickableElement? = null
+ internal var tap: PointerTapElement? = null
+ private var scroll: ScrollElement? = null
+
+ // Measure results: the size at every chain position, and the box each one
+ // occupies once placed (x, y, w, h per element, then the content box).
+ private var sizes = LongArray(1)
+ private var boxes = IntArray(4)
+ internal var measured = false
+
+ /// Whether a [SafeInsetsElement] is in the chain, so the host re-measures
+ /// this node (and only it, with its ancestors) when the safe area changes.
+ internal var usesSafeInsets = false
+
+ /// The top inset this node was measured with, so placing it cannot read a
+ /// newer one.
+ private var placedInsetTop = 0
+ private var lastConstraints: Constraints? = null
+ private var placements = IntArray(0)
+ internal var contentX = 0
+ internal var contentY = 0
+ internal var contentW = 0
+ internal var contentH = 0
+
+ // Offset modifiers move the whole node when its parent places it.
+ internal var offsetX = 0
+ internal var offsetY = 0
+
+ init {
+ cn1StripStyle(this)
+ }
+
+ // --- modifier resolution ------------------------------------------------
+
+ private fun resolveModifier(old: Modifier) {
+ val list = ArrayList()
+ modifier.foldIn(list) { acc, e -> acc.add(e); acc }
+ var weight: WeightElement? = null
+ var align: AlignSelfElement? = null
+ var match = false
+ var alpha = 1f
+ var click: ClickableElement? = null
+ var tap: PointerTapElement? = null
+ var scroll: ScrollElement? = null
+ var tag: String? = null
+ var description: String? = null
+ val chain = ArrayList(list.size)
+ for (e in list) {
+ when (e) {
+ is WeightElement -> weight = e
+ is AlignSelfElement -> align = e
+ is MatchParentSizeElement -> match = true
+ is AlphaElement -> alpha *= e.alpha
+ is ClickableElement -> click = e
+ is PointerTapElement -> tap = e
+ is TestTagElement -> tag = e.tag
+ is SemanticsElement -> description = e.contentDescription
+ is ScrollElement -> {
+ scroll = e
+ chain.add(e)
+ }
+ is LayoutElement, is DrawElement -> chain.add(e)
+ else -> {}
+ }
+ }
+ this.chain = chain.toTypedArray()
+ var insets = false
+ for (k in 0 until chain.size) {
+ if (chain[k] is SafeInsetsElement) {
+ insets = true
+ }
+ }
+ usesSafeInsets = insets
+ this.weight = weight
+ this.alignSelf = align
+ this.matchParentSize = match
+ this.alpha = alpha
+ this.clickable = click
+ this.tap = tap
+ if (this.scroll?.state !== scroll?.state) {
+ this.scroll?.state?.detach(this)
+ scroll?.state?.attach(this, scroll.vertical)
+ }
+ this.scroll = scroll
+ isScrollableY = (scroll != null && scroll.vertical) || lazyAxis() == 1
+ isScrollableX = (scroll != null && !scroll.vertical) || lazyAxis() == 2
+ val interactive = (click != null && click.enabled) || tap != null
+ isGrabsPointerEvents = interactive
+ isFocusable = interactive
+ if (tag != null) {
+ name = tag
+ }
+ if (description != null) {
+ accessibilityText = description
+ }
+ sizes = LongArray(this.chain.size + 1)
+ boxes = IntArray((this.chain.size + 1) * 4)
+ }
+
+ // --- measuring ----------------------------------------------------------
+
+ /// Marks this node and every ancestor up to the composition's root as needing
+ /// a new measure pass, and asks the host for one.
+ fun invalidateMeasure() {
+ var n: Container? = this
+ while (n != null) {
+ if (n is UiNode) {
+ if (!n.measured && n !== this) {
+ break
+ }
+ n.measured = false
+ n.setShouldCalcPreferredSize(true)
+ } else if (n is ComposeHost) {
+ n.requestRemeasure()
+ break
+ }
+ n = n.parent
+ }
+ }
+
+ /// The host this node is composed into, or null while it is detached.
+ private fun composeHost(): ComposeHost? {
+ var n: Container? = parent
+ while (n != null) {
+ if (n is ComposeHost) {
+ return n
+ }
+ n = n.parent
+ }
+ return null
+ }
+
+ /// Measures this node under `c` and answers its size (packed with [size]).
+ fun measure(c: Constraints): Long {
+ val last = lastConstraints
+ if (measured && last != null && last.minWidth == c.minWidth && last.maxWidth == c.maxWidth &&
+ last.minHeight == c.minHeight && last.maxHeight == c.maxHeight) {
+ return sizes[0]
+ }
+ val result = measureChain(0, c)
+ lastConstraints = c
+ measured = true
+ placeChain()
+ return result
+ }
+
+ val measuredWidth: Int get() = sizes[0].w()
+ val measuredHeight: Int get() = sizes[0].h()
+
+ private fun measureChain(i: Int, c: Constraints): Long {
+ if (i == chain.size) {
+ val s = measureContent(c)
+ val r = size(c.constrainWidth(s.w()), c.constrainHeight(s.h()))
+ sizes[i] = r
+ return r
+ }
+ val d = Density
+ val r: Long = when (val e = chain[i]) {
+ is SafeInsetsElement -> {
+ val host = composeHost()
+ placedInsetTop = if (e.top && host != null) host.insetTop else 0
+ val v = placedInsetTop + (if (e.bottom && host != null) host.insetBottom else 0)
+ val inner = measureChain(i + 1, c.offset(0, v))
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h() + v))
+ }
+ is PaddingElement -> {
+ val l = d.px(e.start)
+ val t = d.px(e.top)
+ val h = l + d.px(e.end)
+ val v = t + d.px(e.bottom)
+ val inner = measureChain(i + 1, c.offset(h, v))
+ size(c.constrainWidth(inner.w() + h), c.constrainHeight(inner.h() + v))
+ }
+ is SizeElement -> {
+ val inner = measureChain(i + 1, sizeConstraints(e, c))
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h()))
+ }
+ is FillElement -> {
+ var minW = c.minWidth
+ var maxW = c.maxWidth
+ var minH = c.minHeight
+ var maxH = c.maxHeight
+ if (e.horizontal && c.hasBoundedWidth) {
+ val w = c.constrainWidth(Math.round(c.maxWidth * e.fraction))
+ minW = w
+ maxW = w
+ }
+ if (e.vertical && c.hasBoundedHeight) {
+ val h = c.constrainHeight(Math.round(c.maxHeight * e.fraction))
+ minH = h
+ maxH = h
+ }
+ val inner = measureChain(i + 1, Constraints(minW, maxW, minH, maxH))
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h()))
+ }
+ is WrapElement -> {
+ val inner = measureChain(i + 1, Constraints(
+ if (e.horizontal) 0 else c.minWidth,
+ if (e.horizontal && e.unbounded) Constraints.INF else c.maxWidth,
+ if (e.vertical) 0 else c.minHeight,
+ if (e.vertical && e.unbounded) Constraints.INF else c.maxHeight))
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h()))
+ }
+ is AspectRatioElement -> {
+ var cc = c
+ if (c.hasBoundedWidth && !c.hasFixedHeight) {
+ val h = c.constrainHeight(Math.round(c.maxWidth / e.ratio))
+ cc = Constraints(c.maxWidth, c.maxWidth, h, h)
+ } else if (c.hasBoundedHeight) {
+ val w = c.constrainWidth(Math.round(c.maxHeight * e.ratio))
+ cc = Constraints(w, w, c.maxHeight, c.maxHeight)
+ }
+ val inner = measureChain(i + 1, cc)
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h()))
+ }
+ is ScrollElement -> {
+ val inner = if (e.vertical) {
+ measureChain(i + 1, Constraints(c.minWidth, c.maxWidth, 0, Constraints.INF))
+ } else {
+ measureChain(i + 1, Constraints(0, Constraints.INF, c.minHeight, c.maxHeight))
+ }
+ size(c.constrainWidth(inner.w()), c.constrainHeight(inner.h()))
+ }
+ else -> measureChain(i + 1, c)
+ }
+ sizes[i] = r
+ return r
+ }
+
+ private fun sizeConstraints(e: SizeElement, c: Constraints): Constraints {
+ val d = Density
+ var minW = if (e.minWidth.value.isNaN()) -1 else d.px(e.minWidth)
+ var maxW = if (e.maxWidth.value.isNaN()) -1 else if (e.maxWidth.value.isInfinite()) Constraints.INF else d.px(e.maxWidth)
+ var minH = if (e.minHeight.value.isNaN()) -1 else d.px(e.minHeight)
+ var maxH = if (e.maxHeight.value.isNaN()) -1 else if (e.maxHeight.value.isInfinite()) Constraints.INF else d.px(e.maxHeight)
+ if (e.enforceIncoming) {
+ if (minW >= 0) minW = c.constrainWidth(minW)
+ if (maxW >= 0) maxW = c.constrainWidth(maxW)
+ if (minH >= 0) minH = c.constrainHeight(minH)
+ if (maxH >= 0) maxH = c.constrainHeight(maxH)
+ }
+ val rMinW = if (minW >= 0) minW else if (maxW >= 0) Math.min(c.minWidth, maxW) else c.minWidth
+ val rMaxW = if (maxW >= 0) Math.max(maxW, rMinW) else Math.max(c.maxWidth, rMinW)
+ val rMinH = if (minH >= 0) minH else if (maxH >= 0) Math.min(c.minHeight, maxH) else c.minHeight
+ val rMaxH = if (maxH >= 0) Math.max(maxH, rMinH) else Math.max(c.maxHeight, rMinH)
+ return Constraints(rMinW, rMaxW, rMinH, rMaxH)
+ }
+
+ /// Measures what is inside the innermost box. Containers run [policy] over
+ /// their children; leaves override this.
+ protected open fun measureContent(c: Constraints): Long {
+ val kids = ArrayList(componentCount)
+ for (k in 0 until componentCount) {
+ kids.add(getComponentAt(k))
+ }
+ if (placements.size != kids.size * 4) {
+ placements = IntArray(kids.size * 4)
+ }
+ return policy.measure(this, kids, c)
+ }
+
+ /// Records where a child goes, relative to the content box.
+ fun place(index: Int, x: Int, y: Int, w: Int, h: Int) {
+ if (placements.size < (index + 1) * 4) {
+ placements = placements.copyOf((index + 1) * 4)
+ }
+ placements[index * 4] = x
+ placements[index * 4 + 1] = y
+ placements[index * 4 + 2] = w
+ placements[index * 4 + 3] = h
+ }
+
+ /// Fixes each chain position's box, outermost first.
+ private fun placeChain() {
+ val d = Density
+ var x = 0
+ var y = 0
+ offsetX = 0
+ offsetY = 0
+ var leading = true
+ for (i in 0..chain.size) {
+ val w = sizes[i].w()
+ val h = sizes[i].h()
+ boxes[i * 4] = x
+ boxes[i * 4 + 1] = y
+ boxes[i * 4 + 2] = w
+ boxes[i * 4 + 3] = h
+ if (i == chain.size) {
+ break
+ }
+ val e = chain[i]
+ val nw = sizes[i + 1].w()
+ val nh = sizes[i + 1].h()
+ when (e) {
+ is SafeInsetsElement -> {
+ y += placedInsetTop
+ leading = false
+ }
+ is PaddingElement -> {
+ x += d.px(e.start)
+ y += d.px(e.top)
+ leading = false
+ }
+ is OffsetElement -> {
+ if (leading) {
+ offsetX += d.px(e.x)
+ offsetY += d.px(e.y)
+ } else {
+ x += d.px(e.x)
+ y += d.px(e.y)
+ }
+ }
+ is WrapElement -> {
+ val o = e.align.align(androidx.compose.ui.unit.IntSize(nw, nh),
+ androidx.compose.ui.unit.IntSize(w, h), LTR)
+ x += o.x
+ y += o.y
+ leading = false
+ }
+ is ScrollElement -> leading = false
+ is LayoutElement -> {
+ // A box forced larger than its content centers it, as Compose does.
+ x += (w - nw) / 2
+ y += (h - nh) / 2
+ leading = false
+ }
+ else -> leading = false
+ }
+ }
+ val n = chain.size * 4
+ contentX = boxes[n]
+ contentY = boxes[n + 1]
+ contentW = boxes[n + 2]
+ contentH = boxes[n + 3]
+ }
+
+ /// Copies the positions the last measure recorded onto the children.
+ internal fun applyPlacements() {
+ val count = componentCount
+ for (k in 0 until count) {
+ val child = getComponentAt(k)
+ if (placements.size < (k + 1) * 4) {
+ break
+ }
+ var x = contentX + placements[k * 4]
+ var y = contentY + placements[k * 4 + 1]
+ if (child is UiNode) {
+ x += child.offsetX
+ y += child.offsetY
+ }
+ child.setX(x)
+ child.setY(y)
+ child.setWidth(placements[k * 4 + 2])
+ child.setHeight(placements[k * 4 + 3])
+ }
+ }
+
+ /// The axis a node scrolls along by itself, without a scroll modifier: 1 for
+ /// vertical, 2 for horizontal. Lazy lists do.
+ internal open fun lazyAxis(): Int = 0
+
+ /// Measures again under the last constraints, at the same size, and places
+ /// the whole subtree now rather than on the next layout pass.
+ internal fun remeasureInPlace() {
+ val lc = lastConstraints ?: return
+ val w = measuredWidth
+ val h = measuredHeight
+ measured = false
+ measure(lc)
+ if (measuredWidth != w || measuredHeight != h) {
+ // The parent placed this node at the old size; only a full pass can move its siblings.
+ invalidateMeasure()
+ return
+ }
+ applyTree()
+ repaint()
+ }
+
+ private fun applyTree() {
+ applyPlacements()
+ for (k in 0 until componentCount) {
+ val child = getComponentAt(k)
+ if (child is UiNode) {
+ child.applyTree()
+ } else if (child is Container) {
+ child.layoutContainer()
+ }
+ }
+ }
+
+ internal fun layoutFromCn1() {
+ val w = width
+ val h = height
+ if (!measured || sizes[0].w() != w || sizes[0].h() != h) {
+ measure(Constraints.fixed(w, h))
+ }
+ applyPlacements()
+ if (pendingScroll >= 0) {
+ val v = pendingScroll
+ pendingScroll = -1
+ if (isScrollableY) cn1ScrollY(v) else cn1ScrollX(v)
+ }
+ }
+
+ override fun calcScrollSize(): Dimension {
+ // The scrolled extent is the box just inside the scroll modifier.
+ var n = chain.size * 4
+ for (i in chain.indices) {
+ if (chain[i] is ScrollElement) {
+ n = (i + 1) * 4
+ break
+ }
+ }
+ return Dimension(Math.max(width, boxes[n] + boxes[n + 2]), Math.max(height, boxes[n + 1] + boxes[n + 3]))
+ }
+
+ override fun calcPreferredSize(): Dimension {
+ val s = measure(Constraints(0, cn1DisplayWidth(), 0, Constraints.INF))
+ return Dimension(s.w(), s.h())
+ }
+
+ // --- painting -----------------------------------------------------------
+
+ override fun paintBackground(g: Graphics) {
+ val old = g.alpha
+ if (alpha < 1f) {
+ g.concatenateAlpha(Math.round(alpha * 255))
+ }
+ g.isAntiAliased = true
+ val ax = x
+ val ay = y
+ for (i in chain.indices) {
+ val e = chain[i]
+ if (e !is DrawElement) {
+ continue
+ }
+ val bx = ax + boxes[i * 4]
+ val by = ay + boxes[i * 4 + 1]
+ val bw = boxes[i * 4 + 2]
+ val bh = boxes[i * 4 + 3]
+ when (e) {
+ is BackgroundElement -> Painting.fill(g, e.shape, e.color, e.brush, bx, by, bw, bh)
+ is BorderElement -> Painting.stroke(g, e.shape, e.color, Density.px(e.width), bx, by, bw, bh)
+ is DrawBehindElement -> {
+ g.translate(bx, by)
+ try {
+ e.onDraw(Cn1DrawScope(g, Size(bw.toFloat(), bh.toFloat())))
+ } finally {
+ g.translate(-bx, -by)
+ }
+ }
+ else -> {}
+ }
+ }
+ g.alpha = old
+ }
+
+ override fun paint(g: Graphics) {
+ val old = g.alpha
+ if (alpha < 1f) {
+ g.concatenateAlpha(Math.round(alpha * 255))
+ }
+ val clip = clipBox()
+ if (clip >= 0) {
+ g.pushClip()
+ clipTo(g, (chain[clip] as ClipElement).shape, x + boxes[clip * 4], y + boxes[clip * 4 + 1],
+ boxes[clip * 4 + 2], boxes[clip * 4 + 3])
+ }
+ paintContent(g)
+ if (clip >= 0) {
+ g.popClip()
+ }
+ val click = clickable
+ if (pressed && click != null && click.indication) {
+ g.color = 0x000000
+ g.concatenateAlpha(30)
+ val shape = indicationShape()
+ Painting.fill(g, shape, androidx.compose.ui.graphics.Color.Black, null, x, y, width, height)
+ }
+ g.alpha = old
+ }
+
+ /// Paints what is inside the content box; containers paint their children.
+ protected open fun paintContent(g: Graphics) {
+ super.paint(g)
+ }
+
+ /// Clips to `shape`, as Compose does: a card's children stay inside its
+ /// rounded corners. `setClip(Shape)` replaces the clip, so the path is
+ /// first cut to the clip in force, or a card scrolled under a bar would
+ /// paint over it.
+ private fun clipTo(g: Graphics, shape: Shape, cx: Int, cy: Int, cw: Int, ch: Int) {
+ val o = Painting.outline(shape, cw, ch)
+ if (o is androidx.compose.ui.graphics.Outline.Rounded && g.isShapeClipSupported) {
+ if (!Painting.clipRounded(g, o, cx, cy)) {
+ // The clip in force misses the outline (it may still meet a
+ // corner the rounding cuts off): nothing is visible.
+ g.clipRect(0, 0, 0, 0)
+ }
+ return
+ }
+ g.clipRect(cx, cy, cw, ch)
+ }
+
+ private fun clipBox(): Int {
+ for (i in chain.indices) {
+ if (chain[i] is ClipElement) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ private fun indicationShape(): Shape {
+ for (e in chain) {
+ if (e is ClipElement) return e.shape
+ if (e is BackgroundElement) return e.shape
+ }
+ return androidx.compose.ui.graphics.RectangleShape
+ }
+
+ // --- input --------------------------------------------------------------
+
+ private var pressed = false
+ private var pressX = 0
+ private var pressY = 0
+ private var moved = false
+
+ override fun pointerPressed(x: Int, y: Int) {
+ super.pointerPressed(x, y)
+ if (clickable?.enabled == true || tap != null) {
+ pressed = true
+ moved = false
+ pressX = x
+ pressY = y
+ repaint()
+ }
+ }
+
+ override fun pointerDragged(x: Int, y: Int) {
+ if (pressed && !moved) {
+ val slop = Density.px(androidx.compose.ui.unit.Dp(8f))
+ if (Math.abs(x - pressX) > slop || Math.abs(y - pressY) > slop) {
+ moved = true
+ pressed = false
+ repaint()
+ }
+ }
+ super.pointerDragged(x, y)
+ }
+
+ override fun pointerReleased(x: Int, y: Int) {
+ super.pointerReleased(x, y)
+ if (pressed) {
+ pressed = false
+ repaint()
+ if (!moved && contains(x, y)) {
+ tap?.onTap?.invoke((x - absoluteX).toFloat(), (y - absoluteY).toFloat())
+ val c = clickable
+ if (c != null && c.enabled) {
+ c.onClick()
+ }
+ }
+ }
+ }
+
+ /// Clicks the node as a tap would, for tests and automation.
+ fun cn1PerformClick(): Boolean {
+ val c = clickable ?: return false
+ if (!c.enabled) return false
+ c.onClick()
+ return true
+ }
+
+ internal fun cn1ScrollY(v: Int) {
+ scrollY = Math.max(0, Math.min(v, Math.max(0, scrollDimension.height - height)))
+ }
+
+ private var pendingScroll = -1
+
+ /// A scroll position to restore once the node has been measured.
+ internal fun cn1ScrollPending(v: Int, vertical: Boolean) {
+ pendingScroll = v
+ }
+
+ internal fun cn1ScrollX(v: Int) {
+ scrollX = Math.max(0, Math.min(v, Math.max(0, scrollDimension.width - width)))
+ }
+
+ companion object {
+ private val EMPTY = arrayOf()
+ }
+}
+
+/// Codename One's layout pass for a node: copies out what measuring decided.
+internal object NodeLayout : Layout() {
+ override fun layoutContainer(parent: Container) {
+ // Checked, never cast: a failed cast does not throw on iOS.
+ (parent as? UiNode)?.layoutFromCn1()
+ }
+
+ override fun getPreferredSize(parent: Container): Dimension {
+ val node = parent as? UiNode ?: return Dimension(0, 0)
+ val s = node.measure(Constraints(0, cn1DisplayWidth(), 0, Constraints.INF))
+ return Dimension(s.w(), s.h())
+ }
+}
+
+internal fun cn1DisplayWidth(): Int = try {
+ com.codename1.ui.CN.getDisplayWidth().let { if (it > 0) it else 1080 }
+} catch (t: Throwable) {
+ 1080
+}
+
+internal fun cn1DisplayHeight(): Int = try {
+ com.codename1.ui.CN.getDisplayHeight().let { if (it > 0) it else 1920 }
+} catch (t: Throwable) {
+ 1920
+}
+
+/// Removes every theme decoration from a component that a node paints itself.
+///
+/// A node is never focused, pressed or disabled, so only its unselected style is
+/// ever read. Asking for `allStyles` created the other three, each with its own
+/// background painter, for every node: 3,335 styles and 1 MB of a 150-card
+/// screen's heap. A real input that takes focus passes `allStates`.
+internal fun cn1StripStyle(c: Component, allStates: Boolean = false) {
+ val s = if (allStates) c.allStyles else c.unselectedStyle
+ s.setPadding(0, 0, 0, 0)
+ s.setMargin(0, 0, 0, 0)
+ s.setBgTransparency(0)
+ s.setBorder(Border.getEmpty())
+}
+
+/// Measures any child: a node by its own rules, any other Codename One component
+/// by its preferred size, constrained.
+internal fun measureChild(child: Component, c: Constraints): Long {
+ if (child is UiNode) {
+ return child.measure(c)
+ }
+ val pw = child.preferredW
+ val ph = child.preferredH
+ return size(c.constrainWidth(pw), c.constrainHeight(ph))
+}
+
+internal fun Outline.cornerArc(): Int = when (this) {
+ is Outline.Rounded -> Math.round(Math.max(Math.max(topLeft, topRight), Math.max(bottomLeft, bottomRight)) * 2)
+ else -> 0
+}
diff --git a/maven/compose-compat/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory b/maven/compose-compat/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
new file mode 100644
index 00000000000..faf6dc9d6d0
--- /dev/null
+++ b/maven/compose-compat/src/main/resources/META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory
@@ -0,0 +1 @@
+com.codename1.compose.internal.EdtDispatcherFactory
diff --git a/maven/compose-compat/src/test/kotlin/com/codename1/compose/KeynoteAppTest.kt b/maven/compose-compat/src/test/kotlin/com/codename1/compose/KeynoteAppTest.kt
new file mode 100644
index 00000000000..262fe0eb87b
--- /dev/null
+++ b/maven/compose-compat/src/test/kotlin/com/codename1/compose/KeynoteAppTest.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose
+
+import com.codename1.compat.testing.HeadlessImplementation
+import com.codename1.compat.testing.MainThreadRule
+import androidx.compose.foundation.lazy.LazyListNode
+import com.codename1.compose.internal.TextNode
+import com.codename1.compose.internal.TextInputNode
+import com.codename1.compose.internal.UiNode
+import com.codename1.ui.Component
+import com.codename1.ui.Container
+import com.codename1.ui.Display
+import keynote.App
+import keynote.AppState
+import keynote.Programme
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+
+/// Drives the benchmark's hero application through every screen, headlessly.
+class KeynoteAppTest {
+ @get:Rule
+ val mainThread = MainThreadRule()
+
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ fun install() {
+ HeadlessImplementation.install()
+ }
+ }
+
+ private fun waitUntil(form: ComposeForm, what: String, cond: () -> Boolean) {
+ val end = System.currentTimeMillis() + 5000
+ while (System.currentTimeMillis() < end) {
+ ComposeTesting.settle(form)
+ if (cond()) return
+ Display.getInstance().invokeAndBlock { Thread.sleep(10) }
+ }
+ throw AssertionError("timed out waiting for $what")
+ }
+
+ private fun cards(root: Container): Int = count(root) { it.name?.startsWith("session-") == true }
+
+ /// How many items the session list holds; only the ones in view are composed.
+ private fun items(form: ComposeForm): Int = (ComposeTesting.find(form, "list") as LazyListNode).cn1ItemCount()
+
+ private fun count(c: Container, p: (Component) -> Boolean): Int {
+ var n = 0
+ for (i in 0 until c.componentCount) {
+ val k = c.getComponentAt(i)
+ if (p(k)) n++
+ if (k is Container) n += count(k, p)
+ }
+ return n
+ }
+
+ private fun texts(c: Container, out: MutableList = ArrayList()): List {
+ for (i in 0 until c.componentCount) {
+ val k = c.getComponentAt(i)
+ if (k is TextNode) out.addAll(k.cn1Lines())
+ if (k is Container) texts(k, out)
+ }
+ return out
+ }
+
+ private fun click(form: ComposeForm, tag: String) {
+ val node = ComposeTesting.find(form, tag) as UiNode
+ assertTrue("$tag is not clickable", node.cn1PerformClick())
+ ComposeTesting.settle(form)
+ }
+
+ @Test
+ fun everyScreen() {
+ val state = AppState()
+ val form = ComposeTesting.show { App(state) }
+ waitUntil(form, "the programme to load") { cards(form) > 0 }
+ val day1 = Programme.sessions.filter { it.day == 1 }
+ assertEquals(day1.size, items(form))
+ // Virtualized: the cards in view are composed, not the whole day.
+ val list = ComposeTesting.find(form, "list") as LazyListNode
+ assertTrue("composed ${cards(form)} of ${day1.size}", cards(form) < day1.size)
+ assertEquals(cards(form), list.cn1ComposedCount())
+ // Scrolling to the end composes the last card and disposes of the first.
+ list.cn1ScrollY(Int.MAX_VALUE)
+ ComposeTesting.settle(form)
+ list.cn1ScrollY(Int.MAX_VALUE)
+ ComposeTesting.settle(form)
+ assertEquals(1, count(form) { it.name == "session-${day1.last().id}" })
+ assertEquals(0, count(form) { it.name == "session-${day1.first().id}" })
+ list.cn1ScrollY(0)
+ ComposeTesting.settle(form)
+
+ // Favorite a session; it shows on the favorites tab.
+ click(form, "fav-0")
+ assertEquals(listOf(0), state.favorites.toList())
+ click(form, "tab-favorites")
+ assertEquals(1, items(form))
+ assertEquals(1, cards(form))
+ click(form, "tab-schedule")
+
+ // Day chips and search filter the list.
+ click(form, "day-2")
+ assertEquals(Programme.sessions.count { it.day == 2 }, items(form))
+ val input = ComposeTesting.find(form, "search")
+ val field = findInput(input as Container)!!
+ field.field.text = "Compose"
+ ComposeTesting.settle(form)
+ assertEquals("Compose", state.query)
+ val expected = Programme.sessions.count { it.day == 2 && (it.title.contains("Compose", true) ||
+ it.speaker.name.contains("Compose", true)) }
+ assertEquals(expected, items(form))
+
+ // Open a session and come back.
+ val first = Programme.sessions.first { it.day == 2 && it.title.contains("Compose", true) }
+ click(form, "session-${first.id}")
+ assertTrue(texts(form.contentPane).joinToString(" ").contains(first.speaker.name))
+ click(form, "back")
+ assertNull(state.selected)
+
+ // Insights: the chart animates in.
+ click(form, "tab-insights")
+ waitUntil(form, "the chart") { count(form) { it.name == "chart" } == 1 }
+ assertTrue(texts(form.contentPane).contains("You have 1 favorite sessions."))
+
+ // Paint every screen state once, to catch a painting failure.
+ val img = com.codename1.ui.Image.createImage(form.width, form.height)
+ form.paintComponent(img.graphics, true)
+ }
+
+ private fun findInput(c: Container): TextInputNode? {
+ for (i in 0 until c.componentCount) {
+ val k = c.getComponentAt(i)
+ if (k is TextInputNode) return k
+ if (k is Container) findInput(k)?.let { return it }
+ }
+ return null
+ }
+}
diff --git a/maven/compose-compat/src/test/kotlin/com/codename1/compose/LayoutTest.kt b/maven/compose-compat/src/test/kotlin/com/codename1/compose/LayoutTest.kt
new file mode 100644
index 00000000000..a87432c193b
--- /dev/null
+++ b/maven/compose-compat/src/test/kotlin/com/codename1/compose/LayoutTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.dp
+import com.codename1.compat.testing.HeadlessImplementation
+import com.codename1.compat.testing.MainThreadRule
+import com.codename1.ui.Component
+import com.codename1.ui.Container
+import com.codename1.ui.Display
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+
+class LayoutTest {
+ @get:Rule
+ val mainThread = MainThreadRule()
+
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ fun install() {
+ HeadlessImplementation.install()
+ }
+ }
+
+ @Test
+ fun columnRowAndWeights() {
+ val form = ComposeTesting.show {
+ Column(Modifier.fillMaxSize().padding(10.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Box(Modifier.size(50.dp).background(Color.Red).testTag("a"))
+ Row(Modifier.fillMaxWidth().height(20.dp)) {
+ Box(Modifier.weight(1f).fillMaxSize().testTag("w1"))
+ Box(Modifier.width(30.dp).fillMaxSize().testTag("fixed"))
+ Box(Modifier.weight(2f).fillMaxSize().testTag("w2"))
+ }
+ Box(Modifier.fillMaxWidth().height(10.dp), contentAlignment = Alignment.Center) {
+ Spacer(Modifier.size(4.dp).testTag("centered"))
+ }
+ }
+ }
+ // Two pixels per dp under the headless implementation.
+ val a = ComposeTesting.find(form, "a")
+ assertEquals(20, a.absoluteX - form.contentPane.absoluteX)
+ assertEquals(100, a.width)
+ val w1 = ComposeTesting.find(form, "w1")
+ val fixed = ComposeTesting.find(form, "fixed")
+ val w2 = ComposeTesting.find(form, "w2")
+ assertEquals(60, fixed.width)
+ val total = form.contentPane.width - 40
+ assertEquals(total - 60, w1.width + w2.width)
+ org.junit.Assert.assertTrue(Math.abs(w1.width * 2 - w2.width) <= 1)
+ assertEquals(40, w1.height)
+ assertEquals(a.absoluteY + 100 + 8, w1.absoluteY)
+ val c = ComposeTesting.find(form, "centered")
+ assertEquals(8, c.width)
+ assertEquals(w1.absoluteY + 40 + 8 + 6, c.absoluteY)
+ }
+
+ @Test
+ fun clickRecomposes() {
+ var count by mutableIntStateOf(0)
+ val form = ComposeTesting.show {
+ Column {
+ BasicText("Count $count", Modifier.testTag("label"))
+ Box(Modifier.size(40.dp).clickable { count++ }.testTag("button"))
+ }
+ }
+ val label = ComposeTesting.find(form, "label") as com.codename1.compose.internal.TextNode
+ assertEquals(listOf("Count 0"), label.cn1Lines())
+ val button = ComposeTesting.find(form, "button") as com.codename1.compose.internal.UiNode
+ button.cn1PerformClick()
+ ComposeTesting.settle(form)
+ assertEquals(listOf("Count 1"), label.cn1Lines())
+ assertEquals(7 * HeadlessImplementation.CHAR_WIDTH, label.width)
+ }
+
+ @Test
+ fun textWraps() {
+ val form = ComposeTesting.show {
+ Box(Modifier.width(50.dp)) {
+ BasicText("aaa bbb ccc", Modifier.testTag("t"))
+ }
+ }
+ val t = ComposeTesting.find(form, "t") as com.codename1.compose.internal.TextNode
+ assertEquals(listOf("aaa", "bbb", "ccc"), t.cn1Lines())
+ assertEquals(3 * HeadlessImplementation.FONT_HEIGHT, t.height)
+ }
+}
+
+/// Shows compositions and waits for them to settle.
+object ComposeTesting {
+ fun show(content: @androidx.compose.runtime.Composable () -> Unit): ComposeForm {
+ val f = ComposeForm(content = content)
+ f.show()
+ settle(f)
+ return f
+ }
+
+ fun settle(f: ComposeForm) {
+ for (i in 0 until 50) {
+ MainThreadRule.drain()
+ f.view.cn1Frame()
+ f.revalidate()
+ if (!f.view.hasPendingWork()) {
+ MainThreadRule.drain()
+ f.revalidate()
+ return
+ }
+ }
+ throw AssertionError("composition did not settle")
+ }
+
+ fun find(root: Container, tag: String): Component {
+ val r = search(root, tag)
+ assertNotNull("no node tagged $tag", r)
+ return r!!
+ }
+
+ private fun search(c: Container, tag: String): Component? {
+ for (i in 0 until c.componentCount) {
+ val k = c.getComponentAt(i)
+ if (tag == k.name) return k
+ if (k is Container) search(k, tag)?.let { return it }
+ }
+ return null
+ }
+}
diff --git a/maven/compose-compat/src/test/kotlin/com/codename1/compose/LazyListTest.kt b/maven/compose-compat/src/test/kotlin/com/codename1/compose/LazyListTest.kt
new file mode 100644
index 00000000000..6f9dae6c451
--- /dev/null
+++ b/maven/compose-compat/src/test/kotlin/com/codename1/compose/LazyListTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.compose
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListNode
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.dp
+import com.codename1.compat.testing.HeadlessImplementation
+import com.codename1.compat.testing.MainThreadRule
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+
+class LazyListTest {
+ @get:Rule
+ val mainThread = MainThreadRule()
+
+ companion object {
+ @BeforeClass
+ @JvmStatic
+ fun install() {
+ HeadlessImplementation.install()
+ }
+
+ var itemRuns = 0
+ }
+
+ private fun list(form: ComposeForm): LazyListNode = ComposeTesting.find(form, "list") as LazyListNode
+
+ @Test
+ fun parentRecompositionDoesNotRecomposeItems() {
+ var count by mutableIntStateOf(0)
+ itemRuns = 0
+ val form = ComposeTesting.show {
+ Column {
+ BasicText("Count $count", Modifier.testTag("label"))
+ LazyColumn(Modifier.height(300.dp).testTag("list")) {
+ items(100, key = { it }) {
+ itemRuns++
+ BasicText("item $it", Modifier.height(40.dp))
+ }
+ }
+ }
+ }
+ val composed = list(form).cn1ComposedCount()
+ assertTrue("only the items in view are composed", composed in 1 until 100)
+ assertEquals(composed, itemRuns)
+ count++
+ ComposeTesting.settle(form)
+ val label = ComposeTesting.find(form, "label") as com.codename1.compose.internal.TextNode
+ assertEquals(listOf("Count 1"), label.cn1Lines())
+ assertEquals("a recomposition above the list must not recompose its items", composed, itemRuns)
+ }
+
+ @Test
+ fun duplicateKeysAreRefused() {
+ try {
+ ComposeTesting.show {
+ LazyColumn(Modifier.height(300.dp).testTag("list")) {
+ items(5, key = { "same" }) { BasicText("item $it", Modifier.height(40.dp)) }
+ }
+ }
+ fail("two items with one key must be refused")
+ } catch (e: IllegalArgumentException) {
+ assertTrue(e.message!!.contains("same"))
+ }
+ }
+
+ @Test
+ fun scrollToItemLandsOnTheMeasuredItem() {
+ var state: LazyListState? = null
+ val form = ComposeTesting.show {
+ val s = rememberLazyListState()
+ state = s
+ // Sizes nothing like the estimate taken from the first items, so the target's
+ // position is only right once its neighbours have been measured.
+ LazyColumn(Modifier.height(300.dp).testTag("list"), state = s) {
+ items(200, key = { it }) {
+ BasicText("item $it", Modifier.fillMaxWidth().height(if (it < 10) 20.dp else (30 + (it % 7) * 15).dp)
+ .testTag("item-$it"))
+ }
+ }
+ }
+ val node = list(form)
+ for (target in intArrayOf(150, 60, 199, 3)) {
+ runBlocking { state!!.scrollToItem(target) }
+ ComposeTesting.settle(form)
+ val item = ComposeTesting.find(form, "item-$target")
+ // A container's absolute Y has its own scroll taken off; the view's top is this.
+ val top = node.absoluteY + node.scrollY
+ val want = if (target == 199) top + node.height - item.height else top
+ assertEquals("item $target starts where scrollToItem put it", want, item.absoluteY)
+ if (target != 199) assertEquals(target, state!!.firstVisibleItemIndex)
+ }
+ }
+
+ @Test
+ fun aLongListCostsTheWindow() {
+ var state: LazyListState? = null
+ val form = ComposeTesting.show {
+ val s = rememberLazyListState()
+ state = s
+ LazyColumn(Modifier.height(300.dp).testTag("list"), state = s) {
+ items(100000, key = { it }) {
+ BasicText("item $it", Modifier.height((20 + (it % 5) * 10).dp).testTag("item-$it"))
+ }
+ }
+ }
+ val node = list(form)
+ var target = 0
+ while (target < 100000) {
+ runBlocking { state!!.scrollToItem(target) }
+ ComposeTesting.settle(form)
+ assertTrue("composed ${node.cn1ComposedCount()}", node.cn1ComposedCount() < 40)
+ val item = ComposeTesting.find(form, "item-$target")
+ if (target < 99990) assertEquals(node.absoluteY + node.scrollY, item.absoluteY)
+ target += 9973
+ }
+ }
+
+ @Test
+ fun userScrollCanBeTurnedOff() {
+ val form = ComposeTesting.show {
+ LazyColumn(Modifier.height(300.dp).testTag("list"), userScrollEnabled = false) {
+ items(100, key = { it }) { BasicText("item $it", Modifier.height(40.dp)) }
+ }
+ }
+ assertFalse(list(form).isScrollableY)
+ }
+}
diff --git a/maven/core-unittests/spotbugs-exclude.xml b/maven/core-unittests/spotbugs-exclude.xml
index 2177df3765d..6e4de2f489a 100644
--- a/maven/core-unittests/spotbugs-exclude.xml
+++ b/maven/core-unittests/spotbugs-exclude.xml
@@ -379,6 +379,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/SafeAreaRevalidateLoopTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/SafeAreaRevalidateLoopTest.java
index bf785e1f75b..c5915e4e48c 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ui/SafeAreaRevalidateLoopTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/SafeAreaRevalidateLoopTest.java
@@ -33,12 +33,15 @@
import java.util.ArrayList;
import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* A safe-area container writes inset padding onto its own style, measures or lays out
* with it, and hands the padding straight back. That round trip must be silent.
*
+ *
The comment below is kept as the history of why.
+ *
*
It used to announce the setting half as a PADDING style change, and
* {@link Component#styleChanged} answers a PADDING change by queueing a
* {@code revalidateLater()} on the parent. So laying out a Form with a safe-area
@@ -80,7 +83,7 @@ private Container notchedSafeAreaForm(Form f) {
}
@FormTest
- void measuringASafeAreaContainerAnnouncesItsPadding() {
+ void measuringASafeAreaContainerIsSilent() {
Form f = Display.getInstance().getCurrent();
Container inner = notchedSafeAreaForm(f);
@@ -88,18 +91,33 @@ void measuringASafeAreaContainerAnnouncesItsPadding() {
inner.getStyle().addStyleListener(rec);
inner.setShouldCalcPreferredSize(true);
inner.getPreferredSize();
+ inner.layoutContainer();
inner.getStyle().removeStyleListener(rec);
- // It DOES announce, and that is load-bearing -- see the comment on the
- // snapToSafeAreaInternal call in Container. Suppressing it removes the
- // revalidate treadmill, and with it the repaints that peer components
- // depend on: the JavaSE video peer fills its buffer from the AWT side
- // and never requests a repaint itself, so with nothing else repainting
- // the form its frames decode correctly and never reach the screen.
- // Pinned so the optimisation is not reinstated without first making
- // peers drive their own repaints.
- assertTrue(rec.properties.size() > 0,
- "measuring a safe-area container must still announce its padding");
+ assertTrue(rec.properties.isEmpty(),
+ "the safe-area round trip announced " + rec.properties);
+ }
+
+ /**
+ * What the announcement used to hide: a component painted by its parent
+ * kept the flag its own full repaint() set, so each later region repaint --
+ * a peer's new video frame -- was dropped as already covered.
+ */
+ @FormTest
+ void aRegionRepaintAfterAParentPaintIsKept() {
+ Form f = Display.getInstance().getCurrent();
+ f.setLayout(new BorderLayout());
+ Label child = new Label("peer");
+ f.add(BorderLayout.CENTER, child);
+ f.show();
+ f.layoutContainer();
+
+ child.repaint();
+ Image img = Image.createImage(f.getWidth(), f.getHeight());
+ f.paintComponent(img.getGraphics(), true);
+
+ child.repaint(child.getAbsoluteX(), child.getAbsoluteY(), 4, 4);
+ assertNotNull(child.getDirtyRegion(), "a region repaint after the parent painted was dropped");
}
}
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathTest.java
index c7389150fdf..9f1ad248463 100644
--- a/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathTest.java
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/geom/GeneralPathTest.java
@@ -191,6 +191,29 @@ void testContainsAndIntersectsRectangle() {
assertFalse(path.intersect(noOverlap));
}
+ /// A rounded rectangle cut by a clip that crosses its corners: the curves
+ /// running right to left made the crossing count -1, which segmenting
+ /// mistook for an unsupported curve order and threw.
+ @Test
+ void testIntersectRoundedRectangleAcrossCorners() {
+ GeneralPath path = new GeneralPath();
+ path.moveTo(10, 0);
+ path.lineTo(90, 0);
+ path.quadTo(100, 0, 100, 10);
+ path.lineTo(100, 40);
+ path.quadTo(100, 50, 90, 50);
+ path.lineTo(10, 50);
+ path.quadTo(0, 50, 0, 40);
+ path.lineTo(0, 10);
+ path.quadTo(0, 0, 10, 0);
+ path.closePath();
+ assertTrue(path.intersect(new Rectangle(-5, 5, 110, 30)));
+ Rectangle bounds = path.getBounds();
+ assertEquals(0, bounds.getX());
+ assertEquals(100, bounds.getX() + bounds.getWidth());
+ assertTrue(bounds.getY() >= 5 && bounds.getY() + bounds.getHeight() <= 35);
+ }
+
@Test
void testEqualsWithTransform() {
GeneralPath original = new GeneralPath();
diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/util/ThemeStringsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/util/ThemeStringsTest.java
new file mode 100644
index 00000000000..2d101ffb81d
--- /dev/null
+++ b/maven/core-unittests/src/test/java/com/codename1/ui/util/ThemeStringsTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+
+package com.codename1.ui.util;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class ThemeStringsTest {
+
+ @Test
+ void readUtfRejectsBadContinuationBytes() {
+ final byte[][] bad = {{0, 2, (byte) 0xc3, 0x41}, {0, 3, (byte) 0xe2, (byte) 0x82, 0x41}, {0, 1, (byte) 0x80}};
+ for (final byte[] b : bad) {
+ assertThrows(IOException.class, () -> new ThemeStrings(1).readUTF(new DataInputStream(new ByteArrayInputStream(b))));
+ }
+ }
+
+ @Test
+ void hexAndDecimalMatchTheJdk() {
+ ThemeStrings s = new ThemeStrings(0);
+ int[] values = {0, 1, 9, 10, 15, 16, 0xff, 0x7aff, 0xffffff, 0x1000000, -1, Integer.MIN_VALUE, Integer.MAX_VALUE};
+ for (int v : values) {
+ assertEquals(Integer.toHexString(v), s.hex(v));
+ }
+ for (int v = 0; v < 256; v++) {
+ assertEquals(String.valueOf(v), s.decimal(v));
+ }
+ }
+
+ @Test
+ void readUtfRoundTripsAndSharesRepeats() throws IOException {
+ String[] values = {"", "ffffff", "native:MainRegular", "caf\u00e9", "\u05e9\u05dc\u05d5\u05dd", "\u0000x", "ffffff"};
+ ByteArrayOutputStream bo = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bo);
+ for (String v : values) {
+ out.writeUTF(v);
+ }
+ DataInputStream in = new DataInputStream(new ByteArrayInputStream(bo.toByteArray()));
+ ThemeStrings s = new ThemeStrings(0);
+ String[] read = new String[values.length];
+ for (int i = 0; i < values.length; i++) {
+ read[i] = s.readUTF(in);
+ assertEquals(values[i], read[i]);
+ }
+ assertSame(read[1], read[6]);
+ assertSame(read[1], s.hex(0xffffff));
+ assertSame(read[2], s.share(new String("native:MainRegular")));
+ }
+
+ @Test
+ void manyDistinctValuesSurviveGrowth() {
+ ThemeStrings s = new ThemeStrings(0);
+ String[] first = new String[5000];
+ for (int i = 0; i < first.length; i++) {
+ first[i] = s.share("k" + i);
+ }
+ for (int i = 0; i < first.length; i++) {
+ assertSame(first[i], s.share("k" + i));
+ }
+ }
+
+ @Test
+ void floatsAndIntsMatchConcatenation() {
+ ThemeStrings s = new ThemeStrings(16);
+ for (int k = -20000; k <= 20000; k++) {
+ float f = (float) (k / 10.0);
+ assertEquals("" + f + "," + f + "," + f + "," + f, s.floats(f, f, f, f));
+ }
+ float[] odd = {0.25f, 1.125f, 1e7f, 3.4e38f, -0.0f, 1e-5f, 0.001f, Float.NaN, Float.POSITIVE_INFINITY, 123456.7f, 999999.9f};
+ for (float f : odd) {
+ assertEquals("" + f + "," + 0f + "," + f + "," + 1f, s.floats(f, 0f, f, 1f));
+ }
+ for (int v = 0; v < 256; v++) {
+ assertEquals("" + v + "," + 255 + "," + 0 + "," + v, s.ints(v, 255, 0, v));
+ }
+ }
+}
diff --git a/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java b/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java
index f2dd663e152..91f48ba7cec 100644
--- a/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java
+++ b/maven/css-compiler/src/main/java/com/codename1/ui/util/EditableResources.java
@@ -1724,14 +1724,14 @@ public long getDataSize(String id) {
/**
* Adapts loaded themes to the new resource selected/unselected modes
*/
- Hashtable loadTheme(String id, boolean newerVersion) throws IOException {
+ HashMap loadThemeMap(String id, boolean newerVersion) throws IOException {
if(overrideResource != null) {
- Hashtable h = overrideResource.loadTheme(id, newerVersion);
+ HashMap h = overrideResource.loadThemeMap(id, newerVersion);
if(h != null) {
return h;
}
}
- Hashtable h = super.loadTheme(id, newerVersion);
+ HashMap h = super.loadThemeMap(id, newerVersion);
Iterator keyIter = h.keySet().iterator();
while(keyIter.hasNext()) {
diff --git a/maven/integration-tests/kmp-compat-test.sh b/maven/integration-tests/kmp-compat-test.sh
new file mode 100755
index 00000000000..117c2297544
--- /dev/null
+++ b/maven/integration-tests/kmp-compat-test.sh
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation. Codename One designates this
+# particular file as subject to the "Classpath" exception as provided
+# by Oracle in the LICENSE file that accompanied this code.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Codename One through http://www.codenameone.com/ if you
+# need additional information or have any questions.
+#
+# Kotlin Multiplatform sources as a Codename One application, end to end, for
+# every sample under scripts/kmp-compat-samples:
+# - cn1:import-kmp-project copies commonMain (and the JVM source set, minus
+# its desktop window) into common/src/main/kmp and writes the main class;
+# - the Maven build compiles them with the Compose compiler against
+# compose-compat and passes the bytecode compliance check;
+# - the Gradle conversion applies the Compose plugin, and both tools stage
+# the same upload for every target, carrying the Compose runtime and
+# compose-compat as they are -- androidx.compose is never relocated.
+#
+# Needs the reactor installed (mvn install) and a JDK 17+; see inc/gradle.sh.
+SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
+set -e
+SAMPLES="$SCRIPTPATH/../../scripts/kmp-compat-samples"
+source "$SCRIPTPATH/inc/env.sh"
+source "$SCRIPTPATH/inc/gradle.sh"
+source "$SCRIPTPATH/inc/compat.sh"
+
+WORKDIR="${CN1_COMPAT_WORKDIR:-$SCRIPTPATH/build}/kmp-compat"
+rm -rf "$WORKDIR"
+mkdir -p "$WORKDIR"
+FAILED=0
+
+check_kmp_upload() {
+ local jar="$1"
+ assert_zip_has "$jar" "(^|/)$MAIN_ENTRY\\.class\$"
+ assert_zip_has "$jar" "^androidx/compose/runtime/Recomposer\\.class\$"
+ assert_zip_has "$jar" "^com/codename1/compose/ComposeView\\.class\$"
+ assert_zip_has "$jar" "^kotlinx/coroutines/.*\\.class\$"
+ # The dispatcher kotlinx.coroutines finds for Dispatchers.Main.
+ assert_zip_has "$jar" "^META-INF/services/kotlinx\\.coroutines\\.internal\\.MainDispatcherFactory\$"
+ assert_zip_lacks "$jar" "^com/codename1/androidcompat/androidx/compose/"
+}
+
+for sample_dir in "$SAMPLES"/*/; do
+ sample=$(basename "$sample_dir")
+ echo "== $sample"
+ W="$WORKDIR/$sample"
+ mkdir -p "$W"
+ PKG=com.acme.kmp
+ compat_generate_app "$sample" "$W" $PKG KmpApp
+ MAPP="$W/app"
+ (cd "$MAPP" && mvn_local "com.codenameone:codenameone-maven-plugin:$CN1_VERSION:import-kmp-project" \
+ "-Dcn1.kmp.import=$sample_dir") > "$W/import.log" 2>&1 \
+ || { tail -40 "$W/import.log"; fail "$sample: import-kmp-project"; }
+ kmp="$MAPP/common/src/main/kmp"
+ [ -n "$(find "$kmp/commonMain" -name '*.kt' 2>/dev/null | head -1)" ] || fail "$sample: the import copied no commonMain sources"
+ [ -z "$(grep -rl 'androidx\.compose\.ui\.window' "$kmp" 2>/dev/null)" ] \
+ || fail "$sample: the import kept a source that opens a desktop window"
+ grep -q 'left out: iosMain' "$W/import.log" || fail "$sample: the import did not report the iosMain source set"
+ MAIN_ENTRY=com/acme/kmp/KmpApp
+
+ compat_build_common "$sample" "$W" "$MAPP"
+ grep -q 'bytecode-compliance' "$W/mvn-common.log" || fail "$sample: the compliance check did not run"
+ classes="$MAPP/common/target/classes"
+ [ -f "$classes/$MAIN_ENTRY.class" ] || fail "$sample: the build produced no $MAIN_ENTRY.class"
+ # The Compose compiler ran: a composable takes the Composer it added.
+ find "$classes" -name 'AppKt.class' -exec javap -p {} \; 2>/dev/null | grep -q 'androidx.compose.runtime.Composer' \
+ || fail "$sample: App() was not compiled by the Compose compiler"
+
+ compat_convert_to_gradle "$sample" "$W" "$MAPP"
+ GAPP="$W/app-gradle"
+ [ -d "$GAPP/src/main/kmp" ] || fail "$sample: the Gradle conversion dropped src/main/kmp"
+ grep -q 'org.jetbrains.kotlin.plugin.compose' "$GAPP/build.gradle.kts" \
+ || fail "$sample: the Gradle conversion did not apply the Compose compiler plugin"
+
+ compat_stage_all "$sample" "$W" "$MAPP" "$GAPP" check_kmp_upload
+done
+
+[ "$FAILED" = 0 ] || { echo "kmp-compat-test: FAILED"; exit 1; }
+echo "kmp-compat-test: OK"
diff --git a/maven/pom.xml b/maven/pom.xml
index 0e9749d93bf..7c3bea5cf4c 100644
--- a/maven/pom.xml
+++ b/maven/pom.xml
@@ -97,6 +97,8 @@
android-compatswing-compatjavafx-compat
+
+ compose-compatfxml-compilerlottie-transcoder
+
+ compose
+
+
+ ${basedir}/src/main/kmp
+
+
+
+
+
+ google
+ https://maven.google.com
+
+
+
+
+ com.codenameone
+ codenameone-compose-compat
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-compose-compiler-plugin-embeddable
+ ${kotlin.version}
+
+
+
+
+
+
+
@@ -148,7 +192,9 @@
- 1.6.0
+
+ 2.2.10true
@@ -163,7 +209,7 @@
org.jetbrainsannotations
- 13.0
+ ${kotlin.annotations.version}com.codenameone
@@ -212,6 +258,9 @@
${android.gallery.dir}/kotlin${android.gallery.dir}/java${project.build.directory}/generated-sources/android
+
+ ${project.basedir}/src/main/kmp/commonMain/kotlin
+ ${project.basedir}/src/main/kmp/jvmMain/kotlin-no-reflect
@@ -517,8 +566,7 @@
build that does not used to name the libraries one by one, and
the one nobody had named was left out of the translation - the
application then failed in C, on the header of a class no
- source directory supplied. Kotlin's runtime is the exception:
- those builds stage it themselves. -->
+ source directory supplied. -->
org.apache.maven.pluginsmaven-dependency-plugin
@@ -532,7 +580,13 @@
runtime
- org.jetbrains.kotlin,org.jetbrains
+
META-INF/**${project.build.directory}/translation-libs
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
index aba2a1209c4..c80ccc5d16b 100644
--- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java
@@ -58,6 +58,10 @@
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatSwingTreesScreenshotTest;
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatSwingPaintingScreenshotTest;
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatSwingContainersScreenshotTest;
+import com.codenameone.examples.hellocodenameone.tests.kmpcompat.KmpCompatLayoutScreenshotTest;
+import com.codenameone.examples.hellocodenameone.tests.kmpcompat.KmpCompatTypographyScreenshotTest;
+import com.codenameone.examples.hellocodenameone.tests.kmpcompat.KmpCompatListScreenshotTest;
+import com.codenameone.examples.hellocodenameone.tests.kmpcompat.KmpCompatStateScreenshotTest;
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatSwingOptionPaneScreenshotTest;
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatSwingXScreenshotTest;
import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatFxControlsScreenshotTest;
@@ -707,6 +711,11 @@ private static int testTimeoutMs(BaseTest testClass) {
new DesktopCompatFxFxmlScreenshotTest(),
new DesktopCompatFxPanesScreenshotTest(),
new DesktopCompatFxAlertScreenshotTest(),
+ // Compose (common/src/main/kmp), embedded through ComposeView.
+ new KmpCompatLayoutScreenshotTest(),
+ new KmpCompatTypographyScreenshotTest(),
+ new KmpCompatListScreenshotTest(),
+ new KmpCompatStateScreenshotTest(),
// After every test that shows a dialog. It asks for the notification
// permission, and on iOS the system alert nobody answers leaves the
// application inactive for the rest of the run: Display.isMinimized()
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatLayoutScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatLayoutScreenshotTest.java
new file mode 100644
index 00000000000..db42bbcb5fe
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatLayoutScreenshotTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.tests.kmpcompat;
+
+import com.codename1.ui.Component;
+import com.codenameone.examples.hellocodenameone.kmp.KmpScenes;
+import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatScreenshotTest;
+
+/// A Compose screen of common/src/main/kmp, embedded as a component.
+public class KmpCompatLayoutScreenshotTest extends DesktopCompatScreenshotTest {
+ public KmpCompatLayoutScreenshotTest() {
+ super("Compose layout", "KmpCompatLayout");
+ }
+
+ @Override
+ protected Component scene() {
+ return KmpScenes.layout();
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatListScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatListScreenshotTest.java
new file mode 100644
index 00000000000..a9c411b8e7d
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatListScreenshotTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.tests.kmpcompat;
+
+import com.codename1.ui.Component;
+import com.codenameone.examples.hellocodenameone.kmp.KmpScenes;
+import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatScreenshotTest;
+
+/// A Compose screen of common/src/main/kmp, embedded as a component.
+public class KmpCompatListScreenshotTest extends DesktopCompatScreenshotTest {
+ public KmpCompatListScreenshotTest() {
+ super("Compose list", "KmpCompatList");
+ }
+
+ @Override
+ protected Component scene() {
+ return KmpScenes.list();
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatStateScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatStateScreenshotTest.java
new file mode 100644
index 00000000000..0831fa13bd3
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatStateScreenshotTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.tests.kmpcompat;
+
+import com.codename1.ui.Component;
+import com.codenameone.examples.hellocodenameone.kmp.KmpScenes;
+import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatScreenshotTest;
+
+/// A Compose screen of common/src/main/kmp, embedded as a component.
+public class KmpCompatStateScreenshotTest extends DesktopCompatScreenshotTest {
+ public KmpCompatStateScreenshotTest() {
+ super("Compose state", "KmpCompatState");
+ }
+
+ @Override
+ protected Component scene() {
+ return KmpScenes.state();
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatTypographyScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatTypographyScreenshotTest.java
new file mode 100644
index 00000000000..5288d27e210
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/kmpcompat/KmpCompatTypographyScreenshotTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.tests.kmpcompat;
+
+import com.codename1.ui.Component;
+import com.codenameone.examples.hellocodenameone.kmp.KmpScenes;
+import com.codenameone.examples.hellocodenameone.tests.desktopcompat.DesktopCompatScreenshotTest;
+
+/// A Compose screen of common/src/main/kmp, embedded as a component.
+public class KmpCompatTypographyScreenshotTest extends DesktopCompatScreenshotTest {
+ public KmpCompatTypographyScreenshotTest() {
+ super("Compose typography", "KmpCompatTypography");
+ }
+
+ @Override
+ protected Component scene() {
+ return KmpScenes.typography();
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/kmp/commonMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/Screens.kt b/scripts/hellocodenameone/common/src/main/kmp/commonMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/Screens.kt
new file mode 100644
index 00000000000..fa9b30f203e
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/kmp/commonMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/Screens.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.kmp
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+/// Box, Row and Column under padding, size, background, border and weight.
+@Composable
+fun LayoutScreen() {
+ MaterialTheme {
+ Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Box(Modifier.weight(1f).height(48.dp).background(Color(0xFFE53935)))
+ Box(Modifier.weight(2f).height(48.dp).background(Color(0xFF43A047)))
+ Box(Modifier.size(48.dp).background(Color(0xFF1E88E5), RoundedCornerShape(12.dp)))
+ }
+ Box(Modifier.fillMaxWidth().height(96.dp).border(2.dp, Color(0xFF6750A4), RoundedCornerShape(8.dp)),
+ contentAlignment = Alignment.Center) {
+ Text("Centered in a bordered box")
+ }
+ Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier.size(32.dp).background(Color(0xFFFFB300)))
+ Spacer(Modifier.size(12.dp))
+ Column {
+ Text("Row with a spacer", fontWeight = FontWeight.Bold)
+ Text("and a nested column")
+ }
+ }
+ }
+ }
+}
+
+/// The Material 3 type scale and buttons.
+@Composable
+fun TypographyScreen() {
+ MaterialTheme {
+ Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("Headline large", style = MaterialTheme.typography.headlineLarge)
+ Text("Title medium", style = MaterialTheme.typography.titleMedium)
+ Text("Body medium text that is long enough to wrap onto a second line in a phone width column.",
+ style = MaterialTheme.typography.bodyMedium)
+ Text("Label small", style = MaterialTheme.typography.labelSmall)
+ HorizontalDivider()
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Button(onClick = {}) { Text("Filled") }
+ OutlinedButton(onClick = {}) { Text("Outlined") }
+ TextButton(onClick = {}) { Text("Text") }
+ }
+ }
+ }
+}
+
+/// A Scaffold with a top app bar over a LazyColumn of cards.
+@Composable
+fun ListScreen() {
+ MaterialTheme {
+ Scaffold(topBar = { TopAppBar(title = { Text("Compose list") }) }) { padding ->
+ LazyColumn(Modifier.fillMaxSize().padding(padding)) {
+ items(30) { index ->
+ Card(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp)) {
+ Column(Modifier.padding(12.dp)) {
+ Text("Item $index", style = MaterialTheme.typography.titleMedium)
+ Text("Supporting text for row $index")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/// State and recomposition: a counter bumped from an effect, and an
+/// AnimatedVisibility captured at its end state.
+@Composable
+fun StateScreen() {
+ MaterialTheme {
+ var count by remember { mutableIntStateOf(0) }
+ var shown by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ count = 3
+ shown = true
+ }
+ Column(Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text("Clicked $count times", style = MaterialTheme.typography.headlineSmall)
+ Button(onClick = { count++ }) { Text("Click me") }
+ AnimatedVisibility(visible = shown) {
+ Card(Modifier.fillMaxWidth()) {
+ Text("Revealed by AnimatedVisibility", Modifier.padding(16.dp))
+ }
+ }
+ AnimatedVisibility(visible = !shown) {
+ Text("This text is hidden at the end state")
+ }
+ }
+ }
+}
diff --git a/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSessionCard.kt b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSessionCard.kt
new file mode 100644
index 00000000000..109c595d5e7
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSessionCard.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.kmp
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+// The developer guide shows this card beside its Codename One twin,
+// docs/demos/.../kmp/SessionCard.java; keep the two doing the same thing.
+// tag::kotlin-multiplatform-interop-session-card-compose[]
+@Composable
+fun SessionCard(time: String, title: String, speaker: String,
+ favorite: Boolean, onFavorite: () -> Unit) {
+ Card(Modifier.fillMaxWidth()) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Column(Modifier.weight(1f).padding(12.dp)) {
+ Text(time, style = MaterialTheme.typography.labelMedium)
+ Text(title, style = MaterialTheme.typography.titleMedium)
+ Text(speaker, style = MaterialTheme.typography.bodySmall)
+ }
+ IconButton(onFavorite) {
+ Icon(if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, null)
+ }
+ }
+ }
+}
+// end::kotlin-multiplatform-interop-session-card-compose[]
diff --git a/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSnippets.kt b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSnippets.kt
new file mode 100644
index 00000000000..6a9d58d0abf
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/GuideSnippets.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.kmp
+
+import com.codename1.compose.ComposeForm
+import com.codename1.compose.ComposeView
+import com.codename1.system.Lifecycle
+import com.codename1.ui.Form
+import com.codename1.ui.Label
+import com.codename1.ui.layouts.BorderLayout
+
+// The developer guide's entry-point examples (Kotlin-Multiplatform-Interop),
+// kept here so they compile with the rest of the Compose screens. Nothing
+// calls them.
+
+// tag::kotlin-multiplatform-interop-kotlin-002[]
+class MyApp : Lifecycle() {
+ override fun runApp() {
+ ComposeForm { LayoutScreen() }.show()
+ }
+}
+
+// end::kotlin-multiplatform-interop-kotlin-002[]
+internal fun guideMixedForm() {
+ // tag::kotlin-multiplatform-interop-kotlin-003[]
+ val form = Form("Mixed", BorderLayout())
+ form.add(BorderLayout.NORTH, Label("Codename One"))
+ form.add(BorderLayout.CENTER, ComposeView { ListScreen() })
+ form.show()
+ // end::kotlin-multiplatform-interop-kotlin-003[]
+}
diff --git a/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/KmpScenes.kt b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/KmpScenes.kt
new file mode 100644
index 00000000000..ff40f45cedd
--- /dev/null
+++ b/scripts/hellocodenameone/common/src/main/kmp/jvmMain/kotlin/com/codenameone/examples/hellocodenameone/kmp/KmpScenes.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codenameone.examples.hellocodenameone.kmp
+
+import com.codename1.compose.ComposeView
+import com.codename1.ui.Component
+
+/// The Compose screens as Codename One components, for the KmpCompat
+/// screenshot tests, which are Java.
+object KmpScenes {
+ @JvmStatic fun layout(): Component = ComposeView { LayoutScreen() }
+ @JvmStatic fun typography(): Component = ComposeView { TypographyScreen() }
+ @JvmStatic fun list(): Component = ComposeView { ListScreen() }
+ @JvmStatic fun state(): Component = ComposeView { StateScreen() }
+}
diff --git a/scripts/hellocodenameone/pom.xml b/scripts/hellocodenameone/pom.xml
index 77cc3585098..8910a17c372 100644
--- a/scripts/hellocodenameone/pom.xml
+++ b/scripts/hellocodenameone/pom.xml
@@ -31,6 +31,8 @@
171717
+
+ 23.0.0hellocodenameone
@@ -70,6 +72,13 @@
${cn1.version}provided
+
+
+ com.codenameone
+ codenameone-compose-compat
+ ${cn1.version}
+ com.codenameonecodenameone-unity-compat
diff --git a/scripts/javascript/screenshots/KmpCompatLayout.png b/scripts/javascript/screenshots/KmpCompatLayout.png
new file mode 100644
index 00000000000..88262c0d2d2
Binary files /dev/null and b/scripts/javascript/screenshots/KmpCompatLayout.png differ
diff --git a/scripts/javascript/screenshots/KmpCompatList.png b/scripts/javascript/screenshots/KmpCompatList.png
new file mode 100644
index 00000000000..31960f0f3da
Binary files /dev/null and b/scripts/javascript/screenshots/KmpCompatList.png differ
diff --git a/scripts/javascript/screenshots/KmpCompatState.png b/scripts/javascript/screenshots/KmpCompatState.png
new file mode 100644
index 00000000000..21609c8c0e9
Binary files /dev/null and b/scripts/javascript/screenshots/KmpCompatState.png differ
diff --git a/scripts/javascript/screenshots/KmpCompatTypography.png b/scripts/javascript/screenshots/KmpCompatTypography.png
new file mode 100644
index 00000000000..e5a78b09639
Binary files /dev/null and b/scripts/javascript/screenshots/KmpCompatTypography.png differ
diff --git a/scripts/kmp-bench/README.md b/scripts/kmp-bench/README.md
new file mode 100644
index 00000000000..c209b26dacd
--- /dev/null
+++ b/scripts/kmp-bench/README.md
@@ -0,0 +1,155 @@
+# Codename One vs. Compose Multiplatform
+
+One Compose application, **Keynote** (a conference schedule: search, day
+filters, a 150-session list of cards, favorites, a detail screen and an animated
+chart), built twice from the **same `commonMain` source** and run side by side:
+
+- **Codename One**: the source imported into an archetype application
+ (`common/src/main/kmp`), compiled by kotlinc with the Compose compiler against
+ `maven/compose-compat`, translated by ParparVM to C, built by Xcode.
+- **Compose Multiplatform**: the same source as a CMP module
+ (`org.jetbrains.compose`, Kotlin/Native and Skia), linked as an iOS
+ executable.
+
+The shared code is in `app/shared/commonMain`. The only per-side files are each
+side's entry point (`app/cn1/BenchEntry.kt`, `app/cmp/composeApp/src/iosMain`).
+The CMP build adds one compiler opt-in, `ExperimentalMaterial3Api`, which the
+real material3 demands for the top app bar. That keeps the shared source
+identical on both sides. Keynote is written for this benchmark. It is not a
+JetBrains sample.
+
+## Running it
+
+```bash
+export JAVA17_HOME=... # JDK 17: Gradle, kotlinc, the CN1 translation
+scripts/kmp-bench/run_bench.sh --work /tmp/kbench --runs 5 \
+ --maven-repo "$PWD/.m2-repo" # the repository the checkout installed into
+```
+
+The script needs Xcode, Gradle 8.5 or newer on `PATH`, and the reactor installed
+into the repository it is given. It boots one iPhone simulator, shuts it down
+when it finishes, and writes `results.json`, `results.md` and a screenshot of
+each app into the work directory.
+
+## What is measured
+
+| | |
+|---|---|
+| Start-up | From `simctl launch` to the app's `KMPBENCH first-frame` line. Both print it on the first main-thread turn after their UI is installed. CMP's marker fires before Skia has drawn anything, so the comparison favours CMP. Run 1 is the first launch after install (cold); the median of the rest is reported as warm. |
+| Executable | The app binary after `strip`, which is what an App Store build ships. |
+| Bundle | A copy of the `.app` with its executable stripped, plus the bytes of everything in it except the executable. |
+| Recomposition | `KMPBENCH_PERF=1` launches only (`Perf.kt`, shared code). After 30 discarded changes, the day filter is changed 300 times. Each change is timed until a 1 dp marker drawn after the list has been painted, so compose, layout and draw are all included. |
+| Scroll | After one discarded pass, all 150 cards (three days of 50) are scrolled one card per frame with `scrollToItem`. compose-compat has no `scrollBy`. Work is the time from the scroll call to the marker drawn. Interval is the time between consecutive markers. Jank is frames longer than twice the median interval. |
+| Memory | Physical footprint (`footprint `) five seconds after launch: the dirty memory the process owns, which is what iOS charges an app and kills it for. Resident size is not used: on the simulator it counts the shared dyld cache, which every process maps, so an empty app already shows 184 MB. |
+
+## Results
+
+iPhone 17 Pro simulator, Xcode 27, Apple silicon, Release builds, 5 runs.
+Start-up here is from process start (the kernel's start time) to the marker, so
+`simctl`'s own overhead is left out of both.
+
+| | Codename One, Compose | Codename One, Java | Compose Multiplatform |
+|---|---|---|---|
+| Cold start | 961 ms | 805 ms | 1008 ms |
+| Warm start (median) | 580 ms | 572 ms | 604 ms |
+| Executable, stripped | 16.8 MB | 8.9 MB | 22.9 MB |
+| App bundle, executable stripped | 18.3 MB | 10.2 MB | 22.9 MB |
+| Bundle without executable | 1.3 MB | 1.3 MB | 2.5 KB |
+| Physical footprint | 47.0 MB | 47.0 MB | 48.0 MB |
+| Recomposition, day change to drawn (median) | 4.58 ms | 1.49 ms | 19.24 ms |
+| Recomposition, day change to drawn (p95) | 6.17 ms | 2.01 ms | 23.02 ms |
+| Scroll, work per frame (median) | 4.04 ms | 1.76 ms | 17.03 ms |
+| Scroll, work per frame (p95) | 6.60 ms | 2.68 ms | 18.21 ms |
+| Scroll, frame interval (median) | 35.45 ms | 34.79 ms | 33.35 ms |
+| Scroll, frame interval (p95) | 38.12 ms | 37.94 ms | 34.74 ms |
+| Scroll, frames over 2x median | 0 of 144 | 0 of 144 | 0 of 144 |
+
+Runtime values are the median over 5 launches of each launch's median or p95.
+An empty UIKit app measured 640-750 ms start-up and a 34 MB footprint on the
+same simulator.
+
+**The bundles differ mostly in resources.** The CMP bundle holds only the
+executable, `Info.plist` and its signature, because Skia and the fonts it uses
+are linked in. The CN1 bundle holds 1.3 MB besides its executable:
+`Assets.car` (570 KB), `material-design-font.ttf` (357 KB),
+`iOSModernTheme.res` (213 KB), `default.metallib` (92 KB) and
+`kotlin.kotlin_builtins` (29 KB).
+
+**Compose Multiplatform is the reference point, and a good one.** It is the
+closest thing to Codename One's own model -- one codebase, one UI, shared
+logic -- and it keeps pace with the display on every step here. The question
+is how much CPU each runtime spends to get there.
+
+**Codename One runs the same Compose code in a quarter of the time.** A day
+change takes 4.6 ms from state change to drawn on Codename One and 19.2 ms on
+CMP; one card of scrolling takes 4.0 ms against 17.0 ms. Every side then waits
+for the display: `scrollToItem` is issued once per drawn frame, so the frame
+interval is about two display frames on each, and none dropped a frame against
+its own median. These are simulator numbers on an Apple-silicon Mac. They show
+where each runtime spends CPU in identical code. They do not show device frame
+rates, GPU cost, or how a finger-driven fling feels.
+
+**The Java column is the same app without Kotlin, and it is faster again.**
+`app/java/JavaBench.java` is the Keynote screens written against Codename One
+directly -- the same programme generator, the same cards, the same steps timed
+the same way, a 1 px marker painted after the list -- so it ships no Kotlin
+standard library, no Compose runtime and no coroutines. The list is an
+`InfiniteContainer`: cards are made a batch at a time as the scroll nears the
+end, as `LazyColumn` composes only what comes into view, and a card once made
+is rebound rather than rebuilt. A day change is 1.5 ms, thirteen times faster
+than CMP; a scroll step is 1.8 ms, nearly ten times faster; the executable is
+8.9 MB, 39% of CMP's; memory is level with both.
+
+The time gap is mostly the cost of the reactive model. A composition records
+what it read, diffs what it emits against its slot table and hands the changes
+to an applier. An imperative update sets three labels on a card that already
+exists. The size gap is Kotlin: the Java build carries neither the Kotlin
+standard library nor the Compose runtime. The Java card also reads as plainly: the components are made
+once, `bind` fills them, and a test makes a card and presses its button
+without a test rule or semantics tree. The developer guide's Kotlin
+Multiplatform chapter shows the card both ways and a test for it.
+
+Building it found a framework bug every Codename One app on a notched device
+paid: a Form with a Toolbar laid itself out again on every EDT cycle, forever,
+because the safe-area padding it sets for one measurement announced a style
+change that queued another layout. The idle app burned 14-26% CPU and 10-20 MB
+of garbage. `Container.snapToSafeAreaInternal` now sets that padding silently.
+
+The first version of compose-compat took 42 ms per day change, and almost none
+of it was work: the EDT sat idle for 83% of it. A state change waited for a
+dispatched resumption, then for the snapshot to apply, then for frame
+callbacks sent after paint -- three EDT cycles. Frames now run at the start of
+the layout pass, as Android's Choreographer runs them before traversal, and
+the recomposer resumes inline on the EDT, so change, layout and paint share
+one cycle.
+
+**Start-up is the simulator's floor.** An empty UIKit application, with no
+code of its own, takes as long to show its first frame as either side, so
+neither has much start-up of its own left to lose there. Measure on a device
+before quoting a difference.
+
+The Codename One executable includes the whole iOS port and the full Material
+icon font, so the 6.1 MB difference is a floor.
+
+**Memory is level**, 47 MB on both Codename One builds against 48 MB. The Java heap holds
+3.2 MB live after start-up, in about 57,000 objects. Loading the theme used to
+leave 24,000 strings behind -- one per key and value, plus every colour and
+padding built by concatenation, and a cache key per `Font.derive` call. The
+theme loader now decodes into reused buffers and shares repeated values
+(`ThemeStrings`), and a derived font is found without building a key, which
+took 0.6 MB of live heap and about 2 MB of footprint off. Freed pages stay
+resident on iOS, so collecting sooner does not help: garbage that is never
+created is the only saving. Most of what remains is per thread (two
+`Dispatchers.Default` workers that `flowOn` starts) and text rendering (the
+glyph atlas and CoreText).
+
+Codename One also targets platforms CMP does not: watchOS, tvOS, and native
+Windows and Linux binaries with no JVM. Keynote has not been built for those
+here yet.
+
+## Not yet covered
+
+- `LazyColumn` and `LazyRow` with `reverseLayout = true` are refused at run
+ time. The lists compose only the items in view.
+- Scrolling is measured as programmatic `scrollToItem` steps on the simulator,
+ not a touch fling on a device.
diff --git a/scripts/kmp-bench/app/cmp/Info.plist b/scripts/kmp-bench/app/cmp/Info.plist
new file mode 100644
index 00000000000..8b860c21dbd
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/Info.plist
@@ -0,0 +1,14 @@
+
+
+
+CFBundleExecutableKeynote
+CFBundleIdentifiercom.example.kmpbench.cmp
+CFBundleNameKeynote
+CFBundlePackageTypeAPPL
+CFBundleShortVersionString1.0
+CFBundleVersion1
+MinimumOSVersion15.0
+UIDeviceFamily1
+UILaunchScreen
+CADisableMinimumFrameDurationOnPhone
+
diff --git a/scripts/kmp-bench/app/cmp/build.gradle.kts b/scripts/kmp-bench/app/cmp/build.gradle.kts
new file mode 100644
index 00000000000..a1e59554fcf
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/build.gradle.kts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+plugins {
+ kotlin("multiplatform") version "2.2.10" apply false
+ id("org.jetbrains.kotlin.plugin.compose") version "2.2.10" apply false
+ id("org.jetbrains.compose") version "1.9.0" apply false
+}
diff --git a/scripts/kmp-bench/app/cmp/composeApp/build.gradle.kts b/scripts/kmp-bench/app/cmp/composeApp/build.gradle.kts
new file mode 100644
index 00000000000..83278771e26
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/composeApp/build.gradle.kts
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+// The Compose Multiplatform build of the same commonMain the Codename One
+// project compiles. Release builds only: the benchmark measures what ships.
+import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
+
+plugins {
+ kotlin("multiplatform")
+ id("org.jetbrains.kotlin.plugin.compose")
+ id("org.jetbrains.compose")
+}
+
+kotlin {
+ jvm("desktop")
+ listOf(iosSimulatorArm64(), iosArm64()).forEach { target ->
+ target.binaries.executable(listOf(NativeBuildType.RELEASE)) {
+ entryPoint = "keynote.ios.main"
+ }
+ }
+ // The shared code is unchanged from the Codename One side, which needs no
+ // opt-in for the material3 top bar.
+ compilerOptions {
+ optIn.add("androidx.compose.material3.ExperimentalMaterial3Api")
+ }
+ sourceSets {
+ commonMain.dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.materialIconsExtended)
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
+ }
+ val desktopMain by getting {
+ dependencies {
+ implementation(compose.desktop.currentOs)
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.10.2")
+ }
+ }
+ }
+}
+
+compose.desktop {
+ application {
+ mainClass = "keynote.desktop.MainKt"
+ }
+}
diff --git a/scripts/kmp-bench/app/cmp/composeApp/src/desktopMain/kotlin/Main.kt b/scripts/kmp-bench/app/cmp/composeApp/src/desktopMain/kotlin/Main.kt
new file mode 100644
index 00000000000..e61e93ebf84
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/composeApp/src/desktopMain/kotlin/Main.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package keynote.desktop
+
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import keynote.App
+
+fun main() = application {
+ Window(onCloseRequest = ::exitApplication, title = "Keynote",
+ state = rememberWindowState(width = 412.dp, height = 892.dp)) {
+ App()
+ }
+}
diff --git a/scripts/kmp-bench/app/cmp/composeApp/src/iosMain/kotlin/Main.kt b/scripts/kmp-bench/app/cmp/composeApp/src/iosMain/kotlin/Main.kt
new file mode 100644
index 00000000000..684342717ff
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/composeApp/src/iosMain/kotlin/Main.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package keynote.ios
+
+import androidx.compose.ui.window.ComposeUIViewController
+import keynote.App
+import kotlinx.cinterop.BetaInteropApi
+import kotlinx.cinterop.ExperimentalForeignApi
+import platform.Foundation.NSDate
+import platform.Foundation.NSStringFromClass
+import platform.Foundation.timeIntervalSince1970
+import platform.UIKit.UIApplication
+import platform.UIKit.UIApplicationDelegateProtocol
+import platform.UIKit.UIApplicationDelegateProtocolMeta
+import platform.UIKit.UIApplicationMain
+import platform.UIKit.UIResponder
+import platform.UIKit.UIResponderMeta
+import platform.UIKit.UIScreen
+import platform.UIKit.UIWindow
+import platform.darwin.dispatch_async
+import platform.darwin.dispatch_get_main_queue
+import kotlinx.cinterop.autoreleasepool
+import kotlinx.cinterop.cstr
+import kotlinx.cinterop.memScoped
+import kotlinx.cinterop.toCValues
+import kotlinx.cinterop.toKString
+import platform.posix.getenv
+
+/// The whole iOS application: a delegate whose window shows the composition.
+/// An executable rather than a framework inside an Xcode project, so the build
+/// needs no project file; the .app is assembled by build_apps.sh.
+@OptIn(BetaInteropApi::class, ExperimentalForeignApi::class)
+class AppDelegate @OverrideInit constructor() : UIResponder(), UIApplicationDelegateProtocol {
+ companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta
+
+ private var mainWindow: UIWindow? = null
+
+ override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map?): Boolean {
+ // run_bench.sh launches with SIMCTL_CHILD_KMPBENCH_PERF=1 for the runtime measurements.
+ val perf = getenv("KMPBENCH_PERF")?.toKString() == "1"
+ val w = UIWindow(frame = UIScreen.mainScreen.bounds)
+ w.rootViewController = ComposeUIViewController { App(perf = perf) }
+ w.makeKeyAndVisible()
+ mainWindow = w
+ // After the launch method returns, as the Codename One side prints its
+ // marker from callSerially once its form is shown.
+ dispatch_async(dispatch_get_main_queue()) {
+ println("KMPBENCH first-frame " + (NSDate().timeIntervalSince1970 * 1000.0).toLong())
+ }
+ return true
+ }
+}
+
+@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+fun main(args: Array) {
+ memScoped {
+ val argv = (arrayOf("keynote") + args).map { it.cstr.ptr }.toCValues()
+ autoreleasepool {
+ UIApplicationMain(args.size + 1, argv, null, NSStringFromClass(AppDelegate))
+ }
+ }
+}
diff --git a/scripts/kmp-bench/app/cmp/gradle.properties b/scripts/kmp-bench/app/cmp/gradle.properties
new file mode 100644
index 00000000000..e36d638b6e3
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/gradle.properties
@@ -0,0 +1,3 @@
+kotlin.code.style=official
+org.gradle.jvmargs=-Xmx3g
+kotlin.native.ignoreDisabledTargets=true
diff --git a/scripts/kmp-bench/app/cmp/settings.gradle.kts b/scripts/kmp-bench/app/cmp/settings.gradle.kts
new file mode 100644
index 00000000000..cd383448492
--- /dev/null
+++ b/scripts/kmp-bench/app/cmp/settings.gradle.kts
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+rootProject.name = "keynote-cmp"
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ google()
+ }
+}
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ google()
+ }
+}
+include(":composeApp")
diff --git a/scripts/kmp-bench/app/cn1/Bench.java b/scripts/kmp-bench/app/cn1/Bench.java
new file mode 100644
index 00000000000..810cc3b0c9e
--- /dev/null
+++ b/scripts/kmp-bench/app/cn1/Bench.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.example.kmpbench;
+
+import com.codename1.system.Lifecycle;
+
+/**
+ * Boots the Keynote hero application, the same Compose source the Compose
+ * Multiplatform side builds. All the work is in BenchEntry.kt: Compose content
+ * has to be created from Kotlin, where the compiler plugin makes the lambda
+ * composable.
+ */
+public class Bench extends Lifecycle {
+ @Override
+ public void runApp() {
+ BenchEntryKt.start();
+ }
+}
diff --git a/scripts/kmp-bench/app/cn1/BenchEntry.kt b/scripts/kmp-bench/app/cn1/BenchEntry.kt
new file mode 100644
index 00000000000..fccf31403b8
--- /dev/null
+++ b/scripts/kmp-bench/app/cn1/BenchEntry.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.example.kmpbench
+
+import com.codename1.compose.ComposeForm
+import com.codename1.ui.CN
+import keynote.App
+
+/// Shows the application and prints the marker the harness times start-up
+/// against, once the first frame with content has been painted.
+fun start() {
+ // run_bench.sh launches with SIMCTL_CHILD_KMPBENCH_PERF=1 for the runtime measurements.
+ val perf = System.getenv("KMPBENCH_PERF") == "1"
+ val form = ComposeForm { App(perf = perf) }
+ form.show()
+ CN.callSerially {
+ println("KMPBENCH first-frame " + System.currentTimeMillis())
+ }
+}
diff --git a/scripts/kmp-bench/app/java/JavaBench.java b/scripts/kmp-bench/app/java/JavaBench.java
new file mode 100644
index 00000000000..5436a4d851f
--- /dev/null
+++ b/scripts/kmp-bench/app/java/JavaBench.java
@@ -0,0 +1,771 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.example.javabench;
+
+import com.codename1.system.Lifecycle;
+import com.codename1.ui.Button;
+import com.codename1.ui.CN;
+import com.codename1.ui.Component;
+import com.codename1.ui.Container;
+import com.codename1.ui.Font;
+import com.codename1.ui.FontImage;
+import com.codename1.ui.Form;
+import com.codename1.ui.Graphics;
+import com.codename1.ui.InfiniteContainer;
+import com.codename1.ui.Label;
+import com.codename1.ui.Tabs;
+import com.codename1.ui.TextField;
+import com.codename1.ui.Toolbar;
+import com.codename1.ui.animations.Animation;
+import com.codename1.ui.animations.Motion;
+import com.codename1.ui.geom.Dimension;
+import com.codename1.ui.layouts.BorderLayout;
+import com.codename1.ui.layouts.BoxLayout;
+import com.codename1.ui.layouts.FlowLayout;
+import com.codename1.ui.plaf.Border;
+import com.codename1.ui.plaf.RoundRectBorder;
+import com.codename1.ui.plaf.Style;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Keynote application written directly against Codename One: the same
+ * screens, data and benchmark steps as the Compose source in app/shared, with
+ * no Kotlin, no Compose runtime and no coroutines in the binary. run_bench.sh
+ * builds it as the third side of the comparison.
+ */
+public class JavaBench extends Lifecycle {
+ static final int SESSIONS_PER_DAY = 50;
+
+ final List sessions = Programme.generate();
+ final List favorites = new ArrayList();
+ int day = 1;
+ String query = "";
+
+ Form form;
+ Tabs tabs;
+ SessionList schedule;
+ SessionList favoriteList;
+ Insights insights;
+ Marker marker;
+ Container stepHost;
+ Perf perf;
+
+ @Override
+ public void runApp() {
+ form = new Form("KotlinConf-ish 2026", new BorderLayout());
+ form.getToolbar().setTitleCentered(true);
+ // Material's top app bar title: 22 sp, where the iOS theme sets a large title.
+ form.getToolbar().getTitleComponent().getAllStyles().setFont(font("native:MainRegular", 22));
+ tabs = new Tabs(Component.BOTTOM);
+ tabs.setSwipeActivated(false);
+ tabs.addTab("Schedule", FontImage.MATERIAL_DATE_RANGE, 4, scheduleTab());
+ favoriteList = new SessionList(this);
+ tabs.addTab("Favorites", FontImage.MATERIAL_FAVORITE, 4, favoriteList);
+ insights = new Insights(this);
+ tabs.addTab("Insights", FontImage.MATERIAL_INFO, 4, insights);
+ tabs.addSelectionListener((old, now) -> {
+ form.setTitle(now == 2 ? "Insights" : "KotlinConf-ish 2026");
+ if (now == 1) {
+ favoriteList.show(favoriteSessions(), "Tap the heart on a session to keep it here.");
+ } else if (now == 2) {
+ insights.open();
+ }
+ });
+ form.add(BorderLayout.CENTER, tabs);
+ form.show();
+ // A Form focuses its first focusable component; the search field's
+ // blinking caret would then repaint the transparent field, and with it
+ // the whole tab, every half second. Compose does not focus it either.
+ form.setFocused(null);
+ CN.callSerially(() -> System.out.println("KMPBENCH first-frame " + System.currentTimeMillis()));
+ if ("1".equals(System.getenv("KMPBENCH_PERF"))) {
+ perf = new Perf(this);
+ perf.start();
+ }
+ }
+
+ private Container scheduleTab() {
+ TextField search = new TextField("", "Search talks and speakers", 20, TextField.ANY);
+ search.setHint("Search talks and speakers", FontImage.createMaterial(FontImage.MATERIAL_SEARCH, search.getHintLabel().getUnselectedStyle()));
+ search.getAllStyles().setBorder(RoundRectBorder.create().cornerRadius(5f).strokeColor(0x79747e).strokeOpacity(255).stroke(1, false));
+ search.getAllStyles().setBgTransparency(0);
+ search.getAllStyles().setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ search.getAllStyles().setPadding(0.6f, 0.6f, 2.5f, 2.5f);
+ search.getAllStyles().setMarginUnit(Style.UNIT_TYPE_DIPS);
+ search.getAllStyles().setMargin(1.3f, 1.3f, 2.5f, 2.5f);
+ // Material's outlined field is 56 dp; the iOS theme's field is taller.
+ search.setPreferredH(dp(56));
+ search.addDataChangedListener((type, index) -> {
+ query = search.getText().trim();
+ refreshSchedule();
+ });
+ Container days = new Container(new FlowLayout());
+ days.getAllStyles().setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ days.getAllStyles().setPadding(0, 0, 2.5f, 2.5f);
+ for (int d = 1; d <= 3; d++) {
+ days.add(new DayChip(this, d));
+ }
+ schedule = new SessionList(this);
+ marker = new Marker();
+ Container top = BoxLayout.encloseY(search, days);
+ Container tab = new Container(new BorderLayout()) {
+ @Override
+ public void layoutContainer() {
+ if (perf != null) {
+ perf.layoutStep();
+ }
+ super.layoutContainer();
+ }
+ };
+ stepHost = tab;
+ tab.add(BorderLayout.NORTH, top);
+ tab.add(BorderLayout.CENTER, schedule);
+ tab.add(BorderLayout.SOUTH, marker);
+ refreshSchedule();
+ return tab;
+ }
+
+ void setDay(int d) {
+ day = d;
+ refreshSchedule();
+ schedule.getParent().getComponentAt(0).repaint();
+ }
+
+ void refreshSchedule() {
+ List visible = new ArrayList();
+ String q = query.toLowerCase();
+ for (Session s : sessions) {
+ if (s.day == day && (q.length() == 0 || s.title.toLowerCase().indexOf(q) >= 0
+ || s.speaker.toLowerCase().indexOf(q) >= 0)) {
+ visible.add(s);
+ }
+ }
+ schedule.show(visible, "No session matches \"" + query + "\".");
+ }
+
+ List favoriteSessions() {
+ List out = new ArrayList();
+ for (Session s : sessions) {
+ if (favorites.contains(Integer.valueOf(s.id))) {
+ out.add(s);
+ }
+ }
+ return out;
+ }
+
+ void toggleFavorite(Session s) {
+ Integer id = Integer.valueOf(s.id);
+ if (!favorites.remove(id)) {
+ favorites.add(id);
+ }
+ }
+
+ static int dp(float v) {
+ return CN.convertToPixels(v * 0.15875f);
+ }
+
+ private static final java.util.Map FONTS = new java.util.HashMap();
+
+ /** One native font per face and size, shared by every card. */
+ static Font font(String face, float sp) {
+ String key = face + sp;
+ Font f = FONTS.get(key);
+ if (f == null) {
+ f = Font.createTrueTypeFont(face, sp * 0.15875f);
+ FONTS.put(key, f);
+ }
+ return f;
+ }
+
+ static boolean dark() {
+ Boolean d = CN.isDarkMode();
+ return d != null && d.booleanValue();
+ }
+
+ // --- data ---------------------------------------------------------------
+
+ static final String[] TRACK_LABELS = {"Language", "Multiplatform", "Server", "Tooling", "UI"};
+ static final int[] TRACK_COLORS = {0x7F52FF, 0x0095D5, 0x2E7D32, 0xE65100, 0xC2185B};
+
+ static final class Session {
+ int id;
+ String title;
+ String speaker;
+ String company;
+ int track;
+ int day;
+ int start;
+ int length;
+ String room;
+ String description;
+
+ String timeLabel() {
+ return clock(start) + " - " + clock(start + length);
+ }
+ }
+
+ static String clock(int minutes) {
+ int h = minutes / 60;
+ int m = minutes % 60;
+ return (h < 10 ? "0" : "") + h + ":" + (m < 10 ? "0" : "") + m;
+ }
+
+ /** The same generator, seed and order as Data.kt, so both sides show the same 150 sessions. */
+ static final class Programme {
+ static final String[] FIRST = {"Ada", "Linus", "Grace", "Alan", "Barbara", "Ken", "Margaret", "Dennis", "Frances", "Edsger",
+ "Radia", "Bjarne", "Sophie", "Guido", "Hedy", "James"};
+ static final String[] LAST = {"Lovelace", "Hopper", "Turing", "Liskov", "Thompson", "Hamilton", "Ritchie", "Allen",
+ "Dijkstra", "Perlman", "Wilson", "Lamarr", "Gosling", "Kay", "Knuth", "Backus"};
+ static final String[] COMPANIES = {"JetBrains", "Google", "Square", "Netflix", "Touchlab", "Cash App", "Shopify", "Zalando"};
+ static final String[] TOPICS = {"Coroutines", "Flows", "Compose", "K2", "Ktor", "Gradle", "Serialization", "Wasm",
+ "Native interop", "Context parameters", "Value classes", "Multiplatform libraries", "Testing", "Performance",
+ "Accessibility", "Animations", "State hoisting", "Server-side Kotlin", "Build speed", "Code sharing"};
+ static final String[] SHAPES = {"Deep dive into %s", "%s in production", "What's new in %s", "%s: lessons learned",
+ "Mastering %s", "%s from scratch", "The hidden cost of %s", "%s at scale"};
+ static final String[] ROOMS = {"Auditorium", "Room 1", "Room 2", "Workshop A", "Lightning stage"};
+
+ private static long seed;
+
+ private static int next(int n) {
+ seed = seed * 6364136223846793005L + 1442695040888963407L;
+ return (int) ((seed >>> 33) % n);
+ }
+
+ static List generate() {
+ seed = 20261025L;
+ List out = new ArrayList();
+ int id = 0;
+ for (int day = 1; day <= 3; day++) {
+ int start = 9 * 60;
+ int count = 0;
+ while (count < SESSIONS_PER_DAY) {
+ for (String room : ROOMS) {
+ if (count >= SESSIONS_PER_DAY) {
+ break;
+ }
+ String topic = TOPICS[next(TOPICS.length)];
+ String title = replace(SHAPES[next(SHAPES.length)], topic);
+ String speaker = FIRST[next(FIRST.length)] + " " + LAST[next(LAST.length)];
+ String company = COMPANIES[next(COMPANIES.length)];
+ Session s = new Session();
+ s.id = id++;
+ s.title = title;
+ s.speaker = speaker;
+ s.company = company;
+ s.track = next(TRACK_LABELS.length);
+ s.day = day;
+ s.start = start;
+ s.length = "Lightning stage".equals(room) ? 15 : room.startsWith("Workshop") ? 90 : 45;
+ s.room = room;
+ s.description = title + ". " + speaker + " of " + company + " walks through " + topic
+ + ": the model behind it, the mistakes teams make with it, and the patterns that hold up in real applications. "
+ + "Bring questions; the last ten minutes are open Q&A.";
+ out.add(s);
+ count++;
+ }
+ start += 60;
+ }
+ }
+ return out;
+ }
+
+ private static String replace(String shape, String topic) {
+ int i = shape.indexOf("%s");
+ return shape.substring(0, i) + topic + shape.substring(i + 2);
+ }
+ }
+
+ // --- components ---------------------------------------------------------
+
+ /** A Material filter chip: outlined, filled when selected. */
+ static final class DayChip extends Button {
+ private final JavaBench app;
+ private final int day;
+
+ DayChip(JavaBench app, int day) {
+ super("Day " + day);
+ this.app = app;
+ this.day = day;
+ Style s = getAllStyles();
+ s.setFont(font("native:MainRegular", 14));
+ s.setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ s.setPadding(1, 1, 2.5f, 2.5f);
+ s.setMarginUnit(Style.UNIT_TYPE_DIPS);
+ s.setMargin(0, 0, 0, 1.3f);
+ s.setBorder(Border.createEmpty());
+ s.setBgTransparency(0);
+ s.setFgColor(dark() ? 0xE6E1E5 : 0x1D1B20);
+ addActionListener(e -> app.setDay(day));
+ }
+
+ @Override
+ public void paintBackground(Graphics g) {
+ boolean on = app.day == day;
+ int r = dp(8);
+ g.setAntiAliased(true);
+ if (on) {
+ g.setColor(dark() ? 0x3A2D6B : 0xE9E1FF);
+ g.fillRoundRect(getX(), getY(), getWidth(), getHeight(), r * 2, r * 2);
+ } else {
+ g.setColor(0x79747E);
+ g.drawRoundRect(getX(), getY(), getWidth() - 1, getHeight() - 1, r * 2, r * 2);
+ }
+ }
+ }
+
+ /**
+ * The scrolling list of cards: an InfiniteContainer, so cards are made a
+ * batch at a time as the scroll nears the end, as LazyColumn composes only
+ * what comes into view. A card, once made, is kept in a pool and rebound
+ * when the day or the query changes, as a lazy list reuses its item nodes.
+ */
+ static final class SessionList extends InfiniteContainer {
+ private static final int BATCH = 8;
+ private final JavaBench app;
+ private final List pool = new ArrayList();
+ private final Label empty = new Label();
+ private List items = new ArrayList();
+
+ SessionList(JavaBench app) {
+ super(BATCH);
+ this.app = app;
+ getAllStyles().setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ getAllStyles().setPadding(1.3f, 1.3f, 2.5f, 2.5f);
+ empty.getAllStyles().setAlignment(Component.CENTER);
+ empty.getAllStyles().setFgColor(0x49454F);
+ }
+
+ @Override
+ public Component[] fetchComponents(int index, int amount) {
+ if (items.isEmpty()) {
+ return index == 0 ? new Component[] {empty} : new Component[0];
+ }
+ int n = Math.max(0, Math.min(amount, items.size() - index));
+ Component[] out = new Component[n];
+ for (int i = 0; i < n; i++) {
+ while (pool.size() <= index + i) {
+ pool.add(new Card(app));
+ }
+ Card c = pool.get(index + i);
+ c.bind(items.get(index + i));
+ out[i] = c;
+ }
+ return out;
+ }
+
+ void show(List list, String emptyText) {
+ items = list;
+ empty.setText(emptyText);
+ refresh();
+ }
+
+ /** The list's scrollToItem: card i at the top, made first if needed. */
+ void scrollToCard(int i) {
+ int y = 0;
+ if (i < pool.size() && pool.get(i).getParent() == this) {
+ y = pool.get(i).getY() - getStyle().getPaddingTop();
+ } else {
+ y = Integer.MAX_VALUE;
+ }
+ int max = Math.max(0, getScrollDimension().getHeight() - getHeight());
+ setScrollY(Math.max(0, Math.min(y, max)));
+ }
+ }
+
+ /** One session: track stripe, time and room, title, speaker, favorite heart. */
+ static final class Card extends Container {
+ private final JavaBench app;
+ private final Label time = new Label();
+ private final Label title = new Label();
+ private final Label speaker = new Label();
+ private final Button heart = new Button();
+ private Session session;
+ private boolean shownFavorite;
+
+ Card(JavaBench app) {
+ super(new BorderLayout());
+ this.app = app;
+ Style s = getAllStyles();
+ s.setBgTransparency(0);
+ s.setBorder(Border.createEmpty());
+ s.setMarginUnit(Style.UNIT_TYPE_DIPS);
+ s.setMargin(0, 1.9f, 0, 0);
+ s.setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ s.setPadding(1.9f, 1.9f, 2.9f, 0.6f);
+ style(time, "native:MainRegular", 12, 0x49454F);
+ style(title, "native:MainRegular", 16, 0x1D1B20);
+ style(speaker, "native:MainRegular", 12, 0x49454F);
+ title.getAllStyles().setMarginUnit(Style.UNIT_TYPE_DIPS);
+ title.getAllStyles().setMargin(0.6f, 0.3f, 0, 0);
+ heart.getAllStyles().setBgTransparency(0);
+ heart.getAllStyles().setBorder(Border.createEmpty());
+ heart.addActionListener(e -> {
+ app.toggleFavorite(session);
+ updateHeart();
+ });
+ add(BorderLayout.CENTER, BoxLayout.encloseY(time, title, speaker));
+ add(BorderLayout.EAST, BorderLayout.centerAbsolute(heart));
+ addPointerReleasedListener(e -> {
+ if (session != null && !heart.contains(e.getX(), e.getY())) {
+ new Detail(app, session).show();
+ }
+ });
+ }
+
+ private static void style(Label l, String face, float sp, int color) {
+ Style s = l.getAllStyles();
+ s.setFont(font(face, sp));
+ s.setFgColor(color);
+ s.setPadding(0, 0, 0, 0);
+ s.setMargin(0, 0, 0, 0);
+ s.setBgTransparency(0);
+ l.setEndsWith3Points(true);
+ }
+
+ void bind(Session s) {
+ session = s;
+ time.setText(s.timeLabel() + " - " + s.room);
+ title.setText(s.title);
+ speaker.setText(s.speaker + ", " + s.company);
+ updateHeart();
+ }
+
+ private void updateHeart() {
+ boolean fav = app.favorites.contains(Integer.valueOf(session.id));
+ Style s = heart.getAllStyles();
+ s.setFgColor(fav ? 0xE91E63 : 0x49454F);
+ if (fav != shownFavorite || heart.getIcon() == null) {
+ shownFavorite = fav;
+ FontImage.setMaterialIcon(heart, fav ? FontImage.MATERIAL_FAVORITE : FontImage.MATERIAL_FAVORITE_BORDER, 3.8f);
+ }
+ }
+
+ @Override
+ public void paintBackground(Graphics g) {
+ // The rounded card in the track colour, then the card colour over
+ // all of it but a 6 dp stripe, whose inner edge stays straight.
+ int x = getX();
+ int y = getY();
+ int w = getWidth();
+ int h = getHeight();
+ int r = dp(12);
+ int stripe = dp(6);
+ g.setAntiAliased(true);
+ g.setColor(TRACK_COLORS[session == null ? 0 : session.track]);
+ g.fillRoundRect(x, y, w, h, r * 2, r * 2);
+ g.setColor(dark() ? 0x1D1B20 : 0xF7F2FA);
+ g.fillRect(x + stripe, y, w - stripe - r, h);
+ g.fillRoundRect(x + w - r * 2, y, r * 2, h, r * 2, r * 2);
+ }
+ }
+
+ /** Drawn after the list in every frame; the benchmark times a step until it paints. */
+ static final class Marker extends Component {
+ int seq;
+ int drawnSeq;
+ long drawnAt;
+
+ @Override
+ public void paint(Graphics g) {
+ drawnAt = System.nanoTime();
+ drawnSeq = seq;
+ }
+
+ @Override
+ protected Dimension calcPreferredSize() {
+ return new Dimension(1, 1);
+ }
+ }
+
+ /** Sessions per track for the selected day, as bars that grow in. */
+ static final class Insights extends Container {
+ private final JavaBench app;
+ private final Label caption = new Label();
+ private final Container legend = new Container(BoxLayout.y());
+ private final Chart chart = new Chart();
+
+ Insights(JavaBench app) {
+ super(BoxLayout.y());
+ this.app = app;
+ getAllStyles().setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ getAllStyles().setPadding(3.2f, 3.2f, 3.2f, 3.2f);
+ caption.getAllStyles().setFont(font("native:MainBold", 16));
+ add(caption);
+ add(chart);
+ add(legend);
+ }
+
+ void open() {
+ int[] counts = new int[TRACK_LABELS.length];
+ for (Session s : app.sessions) {
+ if (s.day == app.day) {
+ counts[s.track]++;
+ }
+ }
+ caption.setText("Sessions per track, day " + app.day);
+ legend.removeAll();
+ for (int t = 0; t < counts.length; t++) {
+ Label name = new Label(TRACK_LABELS[t]);
+ Label n = new Label(String.valueOf(counts[t]));
+ n.getAllStyles().setFont(font("native:MainBold", 14));
+ legend.add(BorderLayout.centerEastWest(name, n, null));
+ }
+ if (!app.favorites.isEmpty()) {
+ Label fav = new Label("You have " + app.favorites.size() + " favorite sessions.");
+ fav.getAllStyles().setFgColor(0x7F52FF);
+ legend.add(fav);
+ }
+ chart.start(counts);
+ revalidate();
+ }
+ }
+
+ static final class Chart extends Component implements Animation {
+ private int[] counts = new int[0];
+ private Motion grow;
+
+ void start(int[] c) {
+ counts = c;
+ grow = Motion.createEaseInOutMotion(0, 1000, 700);
+ grow.start();
+ getComponentForm().registerAnimated(this);
+ }
+
+ @Override
+ public boolean animate() {
+ if (grow != null && grow.isFinished()) {
+ getComponentForm().deregisterAnimated(this);
+ }
+ return true;
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ int max = 1;
+ for (int n : counts) {
+ max = Math.max(max, n);
+ }
+ int gap = dp(16);
+ int r = dp(8) * 2;
+ int w = counts.length == 0 ? 0 : (getWidth() - gap * (counts.length - 1)) / counts.length;
+ float f = grow == null ? 1 : grow.getValue() / 1000f;
+ g.setAntiAliased(true);
+ for (int i = 0; i < counts.length; i++) {
+ int x = getX() + i * (w + gap);
+ g.setColor(0xE6E0E9);
+ g.fillRoundRect(x, getY(), w, getHeight(), r, r);
+ int h = (int) (getHeight() * counts[i] / (float) max * f);
+ g.setColor(TRACK_COLORS[i]);
+ g.fillRoundRect(x, getY() + getHeight() - h, w, h, r, r);
+ }
+ }
+
+ @Override
+ protected Dimension calcPreferredSize() {
+ return new Dimension(CN.getDisplayWidth(), dp(220));
+ }
+ }
+
+ /** A session's own screen. */
+ static final class Detail extends Form {
+ Detail(JavaBench app, Session s) {
+ super(TRACK_LABELS[s.track], BoxLayout.y());
+ Form back = app.form;
+ getToolbar().setTitleCentered(true);
+ getToolbar().setBackCommand("", Toolbar.BackCommandPolicy.AS_ARROW, e -> back.showBack());
+ getContentPane().getAllStyles().setPaddingUnit(Style.UNIT_TYPE_DIPS);
+ getContentPane().getAllStyles().setPadding(3.2f, 3.2f, 3.2f, 3.2f);
+ Label title = new Label(s.title);
+ title.getAllStyles().setFont(font("native:MainRegular", 24));
+ add(title);
+ add(new Label(s.speaker + ", " + s.company));
+ add(new Label("Day " + s.day + ", " + s.timeLabel(), FontImage.createMaterial(FontImage.MATERIAL_SCHEDULE, "Label", 3.5f)));
+ add(new Label(s.room, FontImage.createMaterial(FontImage.MATERIAL_PLACE, "Label", 3.5f)));
+ com.codename1.components.SpanLabel description = new com.codename1.components.SpanLabel(s.description);
+ add(description);
+ }
+ }
+
+ /**
+ * The steps of Perf.kt, timed the same way: from the state change to the
+ * marker's paint, one frame between steps.
+ */
+ static final class Perf implements Animation {
+ private final JavaBench app;
+ private final List recompose = new ArrayList();
+ private final List work = new ArrayList();
+ private final List frames = new ArrayList();
+ private int phase;
+ private int step;
+ private int gap;
+ private boolean waiting;
+ private boolean requested;
+ private long t0;
+ private long last;
+
+ Perf(JavaBench app) {
+ this.app = app;
+ }
+
+ void start() {
+ System.out.println("KMPBENCH perf-start 0");
+ gap = 10;
+ app.form.registerAnimated(this);
+ }
+
+ @Override
+ public boolean animate() {
+ Marker m = app.marker;
+ if (waiting) {
+ if (m.drawnSeq < m.seq) {
+ return false;
+ }
+ waiting = false;
+ record(m.drawnAt);
+ // Perf.kt notices the paint in the next frame and steps in the
+ // one after; returning here keeps the same two frames per step.
+ return false;
+ }
+ if (gap > 0) {
+ gap--;
+ return false;
+ }
+ if (!requested) {
+ // The step runs at the start of the next cycle's layout pass,
+ // where a Compose frame runs: change, layout and paint then
+ // share one cycle on both sides.
+ requested = true;
+ app.stepHost.revalidateLater();
+ }
+ return false;
+ }
+
+ /** Called from the schedule tab's layout. */
+ void layoutStep() {
+ if (requested && !waiting) {
+ requested = false;
+ next();
+ }
+ }
+
+ private void record(long drawnAt) {
+ long us = (drawnAt - t0) / 1000;
+ if (phase == 0) {
+ if (step > 30) {
+ recompose.add(Long.valueOf(us));
+ }
+ } else if (phase >= 2) {
+ work.add(Long.valueOf(us));
+ if (last != 0) {
+ frames.add(Long.valueOf((drawnAt - last) / 1000));
+ }
+ last = drawnAt;
+ }
+ }
+
+ private void mark() {
+ Marker m = app.marker;
+ m.seq++;
+ m.repaint();
+ waiting = true;
+ }
+
+ private void next() {
+ if (phase == 0) {
+ if (step == 330) {
+ report("recompose-median-us", pct(recompose, 50));
+ report("recompose-p95-us", pct(recompose, 95));
+ phase = 1;
+ step = 0;
+ app.setDay(1);
+ gap = 2;
+ return;
+ }
+ step++;
+ t0 = System.nanoTime();
+ app.setDay(app.day % 3 + 1);
+ mark();
+ return;
+ }
+ // phase 1 scrolls day 1 as a warm-up; phases 2..4 scroll days 1..3.
+ if (step == 0) {
+ app.schedule.scrollToCard(0);
+ app.schedule.repaint();
+ last = 0;
+ step = 1;
+ gap = 1;
+ return;
+ }
+ if (step < SESSIONS_PER_DAY) {
+ t0 = System.nanoTime();
+ app.schedule.scrollToCard(step);
+ app.schedule.repaint();
+ step++;
+ mark();
+ return;
+ }
+ if (phase == 4) {
+ app.form.deregisterAnimated(this);
+ long fm = pct(frames, 50);
+ report("scroll-frames", frames.size());
+ report("scroll-frame-median-us", fm);
+ report("scroll-frame-p95-us", pct(frames, 95));
+ int jank = 0;
+ for (Long f : frames) {
+ if (f.longValue() > 2 * fm) {
+ jank++;
+ }
+ }
+ report("scroll-jank-frames", jank);
+ report("scroll-work-median-us", pct(work, 50));
+ report("scroll-work-p95-us", pct(work, 95));
+ report("perf-done", 0);
+ return;
+ }
+ if (phase == 1) {
+ work.clear();
+ frames.clear();
+ }
+ phase++;
+ step = 0;
+ app.setDay(phase - 1);
+ gap = 2;
+ }
+
+ @Override
+ public void paint(Graphics g) {
+ }
+
+ private static long pct(List v, int p) {
+ Long[] s = v.toArray(new Long[v.size()]);
+ java.util.Arrays.sort(s);
+ return s[Math.min(s.length - 1, (s.length * p) / 100)].longValue();
+ }
+
+ private static void report(String name, long v) {
+ System.out.println("KMPBENCH " + name + " " + v);
+ }
+ }
+}
diff --git a/scripts/kmp-bench/app/prepare.sh b/scripts/kmp-bench/app/prepare.sh
new file mode 100755
index 00000000000..0b366585132
--- /dev/null
+++ b/scripts/kmp-bench/app/prepare.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# Materialises all three sides of the Kotlin Multiplatform benchmark into a work
+# directory, from ONE copy of the application.
+#
+# scripts/kmp-bench/app/prepare.sh --work /tmp/kbench [--maven-repo DIR]
+#
+# shared/commonMain is the application: plain Compose Multiplatform code with
+# no Codename One import in it. Both projects compile that directory as their
+# commonMain, so an edit cannot land on one side only.
+#
+# Outputs:
+# /cn1/ a Codename One project generated from the shipping archetype,
+# with the source in common/src/main/kmp (the compose profile)
+# /cmp/ a JetBrains Compose Multiplatform project (desktop and iOS)
+set -Eeuo pipefail
+trap 'rc=$?; echo "prepare.sh: FAILED at line $LINENO (exit $rc)" >&2' ERR
+
+WORK=""
+CN1_VERSION="${CN1_VERSION:-}"
+MAVEN_REPO_LOCAL="${MAVEN_REPO_LOCAL:-}"
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --work) WORK="$2"; shift 2 ;;
+ --cn1-version) CN1_VERSION="$2"; shift 2 ;;
+ --maven-repo) MAVEN_REPO_LOCAL="$2"; shift 2 ;;
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
+ esac
+done
+[ -n "$WORK" ] || { echo "usage: prepare.sh --work " >&2; exit 2; }
+HERE="$(cd "$(dirname "$0")" && pwd)"
+REPO="$(cd "$HERE/../../.." && pwd)"
+if [ -z "$CN1_VERSION" ]; then
+ CN1_VERSION="$(sed -n 's:.*\(.*-SNAPSHOT\).*:\1:p' "$REPO/maven/pom.xml" | head -1)"
+fi
+MVN_REPO_ARG=""
+[ -n "$MAVEN_REPO_LOCAL" ] && MVN_REPO_ARG="-Dmaven.repo.local=$MAVEN_REPO_LOCAL"
+
+rm -rf "$WORK/cn1" "$WORK/cmp" "$WORK/java"
+mkdir -p "$WORK"
+
+# --- Codename One -----------------------------------------------------------
+
+( cd "$WORK" && mvn -B -q $MVN_REPO_ARG archetype:generate \
+ -DarchetypeArtifactId=cn1app-archetype -DarchetypeGroupId=com.codenameone \
+ -DarchetypeVersion="$CN1_VERSION" -DgroupId=com.example.kmpbench -DartifactId=cn1 \
+ -Dversion=1.0-SNAPSHOT -DmainName=Bench -DinteractiveMode=false )
+COMMON="$WORK/cn1/common"
+cp "$HERE/cn1/Bench.java" "$COMMON/src/main/java/com/example/kmpbench/Bench.java"
+mkdir -p "$COMMON/src/main/kotlin/com/example/kmpbench" "$COMMON/src/main/kmp/commonMain"
+cp "$HERE/cn1/BenchEntry.kt" "$COMMON/src/main/kotlin/com/example/kmpbench/"
+cp -R "$HERE/shared/commonMain/kotlin" "$COMMON/src/main/kmp/commonMain/kotlin"
+echo "codename1.arg.ios.newStorageLocation=true" >> "$COMMON/codenameone_settings.properties"
+
+# --- Codename One, written in Java ------------------------------------------
+# The same screens over Codename One directly: no Kotlin sources, so neither
+# the Kotlin standard library nor the Compose runtime is in the application.
+
+( cd "$WORK" && mvn -B -q $MVN_REPO_ARG archetype:generate \
+ -DarchetypeArtifactId=cn1app-archetype -DarchetypeGroupId=com.codenameone \
+ -DarchetypeVersion="$CN1_VERSION" -DgroupId=com.example.javabench -DartifactId=java \
+ -Dversion=1.0-SNAPSHOT -DmainName=JavaBench -DinteractiveMode=false )
+cp "$HERE/java/JavaBench.java" "$WORK/java/common/src/main/java/com/example/javabench/JavaBench.java"
+echo "codename1.arg.ios.newStorageLocation=true" >> "$WORK/java/common/codenameone_settings.properties"
+
+# --- Compose Multiplatform --------------------------------------------------
+
+cp -R "$HERE/cmp/." "$WORK/cmp/"
+mkdir -p "$WORK/cmp/composeApp/src"
+cp -R "$HERE/shared/commonMain" "$WORK/cmp/composeApp/src/commonMain"
+echo "prepared $WORK (cn1 $CN1_VERSION)"
diff --git a/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/App.kt b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/App.kt
new file mode 100644
index 00000000000..02185b5a036
--- /dev/null
+++ b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/App.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package keynote
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.DateRange
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Schedule
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+/// What the application shows, hoisted so the benchmark can drive it.
+class AppState {
+ var tab by mutableIntStateOf(0)
+ var day by mutableIntStateOf(1)
+ var query by mutableStateOf("")
+ var selected by mutableStateOf(null)
+ val favorites = mutableStateListOf()
+
+ // Benchmark hooks (Perf.kt); unused in a normal launch.
+ var list: LazyListState? = null
+ var tick by mutableIntStateOf(0)
+ var drawnSeq = 0
+ var drawnAt = 0L
+
+ fun toggleFavorite(id: Int) {
+ if (!favorites.remove(id)) favorites.add(id)
+ }
+}
+
+private val Brand = Color(0xFF7F52FF)
+
+@Composable
+fun App(state: AppState = remember { AppState() }, perf: Boolean = false) {
+ val dark = isSystemInDarkTheme()
+ val scheme = if (dark) darkColorScheme(primary = Color(0xFFB9A4FF), secondaryContainer = Color(0xFF3A2D6B))
+ else lightColorScheme(primary = Brand, secondaryContainer = Color(0xFFE9E1FF))
+ MaterialTheme(colorScheme = scheme) {
+ val sessions by remember { Programme.load() }.collectAsState(emptyList())
+ if (perf && sessions.isNotEmpty()) LaunchedEffect(Unit) { runPerf(state) }
+ val selected = state.selected
+ if (selected != null) {
+ SessionDetail(selected, state.favorites.contains(selected.id), { state.toggleFavorite(selected.id) }) {
+ state.selected = null
+ }
+ return@MaterialTheme
+ }
+ Scaffold(
+ topBar = {
+ CenterAlignedTopAppBar(title = { Text(if (state.tab == 2) "Insights" else "KotlinConf-ish 2026",
+ fontWeight = FontWeight.SemiBold) })
+ },
+ bottomBar = {
+ NavigationBar {
+ NavigationBarItem(state.tab == 0, { state.tab = 0 }, { Icon(Icons.Default.DateRange, null) },
+ label = { Text("Schedule") }, modifier = Modifier.testTag("tab-schedule"))
+ NavigationBarItem(state.tab == 1, { state.tab = 1 }, { Icon(Icons.Default.Favorite, null) },
+ label = { Text("Favorites") }, modifier = Modifier.testTag("tab-favorites"))
+ NavigationBarItem(state.tab == 2, { state.tab = 2 }, { Icon(Icons.Default.Info, null) },
+ label = { Text("Insights") }, modifier = Modifier.testTag("tab-insights"))
+ }
+ }
+ ) { padding ->
+ Box(Modifier.padding(padding).fillMaxSize()) {
+ if (sessions.isEmpty()) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
+ } else when (state.tab) {
+ 0 -> Schedule(sessions, state)
+ 1 -> SessionList(sessions.filter { state.favorites.contains(it.id) }, state,
+ empty = "Tap the heart on a session to keep it here.")
+ else -> Insights(sessions, state)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Schedule(sessions: List, state: AppState) {
+ Column(Modifier.fillMaxSize()) {
+ OutlinedTextField(state.query, { state.query = it }, Modifier.fillMaxWidth().padding(16.dp, 8.dp).testTag("search"),
+ placeholder = { Text("Search talks and speakers") }, leadingIcon = { Icon(Icons.Default.Search, null) },
+ singleLine = true, shape = RoundedCornerShape(28.dp))
+ LazyRow(contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ items(listOf(1, 2, 3)) { d ->
+ FilterChip(state.day == d, { state.day = d }, { Text("Day $d") }, Modifier.testTag("day-$d"))
+ }
+ }
+ val q = state.query.trim()
+ val visible = sessions.filter {
+ it.day == state.day && (q.isEmpty() || it.title.contains(q, ignoreCase = true) ||
+ it.speaker.name.contains(q, ignoreCase = true))
+ }
+ val listState = rememberLazyListState()
+ state.list = listState
+ Box(Modifier.weight(1f)) {
+ SessionList(visible, state, empty = "No session matches \"$q\".", listState = listState)
+ }
+ // Drawn after the list in every frame that follows a benchmark step.
+ val seq = state.tick
+ Box(Modifier.size(1.dp).drawBehind { state.drawnAt = nowNanos(); state.drawnSeq = seq })
+ }
+}
+
+@Composable
+private fun SessionList(sessions: List, state: AppState, empty: String,
+ listState: LazyListState = rememberLazyListState()) {
+ if (sessions.isEmpty()) {
+ Box(Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) {
+ Text(empty, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ return
+ }
+ LazyColumn(Modifier.fillMaxSize().testTag("list"), state = listState, contentPadding = PaddingValues(16.dp, 8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ items(sessions, key = { it.id }) { s ->
+ SessionCard(s, state.favorites.contains(s.id), { state.toggleFavorite(s.id) }) { state.selected = s }
+ }
+ }
+}
+
+@Composable
+fun SessionCard(s: Session, favorite: Boolean, onFavorite: () -> Unit, onOpen: () -> Unit) {
+ Card(onOpen, Modifier.fillMaxWidth().testTag("session-${s.id}"),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow)) {
+ Row(Modifier.padding(start = 0.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier.width(6.dp).height(84.dp).background(Color(s.track.color)))
+ Column(Modifier.weight(1f).padding(12.dp)) {
+ Text(s.timeLabel + " - " + s.room, style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.height(4.dp))
+ Text(s.title, style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis)
+ Spacer(Modifier.height(2.dp))
+ Text(s.speaker.name + ", " + s.speaker.company, style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ FavoriteButton(favorite, onFavorite, Modifier.testTag("fav-${s.id}"))
+ }
+ }
+}
+
+@Composable
+private fun FavoriteButton(favorite: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
+ val scale by animateFloatAsState(if (favorite) 1f else 0.8f)
+ IconButton(onClick, modifier) {
+ Icon(if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
+ if (favorite) "Remove from favorites" else "Add to favorites",
+ Modifier.size((24 * scale).dp),
+ tint = if (favorite) Color(0xFFE91E63) else MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+}
+
+@Composable
+private fun SessionDetail(s: Session, favorite: Boolean, onFavorite: () -> Unit, onBack: () -> Unit) {
+ Scaffold(topBar = {
+ CenterAlignedTopAppBar(title = { Text(s.track.label) },
+ navigationIcon = { IconButton(onBack, Modifier.testTag("back")) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") } },
+ actions = { FavoriteButton(favorite, onFavorite) })
+ }) { padding ->
+ Column(Modifier.padding(padding).padding(20.dp).fillMaxWidth()) {
+ Text(s.title, style = MaterialTheme.typography.headlineSmall)
+ Spacer(Modifier.height(16.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier.size(48.dp).background(Color(s.track.color), CircleShape), contentAlignment = Alignment.Center) {
+ Text(s.speaker.name.split(" ").joinToString("") { it.take(1) }, color = Color.White,
+ fontWeight = FontWeight.Bold)
+ }
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text(s.speaker.name, style = MaterialTheme.typography.titleMedium)
+ Text(s.speaker.company, style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+ }
+ Spacer(Modifier.height(16.dp))
+ HorizontalDivider()
+ Spacer(Modifier.height(16.dp))
+ DetailLine(Icons.Default.Schedule, "Day ${s.day}, ${s.timeLabel}")
+ DetailLine(Icons.Default.Place, s.room)
+ Spacer(Modifier.height(16.dp))
+ Text(s.description, style = MaterialTheme.typography.bodyLarge)
+ }
+ }
+}
+
+@Composable
+private fun DetailLine(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String) {
+ Row(Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(icon, null, tint = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.width(12.dp))
+ Text(text, style = MaterialTheme.typography.bodyLarge)
+ }
+}
+
+/// Sessions per track, as bars that grow in when the tab opens.
+@Composable
+private fun Insights(sessions: List, state: AppState) {
+ var shown by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) { shown = true }
+ val grow by animateFloatAsState(if (shown) 1f else 0f, tween(700))
+ val counts = Track.entries.map { t -> t to sessions.count { it.track == t && it.day == state.day } }
+ val max = (counts.maxOfOrNull { it.second } ?: 1).coerceAtLeast(1)
+ Column(Modifier.fillMaxSize().padding(20.dp)) {
+ Text("Sessions per track, day ${state.day}", style = MaterialTheme.typography.titleMedium)
+ Spacer(Modifier.height(16.dp))
+ val bar = MaterialTheme.colorScheme.surfaceContainerHighest
+ Canvas(Modifier.fillMaxWidth().height(220.dp).testTag("chart")) {
+ val gap = 16.dp.toPx()
+ val w = (size.width - gap * (counts.size - 1)) / counts.size
+ counts.forEachIndexed { i, (track, n) ->
+ val x = i * (w + gap)
+ drawRoundRect(bar, Offset(x, 0f), Size(w, size.height), CornerRadius(8.dp.toPx()))
+ val h = size.height * n / max * grow
+ drawRoundRect(Color(track.color), Offset(x, size.height - h), Size(w, h), CornerRadius(8.dp.toPx()))
+ }
+ }
+ Spacer(Modifier.height(12.dp))
+ counts.forEach { (track, n) ->
+ Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) {
+ Box(Modifier.size(12.dp).background(Color(track.color), CircleShape))
+ Spacer(Modifier.width(8.dp))
+ Text(track.label, Modifier.weight(1f))
+ Text("$n", fontWeight = FontWeight.SemiBold)
+ }
+ }
+ AnimatedVisibility(state.favorites.isNotEmpty()) {
+ Text("You have ${state.favorites.size} favorite sessions.", Modifier.padding(top = 12.dp),
+ color = MaterialTheme.colorScheme.primary)
+ }
+ }
+}
diff --git a/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Data.kt b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Data.kt
new file mode 100644
index 00000000000..c061c7bbf57
--- /dev/null
+++ b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Data.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package keynote
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+enum class Track(val label: String, val color: Long) {
+ Language("Language", 0xFF7F52FF),
+ Multiplatform("Multiplatform", 0xFF0095D5),
+ Server("Server", 0xFF2E7D32),
+ Tooling("Tooling", 0xFFE65100),
+ UI("UI", 0xFFC2185B)
+}
+
+data class Speaker(val name: String, val company: String)
+
+data class Session(
+ val id: Int,
+ val title: String,
+ val speaker: Speaker,
+ val track: Track,
+ val day: Int,
+ val startMinutes: Int,
+ val lengthMinutes: Int,
+ val room: String,
+ val description: String
+) {
+ val timeLabel: String get() = clock(startMinutes) + " - " + clock(startMinutes + lengthMinutes)
+}
+
+fun clock(minutes: Int): String {
+ val h = minutes / 60
+ val m = minutes % 60
+ return (if (h < 10) "0" else "") + h + ":" + (if (m < 10) "0" else "") + m
+}
+
+/// The conference programme. Generated from a fixed seed, so every build and
+/// every run of the benchmark sees the same 150 sessions.
+object Programme {
+ private val first = listOf("Ada", "Linus", "Grace", "Alan", "Barbara", "Ken", "Margaret", "Dennis", "Frances", "Edsger",
+ "Radia", "Bjarne", "Sophie", "Guido", "Hedy", "James")
+ private val last = listOf("Lovelace", "Hopper", "Turing", "Liskov", "Thompson", "Hamilton", "Ritchie", "Allen",
+ "Dijkstra", "Perlman", "Wilson", "Lamarr", "Gosling", "Kay", "Knuth", "Backus")
+ private val companies = listOf("JetBrains", "Google", "Square", "Netflix", "Touchlab", "Cash App", "Shopify", "Zalando")
+ private val topics = listOf("Coroutines", "Flows", "Compose", "K2", "Ktor", "Gradle", "Serialization", "Wasm",
+ "Native interop", "Context parameters", "Value classes", "Multiplatform libraries", "Testing", "Performance",
+ "Accessibility", "Animations", "State hoisting", "Server-side Kotlin", "Build speed", "Code sharing")
+ private val shapes = listOf("Deep dive into %s", "%s in production", "What's new in %s", "%s: lessons learned",
+ "Mastering %s", "%s from scratch", "The hidden cost of %s", "%s at scale")
+ private val rooms = listOf("Auditorium", "Room 1", "Room 2", "Workshop A", "Lightning stage")
+
+ val sessions: List by lazy { generate() }
+
+ private fun generate(): List {
+ var seed = 20261025L
+ fun next(n: Int): Int {
+ seed = (seed * 6364136223846793005L + 1442695040888963407L)
+ return ((seed ushr 33) % n).toInt()
+ }
+ val out = ArrayList()
+ var id = 0
+ for (day in 1..3) {
+ var start = 9 * 60
+ while (out.count { it.day == day } < 50) {
+ for (room in rooms) {
+ if (out.count { it.day == day } >= 50) break
+ val topic = topics[next(topics.size)]
+ val title = shapes[next(shapes.size)].replace("%s", topic)
+ val speaker = Speaker(first[next(first.size)] + " " + last[next(last.size)], companies[next(companies.size)])
+ val len = if (room == "Lightning stage") 15 else if (room.startsWith("Workshop")) 90 else 45
+ out.add(Session(id++, title, speaker, Track.entries[next(Track.entries.size)], day, start, len, room,
+ "$title. ${speaker.name} of ${speaker.company} walks through $topic: the model behind it, " +
+ "the mistakes teams make with it, and the patterns that hold up in real applications. " +
+ "Bring questions; the last ten minutes are open Q&A."))
+ }
+ start += 60
+ }
+ }
+ return out
+ }
+
+ /// The programme as a feed would deliver it: off the main thread, after a
+ /// network-like delay.
+ fun load(): Flow> = flow {
+ delay(150)
+ emit(sessions)
+ }.flowOn(Dispatchers.Default)
+}
diff --git a/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Perf.kt b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Perf.kt
new file mode 100644
index 00000000000..ba03c737cba
--- /dev/null
+++ b/scripts/kmp-bench/app/shared/commonMain/kotlin/keynote/Perf.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package keynote
+
+import androidx.compose.runtime.withFrameNanos
+import kotlin.time.TimeSource
+
+/// CPU-bound measurements run inside the app, identical on both sides. Each
+/// result is printed as "KMPBENCH " for run_bench.sh.
+///
+/// Time is taken from a state change to the moment a 1 dp marker after the list
+/// is drawn (`AppState.drawnAt`), so it covers recomposition, layout and drawing
+/// of that frame. The simulator paces frames to its display, so frame interval
+/// alone mostly shows vsync; the change-to-drawn time is the work itself.
+private val origin = TimeSource.Monotonic.markNow()
+internal fun nowNanos(): Long = origin.elapsedNow().inWholeNanoseconds
+
+private suspend fun awaitDrawn(state: AppState, seq: Int) {
+ var guard = 0
+ while (state.drawnSeq < seq) {
+ if (guard++ == 60) { println("KMPBENCH perf-stall " + seq); throw IllegalStateException("marker not drawn") }
+ withFrameNanos { }
+ }
+}
+
+private fun pct(v: List, p: Int): Long {
+ val s = v.sorted()
+ return s[minOf(s.size - 1, (s.size * p) / 100)]
+}
+
+private fun report(name: String, v: Long) = println("KMPBENCH $name $v")
+
+private suspend fun recompose(state: AppState, n: Int): List {
+ val out = ArrayList()
+ for (i in 0 until n) {
+ withFrameNanos { }
+ val t0 = nowNanos()
+ state.day = state.day % 3 + 1
+ val seq = ++state.tick
+ awaitDrawn(state, seq)
+ out.add((state.drawnAt - t0) / 1000)
+ }
+ return out
+}
+
+/// Scrolls the list of the current day one card per frame, to the end.
+private suspend fun scrollDay(state: AppState, work: MutableList, frames: MutableList) {
+ val list = state.list ?: return
+ list.scrollToItem(0)
+ withFrameNanos { }
+ val count = 50 // sessions per day, Data.kt
+ var last = 0L
+ for (i in 1 until count) {
+ withFrameNanos { }
+ val t0 = nowNanos()
+ list.scrollToItem(i)
+ val seq = ++state.tick
+ awaitDrawn(state, seq)
+ work.add((state.drawnAt - t0) / 1000)
+ if (last != 0L) frames.add((state.drawnAt - last) / 1000)
+ last = state.drawnAt
+ }
+}
+
+suspend fun runPerf(state: AppState) {
+ println("KMPBENCH perf-start 0")
+ repeat(10) { withFrameNanos { } }
+ recompose(state, 30) // warm-up, discarded
+ val r = recompose(state, 300)
+ report("recompose-median-us", pct(r, 50)); report("recompose-p95-us", pct(r, 95))
+ state.day = 1; withFrameNanos { }; withFrameNanos { }
+ scrollDay(state, ArrayList(), ArrayList()) // warm-up, discarded
+ val work = ArrayList(); val frames = ArrayList()
+ for (d in 1..3) {
+ state.day = d; withFrameNanos { }; withFrameNanos { }
+ scrollDay(state, work, frames)
+ }
+ val fm = pct(frames, 50)
+ report("scroll-frames", frames.size.toLong())
+ report("scroll-frame-median-us", fm); report("scroll-frame-p95-us", pct(frames, 95))
+ report("scroll-jank-frames", frames.count { it > 2 * fm }.toLong())
+ report("scroll-work-median-us", pct(work, 50)); report("scroll-work-p95-us", pct(work, 95))
+ report("perf-done", 0)
+}
diff --git a/scripts/kmp-bench/chart.py b/scripts/kmp-bench/chart.py
new file mode 100755
index 00000000000..24dcef69059
--- /dev/null
+++ b/scripts/kmp-bench/chart.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation. Codename One designates this
+# particular file as subject to the "Classpath" exception as provided
+# by Oracle in the LICENSE file that accompanied this code.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Codename One through http://www.codenameone.com/ if you
+# need additional information or have any questions.
+#
+"""Draws results.json as the guide's chart, every metric relative to CMP.
+
+Each metric is one group on the same axis: Compose Multiplatform is the dashed
+100% line, and the two bars are Codename One running the same Compose source
+and Codename One running the same screens written in Java, each as a share of
+it. A bar half as long means half the size or half the time. The absolute
+values are printed beside each bar.
+
+ chart.py results.json out.svg
+"""
+import json
+import sys
+
+FONT = 'font-family="Arial, sans-serif"'
+SERIES = [("codename_one", "Codename One, same Compose source", "#b5651d"),
+ ("codename_one_java", "Codename One, the same screens in Java", "#2e7d5b")]
+
+
+def rows(r):
+ """(label, cn1 value, cmp value, formatter) for every metric present."""
+ mb = lambda b: "%.1f MB" % (b / 1048576.0)
+ ms = lambda v: "%.1f ms" % v if v < 10 else "%d ms" % round(v)
+ us = lambda k: (lambda x: x["perf_median_of_runs_us"].get(k, 0) / 1000.0)
+ key = lambda k: (lambda x: x.get(k))
+ spec = [
+ ("Executable, stripped", key("executable_stripped_bytes"), mb),
+ ("App bundle", key("bundle_bytes"), mb),
+ ("Physical footprint", key("footprint_bytes"), mb),
+ ("Cold start", key("cold_start_ms"), ms),
+ ("Warm start, median", key("warm_start_median_ms"), ms),
+ ("Recomposition, median", us("recompose-median-us"), ms),
+ ("Recomposition, p95", us("recompose-p95-us"), ms),
+ ("Scroll work per frame, median", us("scroll-work-median-us"), ms),
+ ("Scroll work per frame, p95", us("scroll-work-p95-us"), ms),
+ ("Scroll frame interval, median", us("scroll-frame-median-us"), ms),
+ ]
+ base = r["compose_multiplatform"]
+ out = []
+ for label, g, f in spec:
+ vals = [g(r[k]) if k in r else None for k, _, _ in SERIES]
+ if g(base) and all(vals):
+ out.append((label, vals, g(base), f))
+ return out
+
+
+def svg(r):
+ data = rows(r)
+ left, axis_w, bar_h, group_h, top = 250, 400, 13, 36, 92
+ scale_max = max(1.25, max(c / m for _, vals, m, _ in data for c in vals) * 1.1)
+ px = lambda share: axis_w * share / scale_max
+ h = top + group_h * len(data) + 40
+ w = left + axis_w + 220
+ out = ['')
+ return "\n".join(out) + "\n"
+
+
+if __name__ == "__main__":
+ with open(sys.argv[1]) as f:
+ results = json.load(f)
+ with open(sys.argv[2], "w") as f:
+ f.write(svg(results))
diff --git a/scripts/kmp-bench/results.json b/scripts/kmp-bench/results.json
new file mode 100644
index 00000000000..3e9bb61fec7
--- /dev/null
+++ b/scripts/kmp-bench/results.json
@@ -0,0 +1,77 @@
+{
+ "codename_one": {
+ "cold_start_ms": 961,
+ "warm_start_median_ms": 580.0,
+ "starts_ms": [
+ 961,
+ 621,
+ 592,
+ 566,
+ 568
+ ],
+ "executable_stripped_bytes": 17617384,
+ "bundle_bytes": 19152896,
+ "bundle_without_executable_bytes": 1405874,
+ "footprint_bytes": 49283072,
+ "perf_median_of_runs_us": {
+ "recompose-p95-us": 6173,
+ "scroll-work-p95-us": 6601,
+ "recompose-median-us": 4576,
+ "scroll-frames": 144,
+ "scroll-work-median-us": 4039,
+ "scroll-frame-p95-us": 38122,
+ "scroll-frame-median-us": 35447,
+ "scroll-jank-frames": 0
+ }
+ },
+ "codename_one_java": {
+ "cold_start_ms": 805,
+ "warm_start_median_ms": 572.0,
+ "starts_ms": [
+ 805,
+ 663,
+ 567,
+ 573,
+ 571
+ ],
+ "executable_stripped_bytes": 9354120,
+ "bundle_bytes": 10731520,
+ "bundle_without_executable_bytes": 1324542,
+ "footprint_bytes": 49283072,
+ "perf_median_of_runs_us": {
+ "recompose-p95-us": 2013,
+ "scroll-work-p95-us": 2677,
+ "recompose-median-us": 1487,
+ "scroll-frames": 144,
+ "scroll-work-median-us": 1759,
+ "scroll-frame-p95-us": 37944,
+ "scroll-frame-median-us": 34789,
+ "scroll-jank-frames": 0
+ }
+ },
+ "compose_multiplatform": {
+ "cold_start_ms": 1008,
+ "warm_start_median_ms": 604.0,
+ "starts_ms": [
+ 1008,
+ 713,
+ 597,
+ 603,
+ 605
+ ],
+ "executable_stripped_bytes": 24041184,
+ "bundle_bytes": 24051712,
+ "bundle_without_executable_bytes": 2510,
+ "footprint_bytes": 50331648,
+ "perf_median_of_runs_us": {
+ "recompose-p95-us": 23015,
+ "scroll-work-p95-us": 18207,
+ "recompose-median-us": 19238,
+ "scroll-frames": 144,
+ "scroll-work-median-us": 17030,
+ "scroll-frame-p95-us": 34740,
+ "scroll-frame-median-us": 33349,
+ "scroll-jank-frames": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/scripts/kmp-bench/run_bench.sh b/scripts/kmp-bench/run_bench.sh
new file mode 100755
index 00000000000..dbd062df04d
--- /dev/null
+++ b/scripts/kmp-bench/run_bench.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation. Codename One designates this
+# particular file as subject to the "Classpath" exception as provided
+# by Oracle in the LICENSE file that accompanied this code.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Codename One through http://www.codenameone.com/ if you
+# need additional information or have any questions.
+#
+# Builds the Keynote app twice from the same Compose source -- once with
+# Codename One (compose-compat, ParparVM) and once with Compose Multiplatform
+# (Kotlin/Native + Skia) -- and a third time from app/java, the same screens
+# written in Java over Codename One directly. Runs all three on one iOS
+# simulator and writes results.json and results.md into the work directory.
+#
+# run_bench.sh --work [--runs N] [--device ] [--maven-repo ]
+#
+# Start-up is process start to marker: each app prints "KMPBENCH first-frame
+# " from the first main-thread turn after its UI is installed, and the
+# start is the kernel's start time for the process, so simctl's own latency is
+# in neither. Compose Multiplatform's marker runs before Skia has drawn, which
+# favours it. Memory is physical footprint (footprint(1)) a few seconds after
+# launch; resident size counts the shared dyld cache on the simulator. Run 1 of
+# each is cold (first launch after install) and reported separately.
+set -Eeuo pipefail
+trap 'rc=$?; echo "run_bench.sh: FAILED at line $LINENO (exit $rc)" >&2' ERR
+
+WORK=""; RUNS=5; DEVICE=""; MAVEN_REPO_LOCAL="${MAVEN_REPO_LOCAL:-}"
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --work) WORK="$2"; shift 2 ;;
+ --runs) RUNS="$2"; shift 2 ;;
+ --device) DEVICE="$2"; shift 2 ;;
+ --maven-repo) MAVEN_REPO_LOCAL="$2"; shift 2 ;;
+ *) echo "unknown argument: $1" >&2; exit 2 ;;
+ esac
+done
+[ -n "$WORK" ] || { echo "usage: run_bench.sh --work [--runs N]" >&2; exit 2; }
+HERE="$(cd "$(dirname "$0")" && pwd)"
+mkdir -p "$WORK"; WORK="$(cd "$WORK" && pwd)"
+MVN_REPO_ARG=""
+[ -n "$MAVEN_REPO_LOCAL" ] && MVN_REPO_ARG="-Dmaven.repo.local=$MAVEN_REPO_LOCAL"
+: "${JAVA17_HOME:?JAVA17_HOME must name a JDK 17}"
+
+"$HERE/app/prepare.sh" --work "$WORK" ${MAVEN_REPO_LOCAL:+--maven-repo "$MAVEN_REPO_LOCAL"}
+
+echo "== Codename One: translate and build"
+( cd "$WORK/cn1" && JAVA_HOME="$JAVA17_HOME" mvn -B -q $MVN_REPO_ARG package -DskipTests \
+ -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source -Dopen=false ) > "$WORK/cn1-build.log" 2>&1
+SRC="$(dirname "$(find "$WORK/cn1/common/target" -maxdepth 2 -name Bench.xcodeproj | head -1)")"
+( cd "$SRC" && xcodebuild -project Bench.xcodeproj -scheme Bench -configuration Release \
+ -sdk iphonesimulator -arch arm64 -derivedDataPath "$WORK/dd" CODE_SIGNING_ALLOWED=NO build ) \
+ > "$WORK/cn1-xcode.log" 2>&1
+CN1_APP="$WORK/dd/Build/Products/Release-iphonesimulator/Bench.app"
+CN1_EXE="$CN1_APP/Bench"
+CN1_ID=com.example.kmpbench
+
+echo "== Codename One, Java: translate and build"
+( cd "$WORK/java" && JAVA_HOME="$JAVA17_HOME" mvn -B -q $MVN_REPO_ARG package -DskipTests \
+ -Dcodename1.platform=ios -Dcodename1.buildTarget=ios-source -Dopen=false ) > "$WORK/java-build.log" 2>&1
+JSRC="$(dirname "$(find "$WORK/java/common/target" -maxdepth 2 -name JavaBench.xcodeproj | head -1)")"
+( cd "$JSRC" && xcodebuild -project JavaBench.xcodeproj -scheme JavaBench -configuration Release \
+ -sdk iphonesimulator -arch arm64 -derivedDataPath "$WORK/dd-java" CODE_SIGNING_ALLOWED=NO build ) \
+ > "$WORK/java-xcode.log" 2>&1
+JAVA_APP="$WORK/dd-java/Build/Products/Release-iphonesimulator/JavaBench.app"
+JAVA_EXE="$JAVA_APP/JavaBench"
+JAVA_ID=com.example.javabench
+
+echo "== Compose Multiplatform: build"
+( cd "$WORK/cmp" && JAVA_HOME="$JAVA17_HOME" gradle --no-daemon -q \
+ :composeApp:linkReleaseExecutableIosSimulatorArm64 ) > "$WORK/cmp-build.log" 2>&1
+# An executable, not a framework in an Xcode project: the bundle is the
+# executable plus an Info.plist, which is all a simulator app needs.
+CMP_APP="$WORK/Keynote.app"; CMP_EXE="$CMP_APP/Keynote"; CMP_ID=com.example.kmpbench.cmp
+rm -rf "$CMP_APP"; mkdir -p "$CMP_APP"
+cp "$WORK/cmp/composeApp/build/bin/iosSimulatorArm64/releaseExecutable/composeApp.kexe" "$CMP_EXE"
+cp "$HERE/app/cmp/Info.plist" "$CMP_APP/Info.plist"
+codesign -s - --force "$CMP_APP" > /dev/null 2>&1
+
+stripped() { cp "$1" "$WORK/strip.tmp"; strip "$WORK/strip.tmp" 2>/dev/null || true; stat -f %z "$WORK/strip.tmp"; }
+# The bundle as shipped: a copy of the .app with its executable stripped.
+bundle() {
+ rm -rf "$WORK/bundle.tmp"; cp -R "$1" "$WORK/bundle.tmp"
+ strip "$WORK/bundle.tmp/$2" 2>/dev/null || true
+ du -sk "$WORK/bundle.tmp" | awk '{print $1 * 1024}'
+}
+# Bytes of everything in the bundle but the executable (fonts, theme, plists).
+resources() { find "$1" -type f ! -path "$1/$2" -exec stat -f %z {} + | awk '{s += $1} END {print s + 0}'; }
+largest() { find "$1" -type f ! -path "$1/$2" -exec stat -f '%z %N' {} + | sort -rn | head -5 | sed "s|$1/||"; }
+
+if [ -z "$DEVICE" ]; then
+ DEVICE="$(xcrun simctl list devices available | sed -n 's/.*iPhone 17 Pro (\([0-9A-F-]*\)).*/\1/p' | head -1)"
+fi
+[ -n "$DEVICE" ] || { echo "no iPhone simulator found; pass --device" >&2; exit 1; }
+xcrun simctl boot "$DEVICE" 2>/dev/null || true
+trap 'xcrun simctl shutdown "$DEVICE" 2>/dev/null || true' EXIT
+xcrun simctl bootstatus "$DEVICE" > /dev/null
+
+# The kernel's start time of a process, epoch milliseconds.
+started() {
+ python3 - "$1" <<'PY'
+import ctypes, ctypes.util, struct, sys
+libc = ctypes.CDLL(ctypes.util.find_library("c"))
+mib = (ctypes.c_int * 4)(1, 14, 1, int(sys.argv[1])) # CTL_KERN, KERN_PROC, KERN_PROC_PID
+buf = ctypes.create_string_buffer(1024); size = ctypes.c_size_t(1024)
+libc.sysctl(mib, 4, buf, ctypes.byref(size), None, 0)
+sec, usec = struct.unpack_from("qi", buf.raw, 0) # kp_proc.p_starttime
+print(sec * 1000 + usec // 1000)
+PY
+}
+# Prints one start-up time per run, milliseconds, space separated.
+measure() {
+ local id="$1" name="$2" exe="$3" out=""
+ for i in $(seq 1 "$RUNS"); do
+ xcrun simctl terminate "$DEVICE" "$id" 2>/dev/null || true
+ local log="$WORK/$name-run$i.log" t0 t
+ ( timeout 20 xcrun simctl launch --console-pty "$DEVICE" "$id" > "$log" 2>&1 || true ) &
+ for _ in $(seq 1 100); do grep -q "KMPBENCH first-frame" "$log" 2>/dev/null && break; sleep 0.1; done
+ t="$(grep -o 'KMPBENCH first-frame [0-9]*' "$log" | awk '{print $3}' | head -1)"
+ [ -n "$t" ] || { echo "$name run $i printed no marker; see $log" >&2; exit 1; }
+ t0="$(started "$(pgrep -f "$exe" | head -1)")"
+ out="$out $((t - t0))"
+ sleep 3
+ # While the app is still up: a launch is killed by its timeout after 20s.
+ [ "$i" = "$RUNS" ] && xcrun simctl io "$DEVICE" screenshot "$WORK/$name.png" > /dev/null 2>&1 || true
+ done
+ echo "$out"
+}
+footprint_bytes() {
+ xcrun simctl launch "$DEVICE" "$1" > /dev/null 2>&1; sleep 5
+ footprint "$(pgrep -f "$2" | head -1)" 2>&1 \
+ | awk '/Footprint:/ { for (i = 1; i <= NF; i++) if ($i == "Footprint:") {
+ v = $(i + 1); u = $(i + 2); print int(v * (u == "GB" ? 1073741824 : u == "MB" ? 1048576 : 1024)); exit } }'
+}
+
+for side in cn1 java cmp; do
+ if [ $side = cn1 ]; then app="$CN1_APP"; id=$CN1_ID; exe=/Bench.app/Bench
+ elif [ $side = java ]; then app="$JAVA_APP"; id=$JAVA_ID; exe=JavaBench.app/JavaBench
+ else app="$CMP_APP"; id=$CMP_ID; exe=Keynote.app/Keynote; fi
+ xcrun simctl uninstall "$DEVICE" "$id" 2>/dev/null || true
+ xcrun simctl install "$DEVICE" "$app"
+ eval "STARTS_$side=\"\$(measure \"\$id\" \"\$side\" \"\$exe\")\""
+done
+# Runtime measurements (app/shared/commonMain/kotlin/keynote/Perf.kt): one
+# launch per run with KMPBENCH_PERF=1, values in microseconds, collected as one
+# "name=v1,v2,..." line per measure.
+perf() {
+ local id="$1" name="$2"
+ for i in $(seq 1 "$RUNS"); do
+ xcrun simctl terminate "$DEVICE" "$id" 2>/dev/null || true
+ local log="$WORK/$name-perf$i.log"
+ ( SIMCTL_CHILD_KMPBENCH_PERF=1 timeout 180 xcrun simctl launch --console-pty "$DEVICE" "$id" > "$log" 2>&1 || true ) &
+ for _ in $(seq 1 1700); do grep -q "KMPBENCH perf-done" "$log" 2>/dev/null && break; sleep 0.1; done
+ grep -q "KMPBENCH perf-done" "$log" || { echo "$name perf run $i did not finish; see $log" >&2; exit 1; }
+ xcrun simctl terminate "$DEVICE" "$id" 2>/dev/null || true
+ done
+ cat "$WORK/$name"-perf[0-9]*.log | tr -d '\r' | awk '{ for (j = 1; j < NF; j++) if ($j == "KMPBENCH") break }
+ j < NF && $(j + 1) !~ /^(perf-done|perf-start|first-frame)$/ {
+ v[$(j + 1)] = v[$(j + 1)] (v[$(j + 1)] == "" ? "" : ",") $(j + 2) } END { for (k in v) print k "=" v[k] }' > "$WORK/$name-perf.txt"
+}
+perf $CN1_ID cn1; perf $JAVA_ID java; perf $CMP_ID cmp
+CN1_RSS="$(footprint_bytes $CN1_ID /Bench.app/Bench)"; JAVA_RSS="$(footprint_bytes $JAVA_ID JavaBench.app/JavaBench)"; CMP_RSS="$(footprint_bytes $CMP_ID Keynote.app/Keynote)"
+
+python3 - "$WORK" < /* _beginthreadex */
#include
#include
+#include
+#include
+#include
#include "cn1_win_compat.h"
@@ -565,4 +568,69 @@ int cn1_win_zone_offset_millis(const char* zoneId, long long millis, int* offset
return 1;
}
+FILE* cn1_win_fopen_utf8(const char* path, const char* mode) {
+ wchar_t wmode[8];
+ wchar_t* wide;
+ wchar_t* full;
+ wchar_t* target;
+ DWORD fullLen;
+ int wideLen;
+ int i;
+ FILE* f;
+ for(i = 0; mode[i] != 0 && i < 7; i++) {
+ wmode[i] = (wchar_t)(unsigned char)mode[i];
+ }
+ wmode[i] = 0;
+ wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0);
+ if(wideLen <= 0) {
+ errno = EINVAL;
+ return NULL;
+ }
+ wide = (wchar_t*)malloc(sizeof(wchar_t) * (size_t)wideLen);
+ if(wide == NULL) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ MultiByteToWideChar(CP_UTF8, 0, path, -1, wide, wideLen);
+ /* Already prefixed, or short enough for the classic API: as given. */
+ if(wideLen - 1 < MAX_PATH - 12 || wcsncmp(wide, L"\\\\?\\", 4) == 0) {
+ f = _wfopen(wide, wmode);
+ free(wide);
+ return f;
+ }
+ fullLen = GetFullPathNameW(wide, 0, NULL, NULL);
+ if(fullLen == 0) {
+ f = _wfopen(wide, wmode);
+ free(wide);
+ return f;
+ }
+ /* Room for \\?\ or \\?\UNC\ in front of the resolved path. */
+ full = (wchar_t*)malloc(sizeof(wchar_t) * ((size_t)fullLen + 8));
+ if(full == NULL) {
+ free(wide);
+ errno = ENOMEM;
+ return NULL;
+ }
+ target = full + 8;
+ if(GetFullPathNameW(wide, fullLen, target, NULL) == 0) {
+ f = _wfopen(wide, wmode);
+ free(full);
+ free(wide);
+ return f;
+ }
+ if(target[0] == L'\\' && target[1] == L'\\') {
+ /* \\server\share\x becomes \\?\UNC\server\share\x */
+ target += 2;
+ target -= 8;
+ memcpy(target, L"\\\\?\\UNC\\", 8 * sizeof(wchar_t));
+ } else {
+ target -= 4;
+ memcpy(target, L"\\\\?\\", 4 * sizeof(wchar_t));
+ }
+ f = _wfopen(target, wmode);
+ free(full);
+ free(wide);
+ return f;
+}
+
#endif /* _WIN32 */
diff --git a/vm/ByteCodeTranslator/src/cn1_win_compat.h b/vm/ByteCodeTranslator/src/cn1_win_compat.h
index 4ada0eb600d..7ad7d1dc49e 100644
--- a/vm/ByteCodeTranslator/src/cn1_win_compat.h
+++ b/vm/ByteCodeTranslator/src/cn1_win_compat.h
@@ -45,6 +45,7 @@
#include /* struct timespec (C11), localtime_s, _mkgmtime */
#include
#include /* _putenv_s */
+#include /* FILE, for cn1_win_fopen_utf8 */
#ifdef __cplusplus
extern "C" {
@@ -181,6 +182,16 @@ void cn1_win_resume_thread(void* handle);
int cn1_win_zone_offset_millis(const char* zoneId, long long millis, int* offsetOut,
int* dstOut, int* rawOut);
+/* --- opening a file by a UTF-8 path ---
+ fopen on Windows reads its argument in the ANSI code page and stops at
+ MAX_PATH (260) characters, so a UTF-8 path with a non-ASCII name, or any
+ path longer than that, failed to open although the file was there. This
+ converts to UTF-16, resolves the path (which is also what removes "..",
+ since the long-path prefix turns normalization off), adds the \\?\ prefix
+ when the result is too long for the classic API, and calls _wfopen. mode
+ is an ASCII fopen mode. Returns NULL with errno set, as fopen does. */
+FILE* cn1_win_fopen_utf8(const char* path, const char* mode);
+
/* --- environment / time.h POSIX helpers absent from MSVC ---
Thin static-inline wrappers over the MSVC equivalents; used by the date /
timezone runtime in nativeMethods. */
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java
index 96c1f2ad432..fa8a3e8f2c5 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeClass.java
@@ -41,6 +41,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -1393,9 +1394,7 @@ public String generateCCode(List allClasses) {
}
if(isInterface) {
- b.append("int **classToInterfaceMap_");
- b.append(clsName);
- b.append(";\n");
+ appendInterfaceMap(b, allClasses);
}
fullFieldList = new ArrayList();
@@ -1963,8 +1962,7 @@ public String generateCCode(List allClasses) {
// we pretend to have a virtual method here but the optimizer says its not really needed
if(!m.isVirtualOverriden()) {
m.setThunkCases(thunkCasesFor(m));
- m.appendVirtualMethodC(clsName, b, "classToInterfaceMap_" + clsName +
- "[cn1__cls->classId][" + offset + "]", true);
+ m.appendVirtualMethodC(clsName, b, interfaceSlot(offset), true);
m.setThunkCases(null);
}
offset++;
@@ -2034,13 +2032,13 @@ public String generateCCode(List allClasses) {
// ACQUIRE, not a plain load. This is the fast path of a double-checked
// initialisation: the completing store below is a RELEASE, and the two
// together are what make the writes this function performed -- the
- // vtable, and every classToInterfaceMap_[classId] row -- visible
- // to a thread that observes the flag set.
+ // vtable -- visible to a thread that observes the flag set.
//
// With plain accesses on arm64 a second thread could see LOADED==1 while
// those table stores were still invisible, then index a row that read as
- // NULL. OBSERVED: three identical SIGSEGVs at
- // classToInterfaceMap_java_util_NavigableMap[classId] + 0x8, reached from
+ // NULL. OBSERVED: three identical SIGSEGVs at what was then the run-time
+ // built classToInterfaceMap_java_util_NavigableMap[classId] + 0x8 (the map
+ // is constant data now, see appendInterfaceMap), reached from
// TreeSet.clear -> the interface dispatch for NavigableMap.clear, in a
// translator that is single-threaded in its own code but shares the
// process with the GC thread, which also runs Java and so also runs
@@ -2081,57 +2079,20 @@ public String generateCCode(List allClasses) {
b.append(".vtable = initVtableForInterface();\n ");
}
- // create the vtable
- b.append(" class__");
- b.append(clsName);
- b.append(".vtable = malloc(sizeof(void*) *");
- b.append(virtualMethodList.size());
- b.append(");\n");
if(isInterface) {
// special case, java_lang_object calls on interfaces should
- // act like standard virtual method calls
+ // act like standard virtual method calls. The dispatch map is constant
+ // data (appendInterfaceMap), so there is nothing else to build here.
b.append(" class__");
b.append(clsName);
b.append(".vtable = initVtableForInterface();\n");
- b.append(" classToInterfaceMap_");
- b.append(clsName);
- // calloc, not malloc: rows are filled only for classes that implement
- // this interface, so an id that does not read as a registered row must
- // read as NULL rather than as whatever the allocator last left there.
- b.append(" = calloc(cn1_array_start_offset, sizeof(int*));\n");
- for(ByteCodeClass cls : allClasses) {
- if(!cls.isInterface) {
- if(cls.doesImplement(this)) {
- b.append(" classToInterfaceMap_");
- b.append(clsName);
- b.append("[cn1_class_id_");
- b.append(cls.clsName);
- // sizeof(int), not sizeof(int*): a row is an int[] of vtable
- // slot indices -- the map is int**, so the ROW is what is
- // int-sized. Asking for pointer size allocated twice what the
- // row can ever hold, on every interface/implementor pair.
- b.append("] = malloc(sizeof(int) * ");
- b.append(getMethodCountIncludingBase());
- b.append(");\n");
- offset = 0;
- for(BytecodeMethod m : virtualMethodList) {
- if(!m.getClsName().equals("java_lang_Object")) {
- b.append(" classToInterfaceMap_");
- b.append(clsName);
- b.append("[cn1_class_id_");
- b.append(cls.clsName);
- b.append("][");
- b.append(offset);
- b.append("] = ");
- b.append(cls.virtualMethodList.indexOf(m));
- b.append(";\n");
- offset++;
- }
- }
- }
- }
- }
} else {
+ // create the vtable
+ b.append(" class__");
+ b.append(clsName);
+ b.append(".vtable = malloc(sizeof(void*) *");
+ b.append(virtualMethodList.size());
+ b.append(");\n");
b.append(" __INIT_VTABLE_");
b.append(clsName);
b.append("(threadStateData, class__");
@@ -2151,8 +2112,8 @@ public String generateCCode(List allClasses) {
// that recursion guard. Nothing outside this function may treat it as
// "safe to use the class" in the JLS sense -- a class under initialization
// is not finished. The release is what the INLINE GUARDS acquire against:
- // they test this flag, and it is what publishes the vtable and the
- // classToInterfaceMap rows written just above. Guarding them on
+ // they test this flag, and it is what publishes the vtable written just
+ // above. Guarding them on
// __X_LOADED__ instead would also be correct about the vtable and would
// additionally hold other threads until __CLINIT__ returned -- a strictly
// later gate than master opens, which moved layout on four native ports
@@ -2313,6 +2274,143 @@ private static char onDeviceDebugTypeCharFor(ByteCodeField bf) {
* @param interfaceObj the interface to test
* @return true when this class is assignable to it
*/
+ /// The interface dispatch map, emitted as constant data.
+ ///
+ /// Dispatching a method of an interface needs, for the receiver's class, the
+ /// vtable slot that implements it. That used to be a table built by the
+ /// interface's static initializer: one `calloc` of a pointer per class in the
+ /// program, plus one `malloc`ed row per implementing class. With 3,500 classes
+ /// that was 28 KB per interface whatever its implementers, and a Compose
+ /// application initializes ninety of them -- 2.5 MB of dirty heap, written once
+ /// and never changed, describing nothing the translator did not already know.
+ ///
+ /// It is now two `static const` arrays, which the linker puts in read-only
+ /// pages that are mapped from the binary and never dirtied:
+ ///
+ /// - `cn1_itrows_`: the rows, one slot per method that is not
+ /// Object's. Classes that inherit one layout share one row, so the
+ /// hundred-odd implementers of `Iterator` take a handful.
+ /// - `cn1_itix_`: for every class id between the lowest and highest
+ /// implementer, where its row starts. Only implementers ever index it --
+ /// the thunk is only reached with a receiver of the interface's type -- so
+ /// ids outside that range need no entry.
+ ///
+ /// The lookup is the same two dependent loads as before, and needs no
+ /// publication: nothing is written at run time, so no thread can read it half
+ /// built. It adds no branch either: see appendRowLookup.
+ private void appendInterfaceMap(StringBuilder b, List allClasses) {
+ List slots = new ArrayList();
+ for(BytecodeMethod m : virtualMethodList) {
+ if(!m.getClsName().equals("java_lang_Object")) {
+ slots.add(m);
+ }
+ }
+ interfaceMapBase = 0;
+ if(slots.isEmpty()) {
+ return;
+ }
+ List implementers = new ArrayList();
+ int min = Integer.MAX_VALUE;
+ int max = Integer.MIN_VALUE;
+ for(ByteCodeClass cls : allClasses) {
+ // An abstract class is never the class of an object, so no dispatch reads its row.
+ if(!cls.isInterface && !cls.isAbstract && cls.doesImplement(this)) {
+ implementers.add(cls);
+ min = Math.min(min, cls.getClassOffset());
+ max = Math.max(max, cls.getClassOffset());
+ }
+ }
+ if(implementers.isEmpty()) {
+ // Never dispatched through: no object of this type can exist. The thunks
+ // still compile against an empty map.
+ b.append("static const unsigned short cn1_itrows_").append(clsName).append("[1] __attribute__((unused));\n");
+ b.append("static inline JAVA_INT cn1_itrow_").append(clsName).append("(JAVA_INT id) __attribute__((unused));\n");
+ b.append("static inline JAVA_INT cn1_itrow_").append(clsName).append("(JAVA_INT id) {\n");
+ b.append(" __builtin_trap();\n return 0;\n}\n");
+ return;
+ }
+ Map rowOffsets = new HashMap();
+ StringBuilder rows = new StringBuilder();
+ int[] starts = new int[implementers.size()];
+ int rowCount = 0;
+ int maxValue = 0;
+ boolean missing = false;
+ for(int k = 0 ; k < implementers.size() ; k++) {
+ ByteCodeClass cls = implementers.get(k);
+ StringBuilder row = new StringBuilder();
+ for(BytecodeMethod m : slots) {
+ int idx = cls.virtualMethodList.indexOf(m);
+ maxValue = Math.max(maxValue, idx);
+ missing |= idx < 0;
+ row.append(idx).append(',');
+ }
+ String key = row.toString();
+ Integer at = rowOffsets.get(key);
+ if(at == null) {
+ at = rowCount * slots.size();
+ rowOffsets.put(key, at);
+ rows.append("\n ").append(key);
+ rowCount++;
+ }
+ starts[k] = at;
+ }
+ int lastStart = (rowCount - 1) * slots.size() + 1;
+ // A concrete class without the method (a stale or mismatched class file) gets -1, as it
+ // did when the rows were written at start-up: a call through it fails as it always has,
+ // and a class that is never called through still links. An unsigned row would turn the
+ // -1 into 65535, an index past any vtable, so such a table is stored signed.
+ String rowType = missing ? (maxValue < 32768 ? "short" : "JAVA_INT")
+ : (maxValue < 65536 ? "unsigned short" : "JAVA_INT");
+ String ixType = lastStart < 65536 ? "unsigned short" : "JAVA_INT";
+ // Dense, every entry a real row: a gap (a class in the range that does not implement
+ // the interface) takes the row of the lowest implementer; see appendRowLookup.
+ int[] dense = new int[max - min + 1];
+ int lowest = 0;
+ for(int k = 0 ; k < implementers.size() ; k++) {
+ if(implementers.get(k).getClassOffset() == min) {
+ lowest = starts[k];
+ }
+ }
+ java.util.Arrays.fill(dense, lowest);
+ for(int k = 0 ; k < implementers.size() ; k++) {
+ dense[implementers.get(k).getClassOffset() - min] = starts[k];
+ }
+ b.append("static const ").append(rowType).append(" cn1_itrows_").append(clsName)
+ .append("[] __attribute__((unused)) = {");
+ b.append(rows).append("\n};\n");
+ b.append("static const ").append(ixType).append(" cn1_itix_").append(clsName)
+ .append("[").append(max - min + 1).append("] __attribute__((unused)) = {");
+ for(int k = 0 ; k < dense.length ; k++) {
+ b.append(k == 0 ? "" : ",").append(k % 32 == 0 ? "\n " : "").append(dense[k]);
+ }
+ b.append("\n};\n");
+ interfaceMapBase = min;
+ appendRowLookup(b, String.valueOf(max - min + 1), "cn1_itix_" + clsName + "[d]");
+ }
+
+ /// The row of the receiver's class, with no branch: this runs on every interface call, and a
+ /// range test plus a trap here cost the backend benchmark about 1% of its throughput.
+ ///
+ /// A receiver that does not implement the interface can only get here through a cast that
+ /// did not hold, since ParparVM's CHECKCAST is unchecked. Its id is clamped into the table
+ /// (a conditional move, not a branch), and every entry is a real row, so such a call reaches
+ /// a real implementation of the same method with the wrong receiver -- what an unchecked cast
+ /// does everywhere else on ParparVM -- and never a row read from past the table.
+ private void appendRowLookup(StringBuilder b, String len, String read) {
+ b.append("static inline JAVA_INT cn1_itrow_").append(clsName).append("(JAVA_INT id) __attribute__((unused));\n");
+ b.append("static inline JAVA_INT cn1_itrow_").append(clsName).append("(JAVA_INT id) {\n");
+ b.append(" unsigned int d = (unsigned int)(id - ").append(interfaceMapBase).append(");\n");
+ b.append(" return ").append(read.replace("[d]", "[d < " + len + "u ? d : 0]")).append(";\n}\n");
+ }
+
+ /// The lowest class id cn1_itix_ covers; see appendInterfaceMap.
+ private int interfaceMapBase;
+
+ /// The vtable slot of the receiver's class for the interface method at `slot`.
+ private String interfaceSlot(int slot) {
+ return "cn1_itrows_" + clsName + "[cn1_itrow_" + clsName + "(cn1__cls->classId) + " + slot + "]";
+ }
+
boolean doesImplement(ByteCodeClass interfaceObj) {
if(baseInterfacesObject != null) {
if(baseInterfacesObject.contains(interfaceObj)) {
@@ -3160,7 +3258,7 @@ public boolean hasMarkWork() {
/// from then on it is always false. Two kinds qualify:
///
/// - A class with NO . Its __STATIC_INITIALIZER only mallocs and fills its
- /// vtable, or for an interface its classToInterfaceMap rows. These run first, in
+ /// vtable, or for an interface points its vtable at Object's. These run first, in
/// cn1EagerInitClasses, at the very start of initConstantPool.
/// - A class whose is PURE (see computePureClinits): it reads and writes
/// only its own static fields, constants, string literals and arrays it creates,
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeField.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeField.java
index a37abe13c45..21764cac66f 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeField.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeField.java
@@ -42,6 +42,7 @@ public class ByteCodeField {
private Object value;
private boolean privateField;
private final boolean volatileField;
+ private final String descriptor;
public ByteCodeField(String clsName, int access, String name, String desc, String signature, Object value) {
this.clsName = clsName;
@@ -54,6 +55,7 @@ public ByteCodeField(String clsName, int access, String name, String desc, Strin
finalField = (access & Opcodes.ACC_FINAL) == Opcodes.ACC_FINAL;
volatileField = (access & Opcodes.ACC_VOLATILE) == Opcodes.ACC_VOLATILE;
fieldName = name.replace('$', '_').replace('-', '_');
+ descriptor = desc;
arrayDimensions = 0;
while(desc.startsWith("[")) {
@@ -103,6 +105,11 @@ public ByteCodeField(String clsName, int access, String name, String desc, Strin
}
}
+ /// The JVM descriptor the field was declared with, e.g. `Ljava/lang/Object;`.
+ public String getDescriptor() {
+ return descriptor;
+ }
+
public String getFieldName() {
return fieldName;
}
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
index b2342aa0630..4cd33a985e4 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java
@@ -1079,21 +1079,24 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File
}
}
optionalFrameworks.add("UserNotifications.framework");
- includeFrameworks.add("libiconv.dylib");
+ // Only what the port's natives call. A linked framework is loaded at launch whether or not
+ // anything calls it, with everything it links in turn, so one listed here "in case" costs
+ // every application memory and start-up time. MapKit, GameKit, QuickLook, AddressBook,
+ // libxml2, libz and libiconv were listed here and called by nothing, and loading them cost
+ // a Compose application 11 MB of dyld memory on the simulator, a fifth of its footprint.
+ // A feature that needs a framework adds it when it is used -- the maps provider brings
+ // MapKit (MapsProviderInjector), contacts bring AddressBook, App Attest brings
+ // DeviceCheck -- and a cn1lib declares its own through ios.add_libs.
//includeFrameworks.add("AdSupport.framework");
// AddressBookUI and AddressBook are iOS-only. On macOS the contacts API
- // is Contacts.framework, and linking the iOS ones fails outright rather
- // than merely going unused.
- if (platform.hasIosDeviceIdioms()) {
- includeFrameworks.add("AddressBookUI.framework");
- includeFrameworks.add("AddressBook.framework");
- } else {
+ // is Contacts.framework, and linking the iOS ones fails outright.
+ // On iOS the builder links them with INCLUDE_CONTACTS_USAGE, the define their natives
+ // compile under (IPhoneBuilder, beside ios.add_libs).
+ if (!platform.hasIosDeviceIdioms()) {
includeFrameworks.add("Contacts.framework");
}
includeFrameworks.add("SystemConfiguration.framework");
- includeFrameworks.add("MapKit.framework");
includeFrameworks.add("AudioToolbox.framework");
- includeFrameworks.add("libxml2.dylib");
includeFrameworks.add("QuartzCore.framework");
if (!isBundledSqliteCipherEnabled()) {
// The system libsqlite3 has no cipher support, so an application that encrypts
@@ -1103,7 +1106,6 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File
includeFrameworks.add("libsqlite3.dylib");
includeFrameworks.add("libsqlite3.0.dylib");
}
- includeFrameworks.add("GameKit.framework");
includeFrameworks.add("Security.framework");
//includeFrameworks.add("StoreKit.framework");
// CoreMotion has no macOS implementation -- a Mac has no accelerometer to
@@ -1136,10 +1138,8 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File
includeFrameworks.add("AVFoundation.framework");
includeFrameworks.add("CoreText.framework");
includeFrameworks.add("CoreVideo.framework");
- includeFrameworks.add("QuickLook.framework");
//includeFrameworks.add("iAd.framework");
includeFrameworks.add("CoreMedia.framework");
- includeFrameworks.add("libz.dylib");
includeFrameworks.add("AVKit.framework");
if(!addFrameworks.equalsIgnoreCase("none")) {
includeFrameworks.addAll(Arrays.asList(Util.splitLiteral(addFrameworks, ';')));
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java
index 69be2e92342..0c78cfdaa60 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java
@@ -2413,8 +2413,8 @@ public void appendVirtualMethodC(String cls, StringBuilder b, String offset, boo
b.append("\n#if CN1_TAGGED_ACTIVE\n if(CN1_TAG_CODE(__cn1ThisObject) == CN1_TAG_INTEGER && CN1_TAG_CODE(__cn1Arg1) == CN1_TAG_INTEGER) { return (__cn1ThisObject == __cn1Arg1) ? JAVA_TRUE : JAVA_FALSE; }\n#endif\n ");
}
- // An eagerly initialized interface (no ) had its classToInterfaceMap
- // rows written in initConstantPool before any Java thread existed, so there is
+ // An eagerly initialized interface (no ) had its vtable written in
+ // initConstantPool before any Java thread existed, so there is
// nothing left for this guard to publish -- see
// ByteCodeClass.isEagerInitEligible.
if(includeStaticInitializer && !ByteCodeClass.eagerInit(cls)) {
@@ -2432,9 +2432,9 @@ public void appendVirtualMethodC(String cls, StringBuilder b, String offset, boo
// self time doing nothing but returning.
//
// The flag is the right thing to test and the ordering is not incidental:
- // class__X.initialized is RELEASE-stored after the vtable and the
- // classToInterfaceMap rows are written, and this thunk is about to index
- // exactly those rows -- so the ACQUIRE load here is what makes them visible.
+ // class__X.initialized is RELEASE-stored after the interface's vtable is
+ // written, and this thunk may read it -- so the ACQUIRE load here is what
+ // makes it visible. (The dispatch map itself is constant data.)
// Testing __X_LOADED__ instead would be a weaker gate that publishes
// nothing; that distinction is why the inline guards were moved onto this
// flag in the first place.
@@ -2450,8 +2450,8 @@ public void appendVirtualMethodC(String cls, StringBuilder b, String offset, boo
// CN1_CLASS_OF is not a field load when tagged values are compiled in: it
// masks the pointer, tests the tag and selects between a proxy and the object
// before it loads anything. The interface form of this thunk used it twice --
- // once for the vtable and once for the classId that indexes
- // classToInterfaceMap -- so resolving it once here removes that work from
+ // once for the vtable and once for the classId that indexes the
+ // interface map -- so resolving it once here removes that work from
// every interface dispatch, which is the hottest indirect call the VM makes.
// Skip the tagged resolve where a tagged value cannot arrive: this thunk only
// ever sees objects whose class is its owner or below, and a boxed immediate
@@ -2467,7 +2467,7 @@ public void appendVirtualMethodC(String cls, StringBuilder b, String offset, boo
/* A SWITCH ON THE CLASS ID, ahead of the indirect dispatch below.
*
* The indirect form is six dependent loads -- class pointer, class id, the
- * classToInterfaceMap row, the offset within it, the vtable, the slot -- and
+ * interface map row, the offset within it, the vtable, the slot -- and
* then a branch the C compiler cannot see through, so nothing on either side
* of the call is optimised across it. The switch is ONE load and a jump table,
* and every arm is a named callee that ThinLTO can inline.
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
index b8607cf4680..77b58f23010 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/JavascriptMethodGenerator.java
@@ -1286,6 +1286,18 @@ private static void appendClassRegistration(StringBuilder out, ByteCodeClass cls
}
if (cls.isIsInterface()) {
out.append(" I: 1,\n");
+ // ``D`` marks an interface that declares a default (non-abstract,
+ // non-static) method. JVMS 5.5 initializes such an interface along
+ // with any class implementing it, and the runtime relies on this
+ // flag to do so: the default method's body reads the interface's
+ // own statics without an init guard (see
+ // defaultMethodInterfacesToInit in parparvm_runtime.js).
+ for (BytecodeMethod m : cls.getMethods()) {
+ if (!m.isAbstract() && !m.isStatic() && !m.isEliminated()) {
+ out.append(" D: 1,\n");
+ break;
+ }
+ }
}
if (cls.isIsAbstract()) {
out.append(" A: 1,\n");
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java
index 07e522c1d4f..4b015e95b47 100644
--- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java
@@ -492,6 +492,7 @@ public static void cleanup() {
+ " refused=" + BytecodeMethod.stackIterRefused);
}
LocalReceiverTypes.clear();
+ RuntimeLowering.cleanup();
cn1SubclassIndex = null;
cn1SubclassIndexSize = -1;
nativeSources = null;
@@ -560,19 +561,44 @@ public static void parse(File sourceFile) throws Exception {
try (InputStream in = new FileInputStream(sourceFile)) {
r = new ClassReader(in);
}
+ if (RuntimeLowering.isReplacedStub(r.getClassName())) {
+ // Regenerated from the inputs' service files once everything is parsed.
+ return;
+ }
Parser p = new Parser();
p.clsName = r.getClassName().replace('/', '_').replace('$', '_').replace('-', '_');
p.cls = new ByteCodeClass(p.clsName, r.getClassName());
readingClassDepth++;
try {
- r.accept(p, ClassReader.EXPAND_FRAMES);
+ r.accept(RuntimeLowering.adapt(p), ClassReader.EXPAND_FRAMES);
} finally {
readingClassDepth--;
}
classes.add(p.cls);
}
+
+ /// Writes a class the translator generates itself into a [ClassVisitor], as a
+ /// class file would be read into one.
+ interface SyntheticEmitter {
+ void emit(ClassVisitor cv);
+ }
+
+ /// Parses a generated class exactly as [#parse(File)] parses one read from disk.
+ static void parseSynthetic(String internalName, SyntheticEmitter emitter) {
+ BytecodeMethod.setDependencyGraph(dependencyGraph);
+ Parser p = new Parser();
+ p.clsName = internalName.replace('/', '_').replace('$', '_').replace('-', '_');
+ p.cls = new ByteCodeClass(p.clsName, internalName);
+ readingClassDepth++;
+ try {
+ emitter.emit(p);
+ } finally {
+ readingClassDepth--;
+ }
+ classes.add(p.cls);
+ }
private static ByteCodeClass getClassByName(String name) {
return classIndex().get(name.replace('/', '_').replace('$', '_').replace('-', '_'));
@@ -1390,6 +1416,9 @@ private static String fourChars(String s) {
}
public static void writeOutput(File outputDirectory) throws Exception {
+ // Field updaters and service providers: classes generated from what the parse
+ // found, which every later pass has to see as if they had been read.
+ RuntimeLowering.writeSynthetics(outputDirectory);
// Must run before anything is emitted: the instanceof call sites bake the
// dense bit index in, so the assignment has to exist before the first one.
collectTypeTests();
@@ -1643,6 +1672,7 @@ public static void writeOutput(File outputDirectory) throws Exception {
verifyNativeMethods(handWrittenNativeSources);
generateClassAndMethodIndexHeader(outputDirectory);
+ RuntimeLowering.pruneCulledSynthetics(outputDirectory);
boolean concatenate = "true".equals(Util.getProperty("concatenateFiles", "false"));
ConcatenatingFileOutputStream cos = concatenate ? new ConcatenatingFileOutputStream(outputDirectory) : null;
diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RuntimeLowering.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RuntimeLowering.java
new file mode 100644
index 00000000000..48fe43b5ca4
--- /dev/null
+++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/RuntimeLowering.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.tools.translator;
+
+import com.codename1.tools.translator.classfile.ClassVisitor;
+import com.codename1.tools.translator.classfile.Label;
+import com.codename1.tools.translator.classfile.MethodVisitor;
+import com.codename1.tools.translator.classfile.Opcodes;
+import com.codename1.tools.translator.classfile.Type;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/// The two JDK facilities a device cannot provide at run time, provided at build time.
+///
+/// Both are reflection in disguise, and both are everywhere in Kotlin libraries:
+/// kotlinx.coroutines, the Compose runtime, atomicfu and Ktor are built on them.
+///
+/// #### Field updaters
+///
+/// `AtomicReferenceFieldUpdater.newUpdater(Owner.class, V.class, "field")` (and the
+/// `Integer` and `Long` forms) finds a field by name. Every compiler emits the
+/// arguments as constants right before the call, so [#adapt] recognises the three (or
+/// two) `ldc` instructions and the `invokestatic`, and replaces them with
+/// `new Owner$cn1fu$field()`: a subclass of the updater that [#writeSynthetics]
+/// generates for that one field. Its `get` and `set` are a plain `getfield` and
+/// `putfield`, which on a volatile field are already atomic. Its `compareAndSet`
+/// calls a static `cas0`, which is:
+///
+/// - on a native target, a native whose C body is generated here -- a C11 compare and
+/// exchange on the struct member, behind the same SATB and generational barriers
+/// the generated setter applies. A lock would not do: code that uses an updater
+/// also writes the field directly (atomicfu lowers `value = x` to a volatile
+/// store), and only a hardware CAS is atomic with respect to that store;
+/// - on JavaScript, branch-free bytecode: every thread there runs on one host thread,
+/// so nothing can run between the read and the write.
+///
+/// A call whose arguments are not constants is left alone, and reaches the JavaAPI
+/// method, which throws.
+///
+/// #### Services
+///
+/// `java.util.ServiceLoader` cannot search a class path. [#recordResource] collects
+/// every `META-INF/services` file among the translated inputs, and the generated
+/// `java.util.ServiceLoader$Registry.providers(Class)` instantiates the providers each
+/// names, in order. The JavaAPI stub of that class is skipped when parsing
+/// ([#isReplacedStub]) and always regenerated, empty for an application that has no
+/// service files.
+final class RuntimeLowering {
+ static final String REGISTRY = "java/util/ServiceLoader$Registry";
+ private static final String REF_UPDATER = "java/util/concurrent/atomic/AtomicReferenceFieldUpdater";
+ private static final String INT_UPDATER = "java/util/concurrent/atomic/AtomicIntegerFieldUpdater";
+ private static final String LONG_UPDATER = "java/util/concurrent/atomic/AtomicLongFieldUpdater";
+ private static final String GENERATED_C = "cn1_runtime_lowering";
+ private static final int CLASS_VERSION_8 = 52;
+
+ /// Every field an updater was resolved for, keyed by the generated class name.
+ private static final Map UPDATERS = new TreeMap();
+ /// Service interface (binary name) to provider class names, in file order.
+ private static final Map> SERVICES = new TreeMap>();
+
+ private RuntimeLowering() {
+ }
+
+ static void cleanup() {
+ UPDATERS.clear();
+ SERVICES.clear();
+ CAS_BODIES.clear();
+ }
+
+ static boolean isReplacedStub(String internalName) {
+ return REGISTRY.equals(internalName);
+ }
+
+ /// One resolved updater: the generated class, the class named as the owner, and
+ /// the field.
+ private static final class Updater {
+ final String generated;
+ final String owner;
+ final String field;
+ final String updaterClass;
+ /// `I`, `J` or `L` -- the updater's kind, not the field's declared type.
+ final char kind;
+
+ Updater(String generated, String owner, String field, String updaterClass, char kind) {
+ this.generated = generated;
+ this.owner = owner;
+ this.field = field;
+ this.updaterClass = updaterClass;
+ this.kind = kind;
+ }
+ }
+
+ /// Wraps the parser's visitor so updater call sites are rewritten as they are read.
+ static ClassVisitor adapt(ClassVisitor parser) {
+ return new ClassVisitor(parser) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+ MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
+ return mv == null ? null : new CallSites(mv);
+ }
+ };
+ }
+
+ /// Holds back trailing `ldc` instructions until it knows whether a `newUpdater`
+ /// follows them.
+ private static final class CallSites extends MethodVisitor {
+ private final List
+
+
+
+ google
+ https://maven.google.com
+
+
+
@@ -107,6 +116,53 @@
test
+
+
+ copy-kmp-runtime
+ generate-test-resources
+
+ copy
+
+
+ ${project.build.directory}/kmp-runtime
+
+ org.jetbrains.kotlinkotlin-stdlib2.4.21
+ org.jetbrains.kotlinxkotlinx-coroutines-core-jvm1.11.0
+ androidx.compose.runtimeruntime-desktop1.12.1
+ androidx.compose.runtimeruntime-annotation-jvm1.12.1
+ androidx.collectioncollection-jvm1.5.0
+ androidx.annotationannotation-jvm1.9.1
+ org.jetbrainsannotations23.0.0
+
+
+
+
+ copy-kmp-compiler
+ generate-test-resources
+
+ copy
+
+
+ ${project.build.directory}/kmp-compiler
+
+ org.jetbrains.kotlinkotlin-compiler-embeddable2.4.21
+ org.jetbrains.kotlinkotlin-compose-compiler-plugin-embeddable2.4.21
+ org.jetbrains.kotlinkotlin-stdlib2.4.21
+ org.jetbrains.kotlinkotlin-script-runtime2.4.21
+ org.jetbrains.kotlinkotlin-daemon-embeddable2.4.21
+ org.jetbrains.kotlinkotlin-build-tools-api2.4.21
+ org.jetbrains.kotlinxkotlinx-coroutines-core-jvm1.8.0
+ org.jetbrainsannotations13.0
+ org.jetbrains.kotlinkotlin-reflect1.6.10
+
+
+