mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 10:47:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30cbbc4be1 | ||
|
|
4a196c9c23 | ||
|
|
a62a17a7bf | ||
|
|
b0530cdccb | ||
|
|
e483a4b683 | ||
|
|
23eaa5bc8b | ||
|
|
18286f689b | ||
|
|
319c550af4 | ||
|
|
c7608b6404 | ||
|
|
c71febbb23 | ||
|
|
5ab3bd6b97 | ||
|
|
c59caf7272 | ||
|
|
c36f11ab4f | ||
|
|
7189433e5b | ||
|
|
dadb9e45d4 | ||
|
|
c1fabc5b5c | ||
|
|
5def89f182 | ||
|
|
4bd9ca234e | ||
|
|
924229fafe | ||
|
|
2314fca8ee | ||
|
|
2210d1fa01 | ||
|
|
746d3cca9c | ||
|
|
7f5773fc3c | ||
|
|
90f56143c4 | ||
|
|
ab4b82e492 | ||
|
|
4a3d8e5ca2 | ||
|
|
7f7f57d06d | ||
|
|
c45aed105c | ||
|
|
7e829ef87b | ||
|
|
cc9940c705 | ||
|
|
bb0b5a9a27 | ||
|
|
5686e8785f | ||
|
|
3da62b654e | ||
|
|
b6fb974264 | ||
|
|
07bfd84810 | ||
|
|
2db54e0e19 | ||
|
|
a2252a2ede | ||
|
|
fcc581d48b | ||
|
|
ec3bc51346 | ||
|
|
08a14d79cb | ||
|
|
812063a320 | ||
|
|
983cafc9b9 | ||
|
|
5e05f0d354 | ||
|
|
73d98a3dc3 | ||
|
|
7a78e5e7a6 | ||
|
|
f6164e7cf0 | ||
|
|
43f88c7866 | ||
|
|
70602e4c91 | ||
|
|
653f32e08c | ||
|
|
a98068ba09 | ||
|
|
b2a23d60b8 | ||
|
|
8fddb0a94c | ||
|
|
15e12ec2b9 | ||
|
|
c5945f1a94 | ||
|
|
d4cb543090 | ||
|
|
63b430873d | ||
|
|
3a584bbe8b | ||
|
|
2873df25ae | ||
|
|
bff6b484e5 | ||
|
|
4371335b37 | ||
|
|
468c347357 | ||
|
|
65503e60b8 | ||
|
|
a832de5ce1 | ||
|
|
5caef6b2b3 | ||
|
|
b50081014e | ||
|
|
5dc3a4525a | ||
|
|
716a005da6 | ||
|
|
5890dd0fe3 | ||
|
|
29856d9bea | ||
|
|
5184d3ec24 | ||
|
|
0610460ebb | ||
|
|
8081fc13f6 | ||
|
|
3fec62fbc2 | ||
|
|
3f76f77e5b | ||
|
|
1f33506c13 | ||
|
|
44e09aa89f | ||
|
|
0870cd923a | ||
|
|
49b14337eb | ||
|
|
74f186136b | ||
|
|
bbaa04354c | ||
|
|
86c97cbad1 | ||
|
|
5a05663050 | ||
|
|
ebe1d67620 | ||
|
|
7eaebeb787 | ||
|
|
2f1f7d5ba5 | ||
|
|
92454ae8a1 | ||
|
|
aad70f342b | ||
|
|
cf597c12e0 | ||
|
|
cbddf8ca0b | ||
|
|
ad738a643e | ||
|
|
8f5c59bd64 | ||
|
|
914bab840d | ||
|
|
8f96ba9478 | ||
|
|
5a9e7b0f7f | ||
|
|
5dfd9200f8 | ||
|
|
8c7168a1a9 | ||
|
|
377f3aa775 | ||
|
|
662975c802 | ||
|
|
3b420a7bad | ||
|
|
2dad6c9bcc | ||
|
|
723f80c8af | ||
|
|
450247175f | ||
|
|
5d6f772a1c | ||
|
|
575c19f921 | ||
|
|
328663b9e7 | ||
|
|
c9ddd3488d | ||
|
|
9958386792 | ||
|
|
f5fae6d32e | ||
|
|
b94f1f7bbb | ||
|
|
ffbb87eddd | ||
|
|
63fad4083f | ||
|
|
872d740ce7 | ||
|
|
4c2d46429d | ||
|
|
120abbad93 | ||
|
|
a39002f1b8 | ||
|
|
fb470ecf0c | ||
|
|
2b6ac65a1f | ||
|
|
1b2e83a155 | ||
|
|
8a6cb868d9 | ||
|
|
4a7080b313 | ||
|
|
1afb13aae0 | ||
|
|
3d519bcfcd | ||
|
|
72287fc995 | ||
|
|
0b4a170910 | ||
|
|
72e6f145b3 | ||
|
|
367ed94c01 | ||
|
|
fa9bd93ada | ||
|
|
d1551bc9c1 | ||
|
|
e9527ee8c2 | ||
|
|
6040816cf7 | ||
|
|
6f3bc915b2 | ||
|
|
1ba84a3a2c | ||
|
|
e807ecf973 | ||
|
|
dc35f5bd37 | ||
|
|
4888fdcda0 | ||
|
|
85e0179590 |
@@ -18,9 +18,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# Each host runs only the publish tasks Gradle actually generated for it.
|
# Each host runs only the publish tasks Gradle actually generated for it.
|
||||||
# macOS also publishes the shared kotlinMultiplatform metadata module.
|
# WINDOWS also publishes the shared kotlinMultiplatform metadata modules
|
||||||
# No library module declares jvm() today — apps do their own JVM parity
|
# (see its matrix entry) + the jvm jar + the bridge plugin.
|
||||||
# build against upstream Compose, so JVM isn't part of the publication.
|
|
||||||
# `target` is the Gradle K/N target token (Capitalised) — one per host,
|
# `target` is the Gradle K/N target token (Capitalised) — one per host,
|
||||||
# used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is
|
# used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is
|
||||||
# skipped for the app build because SDL3 static libs on the ubuntu runner
|
# skipped for the app build because SDL3 static libs on the ubuntu runner
|
||||||
@@ -32,7 +31,6 @@ jobs:
|
|||||||
target: MacosArm64
|
target: MacosArm64
|
||||||
exe_ext: kexe
|
exe_ext: kexe
|
||||||
tasks: >-
|
tasks: >-
|
||||||
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
|
|
||||||
publishMacosArm64PublicationToGitHubPackagesRepository
|
publishMacosArm64PublicationToGitHubPackagesRepository
|
||||||
- runner: ubuntu-22.04
|
- runner: ubuntu-22.04
|
||||||
host: linux
|
host: linux
|
||||||
@@ -45,8 +43,18 @@ jobs:
|
|||||||
host: windows
|
host: windows
|
||||||
target: MingwX64
|
target: MingwX64
|
||||||
exe_ext: exe
|
exe_ext: exe
|
||||||
|
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is
|
||||||
|
# the ONLY host that declares every target (vHostSupportsMingw is
|
||||||
|
# host-gated), so only its generated .module files carry the full
|
||||||
|
# variant table — the macOS-published roots of v0.1.15 had NO
|
||||||
|
# mingwX64 variants and Windows consumers could not resolve them.
|
||||||
|
# The host-independent jvm jar (:material-symbols) and the bridge
|
||||||
|
# plugin ride along on the same job.
|
||||||
tasks: >-
|
tasks: >-
|
||||||
publishMingwX64PublicationToGitHubPackagesRepository
|
publishMingwX64PublicationToGitHubPackagesRepository
|
||||||
|
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
|
||||||
|
publishJvmPublicationToGitHubPackagesRepository
|
||||||
|
:compose-desktop-native-bridge:publishAllPublicationsToGitHubPackagesRepository
|
||||||
|
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
@@ -95,23 +103,33 @@ jobs:
|
|||||||
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
|
# intrinsic K/N's LLD can't resolve is aliased to msvcrt's _setjmpex at
|
||||||
# link time via --defsym in sdl3.def.
|
# link time via --defsym in sdl3.def.
|
||||||
|
|
||||||
# Build static SDL3 / SDL3_ttf / SDL3_image / FreeType from
|
# Build the static native deps from scripts/build-sdl/build-sdl.properties.
|
||||||
# scripts/build-sdl/build-sdl.properties. Cached against the manifest +
|
# Only Windows ships the SDL3 renderer (no Skiko on mingw), so only it needs
|
||||||
# scripts so version bumps invalidate cleanly.
|
# SDL3_ttf / SDL3_image / FreeType (SDL3_ttf text, SDL3_image decode, FreeType
|
||||||
# `cache_version` is a manual bump for when the toolchain (runner OS,
|
# icon-axis rasterisation). macOS / Linux publish the Skia renderer — Skiko
|
||||||
# compiler flavour, K/N sysroot expectations) changes without any
|
# handles text / images / icons — so they only need base SDL3 for the main
|
||||||
# scripts/build-sdl/ file changing. Increment to force a rebuild.
|
# loop (windowing / input / audio / filesystem). Matches the cinterop guard
|
||||||
|
# in compose/ui/ui/build.gradle.kts, which skips those three cinterops on the
|
||||||
|
# Skia targets. Cached against the manifest + scripts so version bumps
|
||||||
|
# invalidate cleanly; bump the `v3` key when the toolchain (runner OS,
|
||||||
|
# compiler flavour, K/N sysroot) changes without a scripts/build-sdl/ edit.
|
||||||
- name: Cache libs/
|
- name: Cache libs/
|
||||||
id: cache-libs
|
id: cache-libs
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: libs
|
path: libs
|
||||||
key: sdl-libs-v2-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
|
key: sdl-libs-v3-${{ matrix.host }}-${{ hashFiles('scripts/build-sdl/**') }}
|
||||||
|
|
||||||
- name: Build static SDL / TTF / image / FreeType
|
# Windows: full SDL3 renderer stack. macOS / Linux (Skia): base SDL3 only.
|
||||||
|
- name: Build static native deps
|
||||||
if: steps.cache-libs.outputs.cache-hit != 'true'
|
if: steps.cache-libs.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: bash scripts/build-sdl/build-all.sh
|
run: |
|
||||||
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
|
python3 scripts/build-sdl/build-all.py
|
||||||
|
else
|
||||||
|
python3 scripts/build-sdl/build-all.py sdl3
|
||||||
|
fi
|
||||||
|
|
||||||
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
|
# src/vendor/ trees are gitignored — a fresh checkout has zero vendored
|
||||||
# androidx.compose.* files. Sync copies them in from the upstream refs
|
# androidx.compose.* files. Sync copies them in from the upstream refs
|
||||||
@@ -121,16 +139,65 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: bash scripts/compose-fork/sync.sh
|
run: bash scripts/compose-fork/sync.sh
|
||||||
|
|
||||||
# GitHub Packages sometimes returns 409 Conflict on a fresh upload when
|
# GitHub Packages FORBIDS overwriting a file that already exists in a
|
||||||
# the same coordinate was recently deleted (soft-delete propagation).
|
# version, and maven-publish uploads each publication's files
|
||||||
# `--continue` lets Gradle finish the rest of the publish tasks past a
|
# SEQUENTIALLY from the first one. So when an upload dies mid-publication
|
||||||
# single 409, and the outer retry re-runs the whole thing (Gradle skips
|
# (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409
|
||||||
# already-uploaded artifacts). Two attempts is enough for the transient
|
# Conflict on the first already-uploaded file and aborts BEFORE reaching
|
||||||
# cases we've hit; the third pass falls through as a hard failure.
|
# the missing tail files — the version is poisoned and can never
|
||||||
|
# complete. A 409 is therefore only benign when the publication had
|
||||||
|
# already completed; you can't tell from the 409 alone. The fix: between
|
||||||
|
# attempts, DELETE this host's package versions for the current release
|
||||||
|
# and re-upload from a clean slate. Each host owns DISJOINT package
|
||||||
|
# names (roots / -mingwx64 / -jvm / bridge on Windows, -macosarm64 on
|
||||||
|
# macOS, -linuxx64/-linuxarm64 on Linux), so parallel jobs never touch
|
||||||
|
# each other's uploads. Uses the run's GITHUB_TOKEN (packages: write);
|
||||||
|
# a 403 on delete just degrades to the old blind-retry behaviour.
|
||||||
- name: Publish to GitHub Packages
|
- name: Publish to GitHub Packages
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
VER="${PUBLISH_VERSION#v}"
|
||||||
|
OWNER="${GITHUB_REPOSITORY%%/*}"
|
||||||
|
# Keep in sync with the module list in settings.gradle.kts.
|
||||||
|
MODULES="ui ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple window material-symbols navigation3-ui components-resources"
|
||||||
|
case "${{ matrix.host }}" in
|
||||||
|
macos) SUFFIXES="-macosarm64" ;;
|
||||||
|
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
||||||
|
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
||||||
|
esac
|
||||||
|
host_packages() {
|
||||||
|
for m in $MODULES; do
|
||||||
|
for s in $SUFFIXES; do
|
||||||
|
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
|
echo "com.bitsycore.compose.sdl.material-symbols-jvm"
|
||||||
|
echo "com.bitsycore.compose.sdl.compose-desktop-native-bridge"
|
||||||
|
echo "com.bitsycore.compose-desktop-native.bridge.com.bitsycore.compose-desktop-native.bridge.gradle.plugin"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
cleanup_host_versions() {
|
||||||
|
echo "Deleting this host's partially-published $VER package versions"
|
||||||
|
host_packages | while read -r pkg; do
|
||||||
|
vid=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
|
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions?per_page=100" \
|
||||||
|
| jq -r --arg v "$VER" '[.[]? | select(.name==$v) | .id] | first // empty')
|
||||||
|
[ -z "$vid" ] && continue
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
|
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
|
||||||
|
if [ "$code" != "204" ]; then
|
||||||
|
# Deleting the LAST version of a package is refused (400) —
|
||||||
|
# a package new in this release: drop the whole package.
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
|
"https://api.github.com/users/$OWNER/packages/maven/$pkg")
|
||||||
|
fi
|
||||||
|
echo " $pkg@$VER -> HTTP $code"
|
||||||
|
done
|
||||||
|
}
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "::group::Publish attempt $attempt"
|
echo "::group::Publish attempt $attempt"
|
||||||
if ./gradlew ${{ matrix.tasks }} --continue --no-daemon --stacktrace; then
|
if ./gradlew ${{ matrix.tasks }} --continue --no-daemon --stacktrace; then
|
||||||
@@ -138,7 +205,11 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
[ $attempt -lt 3 ] && { echo "::warning::Publish attempt $attempt failed — sleeping 60s and retrying"; sleep 60; }
|
if [ $attempt -lt 3 ]; then
|
||||||
|
echo "::warning::Publish attempt $attempt failed — cleaning this host's $VER packages and retrying"
|
||||||
|
cleanup_host_versions || true
|
||||||
|
sleep 30
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
echo "::error::Publish failed after 3 attempts"
|
echo "::error::Publish failed after 3 attempts"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -167,15 +238,11 @@ jobs:
|
|||||||
target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}')
|
target_dir=$(echo "${{ matrix.target }}" | awk '{print tolower(substr($0,1,1)) substr($0,2)}')
|
||||||
version="${GITHUB_REF_NAME#v}"
|
version="${GITHUB_REF_NAME#v}"
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
# Windows Git Bash has no `zip`; every runner has JDK 21 → use `jar cf`
|
|
||||||
# which writes a valid ZIP file. `-C dir file` = enter dir then add
|
|
||||||
# `file` at the archive root, so the archive stays flat like `zip -j`.
|
|
||||||
for app in demo apidemo; do
|
for app in demo apidemo; do
|
||||||
src="$app/build/bin/$target_dir/releaseExecutable"
|
src="$app/build/bin/$target_dir/releaseExecutable"
|
||||||
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
|
zip_path="$(pwd)/dist/$app-$version-$target_dir.zip"
|
||||||
jar --create --no-manifest --file "$zip_path" \
|
python3 -c 'import sys,os,zipfile; z=zipfile.ZipFile(sys.argv[1],"w",zipfile.ZIP_DEFLATED,compresslevel=9); [z.write(f,os.path.basename(f)) for f in sys.argv[2:]]; z.close()' \
|
||||||
-C "$src" "$app.${{ matrix.exe_ext }}" \
|
"$zip_path" "$src/$app.${{ matrix.exe_ext }}" "$src/data.kres"
|
||||||
-C "$src" "data.kres"
|
|
||||||
done
|
done
|
||||||
ls -la dist/
|
ls -la dist/
|
||||||
|
|
||||||
|
|||||||
@@ -61,3 +61,6 @@ bin/
|
|||||||
### Python ###
|
### Python ###
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
|
# Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest
|
||||||
|
/*.bmp
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
Guidance for Claude Code working in this repository. This is the primary
|
Guidance for Claude Code working in this repository. This is the primary
|
||||||
context — read it first, then look at the files it points to.
|
context — read it first, then look at the files it points to.
|
||||||
|
|
||||||
|
## Documentation map
|
||||||
|
|
||||||
|
- [README.md](README.md) — public overview + quickstart (bridge plugin,
|
||||||
|
`nativeComposeWindow`, sample apps).
|
||||||
|
- [RENDERER.md](RENDERER.md) — the rendering layer: architecture, the retained-
|
||||||
|
layer engine, convergence decisions, learnings, and open work. Read it before
|
||||||
|
touching any renderer, graphics-actual, or layer-engine code.
|
||||||
|
- [TOOLING.md](TOOLING.md) — build/vendor/verify scripts and workflows
|
||||||
|
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
|
||||||
|
verify-mac) + the version map and the ref-bump / release runbooks.
|
||||||
|
- [TODO.md](TODO.md) — audited list of no-ops, stubs, and hardcodes left in the
|
||||||
|
port, with severity, plus what's missing for a stable 1.12 release.
|
||||||
|
- This file — architecture, module layout, vendoring rules, source-set
|
||||||
|
hierarchy, density flow, conventions, and common pitfalls.
|
||||||
|
|
||||||
## What this project is
|
## What this project is
|
||||||
|
|
||||||
**ComposeNativeSDL3** — a Kotlin/Native port of Compose Multiplatform running
|
**ComposeNativeSDL3** — a Kotlin/Native port of Compose Multiplatform running
|
||||||
@@ -42,7 +57,12 @@ compose/
|
|||||||
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
||||||
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
||||||
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
||||||
│ └── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
|
│ ├── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
|
||||||
|
│ └── ui-tooling-preview/ → :ui-tooling-preview — androidx.compose.ui.tooling.preview.*
|
||||||
|
│ (the common @Preview + PreviewParameterProvider,
|
||||||
|
│ vendored verbatim; the Maven artifact ships no
|
||||||
|
│ mingwX64/linux klibs). IDE-only metadata — previews
|
||||||
|
│ render through the apps' jvm parity targets.
|
||||||
├── animation/
|
├── animation/
|
||||||
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.*
|
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.*
|
||||||
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core)
|
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core)
|
||||||
@@ -60,8 +80,21 @@ compose/
|
|||||||
|
|
||||||
utils/
|
utils/
|
||||||
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
||||||
(Outlined / Rounded / Sharp). Apps get one dep;
|
(Outlined / Rounded / Sharp). COMMON API (usable from
|
||||||
the consumer Zip task bundles only the fonts used.
|
shared app code) + per-stack actuals: native renders
|
||||||
|
via :foundation IconFontIcon (IconFont handles the
|
||||||
|
SDL3/Skia split), jvm() via Skiko directly
|
||||||
|
(Typeface.makeClone per axes — upstream's FontCache
|
||||||
|
drops variationSettings from its key). Its commonMain
|
||||||
|
declares official Maven compose coords — the root
|
||||||
|
build's FULL-COMMONIZATION BRIDGE substitutes the
|
||||||
|
whole ui / foundation / animation / material3 /
|
||||||
|
nav3-ui family to project modules on native configs
|
||||||
|
(:demo and :apidemo commonMains rely on the same
|
||||||
|
bridge; :compose-desktop-native-bridge ships it to
|
||||||
|
consumers). Apps get one dep; the consumer Zip task
|
||||||
|
bundles only the fonts used (native) and
|
||||||
|
jvmProcessResources stages the same fonts (jvm).
|
||||||
|
|
||||||
components/
|
components/
|
||||||
└── resources/library/ → :components-resources — the OFFICIAL Compose resources
|
└── resources/library/ → :components-resources — the OFFICIAL Compose resources
|
||||||
@@ -93,12 +126,28 @@ demo/ → :demo — flagship showcase app (30+ screens) + th
|
|||||||
MULTIPLATFORM: also has a jvm() target running the SAME shared
|
MULTIPLATFORM: also has a jvm() target running the SAME shared
|
||||||
screens on stock JVM Compose Desktop (`./gradlew :demo:run`,
|
screens on stock JVM Compose Desktop (`./gradlew :demo:run`,
|
||||||
MainJvmKt) — the parity reference; differences vs native = port bugs
|
MainJvmKt) — the parity reference; differences vs native = port bugs
|
||||||
apidemo/ → :apidemo — Postman-style REST API manager
|
apidemo/ → :apidemo — Postman-style REST API manager. MULTIPLATFORM like :demo
|
||||||
scripts/ → vendor-sync + python helper scripts (compose-fidelity/coverage,
|
(`./gradlew :apidemo:run`): the whole UI lives in commonMain
|
||||||
material-symbols generate/subset) + compose-fork/;
|
against the official Maven coords; SDL-backed APIs go through
|
||||||
scripts/build-sdl/ = Windows static-lib build scripts (bash)
|
expect/actual seams (compat/Compat.kt — native actuals delegate
|
||||||
|
to com.compose.sdl, jvm actuals use AWT + upstream desktop).
|
||||||
|
mTLS / TLS-chain inspection stays native-only (bundled libcurl).
|
||||||
|
gradle-plugin/
|
||||||
|
└── compose-desktop-native-bridge/ → :compose-desktop-native-bridge — the CONSUMER-side bridge as a
|
||||||
|
published Gradle plugin (id com.bitsycore.compose-desktop-native
|
||||||
|
.bridge, applies to Settings or Project): substitution rules
|
||||||
|
can't ship inside Maven artifacts, so third-party apps apply the
|
||||||
|
plugin, declare OFFICIAL CMP coords in commonMain, and native
|
||||||
|
configurations swap in the published com.bitsycore.compose.sdl
|
||||||
|
klibs. Substituted version defaults to the plugin's own
|
||||||
|
(override: composeDesktopNative.version property). Published by
|
||||||
|
the macOS publish job.
|
||||||
|
scripts/ → vendor-sync + python helper scripts (compose-coverage = API
|
||||||
|
coverage/fidelity vs upstream, material-symbols generate/subset)
|
||||||
|
+ compose-fork/;
|
||||||
|
scripts/build-sdl/ = static-lib build script (python)
|
||||||
libs/ → gitignored per-host static SDL3 / SDL3_ttf / SDL3_image / FreeType
|
libs/ → gitignored per-host static SDL3 / SDL3_ttf / SDL3_image / FreeType
|
||||||
output of scripts/build-sdl/build-*.sh on Windows
|
output of scripts/build-sdl/build-all.py on Windows
|
||||||
```
|
```
|
||||||
|
|
||||||
Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) —
|
Module PATHS stay short (`:ui`, `:foundation`, `:window`, …) —
|
||||||
@@ -194,10 +243,23 @@ Two categories of code live in each module:
|
|||||||
NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding
|
NON-IDEMPOTENT.** Move it OUT of `src/vendor/` into the corresponding
|
||||||
`src/{commonMain,nativeMain,…}` tree, add a header comment noting
|
`src/{commonMain,nativeMain,…}` tree, add a header comment noting
|
||||||
which upstream file it derived from and what changed, and comment
|
which upstream file it derived from and what changed, and comment
|
||||||
its line out of `compose-fork.txt`. Now it's a project file — the
|
its line out of `compose-fork.txt`. **ALWAYS also add the
|
||||||
next sync won't overwrite it, and future upstream changes to that
|
machine-readable provenance line** so the copy can be diffed against
|
||||||
file need to be reconciled by hand. This is fine; do it when the
|
its base and against later upstream versions:
|
||||||
edit is small and the file is unlikely to churn upstream.
|
|
||||||
|
```
|
||||||
|
// VENDOR-BASE: <upstream-repo-relative-path> @ <pinned-tag>
|
||||||
|
// VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/compose-fork/check-vendor-drift.py` reads these at every ref
|
||||||
|
bump: it flags any file whose recorded base lags the current pin and
|
||||||
|
(with the local clone) reports whether the upstream base ACTUALLY
|
||||||
|
changed base..pin — i.e. whether the copy needs hand-reconciling or
|
||||||
|
just a ref re-stamp. Now it's a project file — the next sync won't
|
||||||
|
overwrite it, and future upstream changes to that file need to be
|
||||||
|
reconciled by hand. This is fine; do it when the edit is small and
|
||||||
|
the file is unlikely to churn upstream.
|
||||||
|
|
||||||
4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`, with
|
4. **Skiko-specific things go in `:ui/src/skikoRendererMain/`, with
|
||||||
an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream
|
an SDL3 equivalent in `:ui/src/sdlRendererMain/`.** When upstream
|
||||||
@@ -241,11 +303,15 @@ commonMain
|
|||||||
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
|
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
|
||||||
```
|
```
|
||||||
|
|
||||||
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are declared identically
|
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are `expect`s in
|
||||||
in both `skikoRendererMain` and `sdlRendererMain`. `:window` calls them straight
|
`:ui`'s nativeMain with `actual`s in BOTH `skikoRendererMain` and
|
||||||
from `:ui` — no `expect`/`actual`, no factory layer — and the right impl
|
`sdlRendererMain` — unambiguous because **only one of the two renderer source
|
||||||
resolves because **only one of the two renderer source sets is attached to a
|
sets is attached to a given target**. (They used to be plain duplicate
|
||||||
given target**. Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
|
declarations with no expect; that compiled per-target but shared nativeMain
|
||||||
|
METADATA couldn't see them on a host whose targets span both renderers, which
|
||||||
|
blocked the WINDOWS host from producing :window's KotlinMultiplatform
|
||||||
|
publication — and Windows must publish the root modules, see the publish
|
||||||
|
workflow.) Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
|
||||||
**not even created**, so Gradle has nothing to warn about and Skiko is never
|
**not even created**, so Gradle has nothing to warn about and Skiko is never
|
||||||
pulled in.
|
pulled in.
|
||||||
|
|
||||||
@@ -307,26 +373,19 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|||||||
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
|
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
|
||||||
|
|
||||||
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
|
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
|
||||||
# demo screens on upstream Compose; differences vs the native build = port bugs.
|
# screens on upstream Compose; differences vs the native build = port bugs.
|
||||||
./gradlew :demo:run
|
./gradlew :demo:run
|
||||||
|
./gradlew :apidemo:run
|
||||||
```
|
```
|
||||||
|
|
||||||
### System dependencies
|
### System dependencies
|
||||||
|
|
||||||
**macOS (default Skia build):** `brew install sdl3`. Only need `sdl3_ttf`
|
SDL3 + SDL3_ttf + SDL3_image + FreeType (+ image codecs) are **built from
|
||||||
if you set `-Prenderer=sdl3`. Skiko klibs come from Maven.
|
source as static libraries on every OS** and linked straight into the
|
||||||
|
executable — no brew/apt SDL packages, no runtime .dll/.so/.dylib; a
|
||||||
**Linux (default Skia build):** `sudo apt install libsdl3-dev`. Same caveat
|
distributable is `<app>` + `data.kres`. Build them once per host with
|
||||||
for SDL3_ttf.
|
`python scripts/build-sdl/build-all.py`. Per-host toolchain requirements and
|
||||||
|
the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
|
||||||
**Windows (mingwX64 — always uses SDL3 + SDL3_ttf + SDL3_image + FreeType):**
|
|
||||||
these four libraries + image codecs are **linked statically into the
|
|
||||||
executable** — no runtime DLLs, distributable is just `<app>.exe` +
|
|
||||||
`data.kres`. They're not downloaded — `scripts/build-sdl/build-all.sh` (from Git
|
|
||||||
Bash) builds them from source as static libs into a gitignored, in-repo `libs/`
|
|
||||||
folder. Needs: git, cmake, mingw-w64 gcc/g++ on PATH, plus curl + python for
|
|
||||||
ninja fetch. `scripts/build-sdl/build-all.sh` = `build-freetype.sh` → `build-sdl3.sh` →
|
|
||||||
`build-sdl3-image.sh` → `build-sdl3-ttf.sh`.
|
|
||||||
|
|
||||||
## Runtime bundling — data.kres
|
## Runtime bundling — data.kres
|
||||||
|
|
||||||
@@ -352,19 +411,13 @@ executable, loaded via `SDL_GetBasePath()`). Contents:
|
|||||||
```bash
|
```bash
|
||||||
# Sync every module's compose-fork.txt against the pinned upstream ref.
|
# Sync every module's compose-fork.txt against the pinned upstream ref.
|
||||||
scripts/compose-fork/sync.sh
|
scripts/compose-fork/sync.sh
|
||||||
|
|
||||||
# Sync one module by direct manifest path (module names use ':' → '/' → skip
|
|
||||||
# 'compose/' prefix — nested modules like compose/sdl/window pass the full path).
|
|
||||||
scripts/compose-fork/sync.sh compose/ui/compose-fork.txt
|
|
||||||
|
|
||||||
# Re-format a manifest (align columns, group by upstream folder).
|
|
||||||
scripts/compose-fork/format-manifest.py --discover ../cmp-ref \
|
|
||||||
--manifest compose/ui/compose-fork.txt
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Upstream ref: `scripts/compose-fork/compose.properties` — set to a specific commit
|
Upstream ref: `scripts/compose-fork/compose.properties` — set to a durable tag
|
||||||
of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the
|
of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the
|
||||||
build tell you what broke.
|
build tell you what broke. Per-module sync, manifest re-formatting, and the
|
||||||
|
drift / vendor-clean guardrails are in
|
||||||
|
[TOOLING.md](TOOLING.md#vendoring-upstream-compose).
|
||||||
|
|
||||||
## Key files by area — start here when you need to find something
|
## Key files by area — start here when you need to find something
|
||||||
|
|
||||||
@@ -418,6 +471,21 @@ build tell you what broke.
|
|||||||
`Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't
|
`Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't
|
||||||
ship drop-in equivalents for our anchor / scrim patterns).
|
ship drop-in equivalents for our anchor / scrim patterns).
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
The full tooling reference is [TOOLING.md](TOOLING.md): the static-lib build,
|
||||||
|
vendor sync + drift/clean guardrails, API coverage, the `verify-mac` runbook,
|
||||||
|
the native-vs-JVM parity harness, the interaction probe, and the frame
|
||||||
|
profiler. Quick rules of thumb:
|
||||||
|
|
||||||
|
- Whole-project renderer/layout change → run **parity** (`scripts/parity/parity.py`),
|
||||||
|
the broad net.
|
||||||
|
- Chasing one reported interaction → run the **probe** (`scripts/probe/`), targeted.
|
||||||
|
- Slow frame → **profiler** first (`CDN_PROFILE=1`), optimize second.
|
||||||
|
- Any renderer change → the **`verify-mac`** runbook gates both legs before commit.
|
||||||
|
|
||||||
|
See [RENDERER.md](RENDERER.md) for the renderer work these support.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
Kotlin standard style — plain `camelCase` for parameters, local variables,
|
Kotlin standard style — plain `camelCase` for parameters, local variables,
|
||||||
@@ -469,6 +537,18 @@ that should surface in tooling.
|
|||||||
toggling the renderer property may not invalidate it. Delete
|
toggling the renderer property may not invalidate it. Delete
|
||||||
`.gradle/configuration-cache/` between switches if you see weird
|
`.gradle/configuration-cache/` between switches if you see weird
|
||||||
"couldn't find sdl3_ttf" errors.
|
"couldn't find sdl3_ttf" errors.
|
||||||
|
- **Substituted Maven modules hide their transitives from common metadata** —
|
||||||
|
when the bridge swaps an official coord for a project module on native
|
||||||
|
configs, KGP's granular-metadata visibility check drops that Maven module's
|
||||||
|
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
|
||||||
|
'Color'` / `Cannot access class ...` in `compileCommonMainKotlinMetadata`,
|
||||||
|
while per-target compilation is fine). Declare EVERY artifact the common
|
||||||
|
code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its
|
||||||
|
own bridge rule. Note only the WINDOWS publish job compiles common metadata
|
||||||
|
(it owns the root KotlinMultiplatform publications — the only host that
|
||||||
|
declares every target, so only its .module files carry the full variant
|
||||||
|
table; macOS-published roots left v0.1.15 without mingwX64 variants) —
|
||||||
|
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
|
||||||
- **`Path()` in commonMain returns different actuals per renderer** —
|
- **`Path()` in commonMain returns different actuals per renderer** —
|
||||||
the Skia renderer produces a `SkiaBackedPath` (wraps
|
the Skia renderer produces a `SkiaBackedPath` (wraps
|
||||||
`org.jetbrains.skia.Path`), the SDL renderer produces a project
|
`org.jetbrains.skia.Path`), the SDL renderer produces a project
|
||||||
|
|||||||
@@ -1,196 +1,180 @@
|
|||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
|
[](LICENSE.md)
|
||||||
|
|
||||||
# Compose Desktop Native
|
# Compose Desktop Native
|
||||||
|
|
||||||
A Kotlin/Native port of **Compose Multiplatform** running on **SDL3** for
|
Run [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform)
|
||||||
windowing and input — **no JVM**. Compiles to native binaries for macOS
|
as a native binary, with no JVM. Compose Desktop Native compiles to a single
|
||||||
(arm64), Linux (x64/arm64), Windows (mingwX64).
|
executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
|
||||||
|
[SDL3](https://libsdl.org) for windowing and input.
|
||||||
|
|
||||||
Rendering is pluggable behind one `RenderBackend`:
|
<img src="screenshots/demo.png" width="100%" alt="Compose Desktop Native demo" />
|
||||||
|
|
||||||
- **Skia** (via Skiko klibs) on macOS + Linux — Metal / OpenGL / CPU raster.
|
## How it works
|
||||||
- **SDL3** (`SDL3_ttf` + `SDL_RenderGeometry`) on Windows, and on macOS/Linux
|
|
||||||
when `-Prenderer=sdl3` is passed.
|
|
||||||
|
|
||||||
The Compose **runtime** is the official
|
- **The runtime is the real one.** Composition, snapshots, and the recomposer
|
||||||
`org.jetbrains.compose.runtime:runtime` klibs from Maven — this project
|
are the official `org.jetbrains.compose.runtime` klibs from Maven, never
|
||||||
re-implements the layers on top of it (`androidx.compose.ui.*`,
|
reimplemented.
|
||||||
`.foundation.*`, `.animation.*`, `.material3.*`) by **vendoring
|
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
|
||||||
upstream Compose Multiplatform verbatim** whenever possible and hand-rolling
|
`animation`, and `material3` are copied from Compose Multiplatform verbatim
|
||||||
project actuals + SDL3 / Skia glue only where needed.
|
wherever they compile as is, with project code filling in only the native glue.
|
||||||
|
- **Rendering is pluggable** behind one `RenderBackend`: Skia (via Skiko) on
|
||||||
|
macOS and Linux, and a from-scratch SDL3 renderer (`SDL3_ttf` +
|
||||||
|
`SDL_RenderGeometry`) on Windows, or anywhere with `-Prenderer=sdl3`.
|
||||||
|
- **The platform is SDL3.** Windowing, input, audio, filesystem, file dialogs,
|
||||||
|
and clipboard all go through SDL3, so one code path covers every OS.
|
||||||
|
- **No runtime dependencies.** SDL3 and its codecs are built as static
|
||||||
|
libraries and linked in. A distributable is just the executable plus a
|
||||||
|
`data.kres` resource bundle.
|
||||||
|
|
||||||
## Module layout
|
| Platform | Gradle target | Default renderer |
|
||||||
|
|----------|---------------|------------------|
|
||||||
|
| macOS arm64 | `macosArm64` | Skia (Metal) |
|
||||||
|
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) |
|
||||||
|
| Windows | `mingwX64` | SDL3 |
|
||||||
|
|
||||||
One Gradle module per upstream Compose artifact — the directory tree mirrors
|
## Quickstart
|
||||||
upstream's `compose/` layout, gradle paths stay short via `projectDir`
|
|
||||||
redirects.
|
|
||||||
|
|
||||||
```
|
### A window
|
||||||
compose/
|
|
||||||
├── ui/
|
|
||||||
│ ├── ui/ → :ui (androidx.compose.ui.* + renderers + cinterops)
|
|
||||||
│ ├── ui-util/ → :ui-util
|
|
||||||
│ ├── ui-geometry/ → :ui-geometry
|
|
||||||
│ ├── ui-unit/ → :ui-unit
|
|
||||||
│ └── ui-backhandler/ → :ui-backhandler
|
|
||||||
├── animation/
|
|
||||||
│ ├── animation-core/ → :animation-core
|
|
||||||
│ ├── animation/ → :animation
|
|
||||||
│ └── animation-graphics/ → :animation-graphics
|
|
||||||
├── foundation/
|
|
||||||
│ ├── foundation/ → :foundation
|
|
||||||
│ └── foundation-layout/ → :foundation-layout
|
|
||||||
├── material3/material3/ → :material3
|
|
||||||
├── material/material-ripple/ → :material-ripple
|
|
||||||
└── sdl/ (project modules — the SDL integration layer)
|
|
||||||
└── window/ → :window (nativeComposeApp { Window(...) } + SDL3 main loop,
|
|
||||||
per-window Lifecycle/ViewModelStore/SavedState owners)
|
|
||||||
|
|
||||||
utils/
|
|
||||||
└── material-symbols/ → :material-symbols (codepoints + Outlined/Rounded/Sharp icon fonts)
|
|
||||||
|
|
||||||
components/
|
|
||||||
└── resources/library/ → :components-resources (vendored official compose resources runtime —
|
|
||||||
painterResource/stringResource/Res codegen; the Maven
|
|
||||||
artifact ships no mingwX64/linux klibs)
|
|
||||||
|
|
||||||
navigation3/
|
|
||||||
└── navigation3-ui/ → :navigation3-ui (vendored NavDisplay + scene machinery — the one
|
|
||||||
Navigation 3 layer without a K/N desktop artifact)
|
|
||||||
|
|
||||||
demo/ → :demo (showcase + CLI probe suite; multiplatform — the same screens
|
|
||||||
also build for stock JVM Compose Desktop as a parity reference)
|
|
||||||
apidemo/ → :apidemo (Postman-style REST client)
|
|
||||||
```
|
|
||||||
|
|
||||||
## demo — widget & feature showcase
|
|
||||||
|
|
||||||
<img src="screenshots/demo.png" width="100%" alt="ComposeNativeSDL3 demo" />
|
|
||||||
|
|
||||||
`:demo` is a full tour of the re-implemented Compose + Material 3 surface —
|
|
||||||
30+ sidebar screens covering buttons, text fields, layout, modifiers, shapes,
|
|
||||||
images, state & recomposition, scrolling & lazy lists, dialogs, icons, canvas,
|
|
||||||
graphics layers, custom layout, animation and gestures.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew :demo:runDebugExecutableMacosArm64 # macOS (Skia / Metal)
|
|
||||||
./gradlew :demo:runDebugExecutableLinuxX64 # Linux (Skia / OpenGL)
|
|
||||||
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (SDL3)
|
|
||||||
./gradlew :demo:run # stock JVM Compose Desktop (parity reference)
|
|
||||||
```
|
|
||||||
|
|
||||||
The same shared screens drive both stacks — the JVM target runs them on
|
|
||||||
upstream Compose Desktop unchanged, so any visual or behavioural difference
|
|
||||||
against the native build is a porting bug, not a demo artifact.
|
|
||||||
|
|
||||||
CLI: `--gpu=…`, `--screen=<Name>` (one screen, no sidebar),
|
|
||||||
`--screenshot=out.bmp --frames=N`, `--width=W --height=H`.
|
|
||||||
|
|
||||||
## apidemo — API Manager
|
|
||||||
|
|
||||||
<img src="screenshots/apidemo.png" width="100%" alt="ComposeNativeSDL3 API Manager" />
|
|
||||||
|
|
||||||
`:apidemo` is a Postman-style REST client built entirely on the library:
|
|
||||||
request collections (**packs**, nested sub-packs, linked copies), a
|
|
||||||
**Session → Pack → Request** inheritance ladder for variables / headers /
|
|
||||||
query params / client certs, syntax-highlighted JSON / XML / YAML / HTML body
|
|
||||||
editors, a response viewer with timing, size and TLS-chain inspection, mTLS
|
|
||||||
client certificates, drag-and-drop tree management, request history and
|
|
||||||
file-based sessions.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew :apidemo:runDebugExecutableMacosArm64
|
|
||||||
./gradlew :apidemo:runDebugExecutableLinuxX64
|
|
||||||
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|
||||||
```
|
|
||||||
|
|
||||||
Add `-Prenderer=sdl3` on macOS/Linux to drop Skiko and use the pure-SDL3
|
|
||||||
renderer everywhere.
|
|
||||||
|
|
||||||
## Minimal app
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import com.compose.sdl.nativeComposeWindow
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
|
||||||
fun main() = nativeComposeWindow(title = "Hello") {
|
fun main() = nativeComposeWindow(title = "Hello") {
|
||||||
Text("Hello from ComposeNativeSDL3")
|
Text("Hello from Compose Desktop Native")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The lambda runs with a `ComposeWindowScope` receiver exposing
|
The content lambda runs with a `ComposeWindowScope` receiver that exposes
|
||||||
`window: ComposeNativeWindow` (`setTitle` / `setSize` / `minimize` /
|
`window: ComposeNativeWindow` (`setTitle`, `setSize`, `minimize`, `maximize`,
|
||||||
`maximize` / `setFullscreen` / `close` / …); the same handle is reachable
|
`setFullscreen`, `close`). The same handle is reachable from any nested
|
||||||
from any nested composable via `LocalComposeNativeWindow.current`.
|
composable via `LocalComposeNativeWindow.current`.
|
||||||
|
|
||||||
Add these to your module's `commonMain.dependencies`:
|
For multiple windows, wrap them in `nativeComposeApp`, where the set of
|
||||||
|
`Window(...)` calls is state driven (Compose Desktop style):
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
implementation(project(":window")) // window + main loop
|
fun main() = nativeComposeApp {
|
||||||
implementation(project(":material3")) // Material 3 widgets
|
Window(title = "Main", onCloseRequest = ::exitApplication) { /* ... */ }
|
||||||
implementation(project(":material-symbols")) // icon-font composables (optional)
|
if (showInspector) {
|
||||||
|
Window(title = "Inspector", onCloseRequest = { showInspector = false }) { /* ... */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Each window gets its own `Lifecycle`, `ViewModelStore`, and `SavedStateRegistry`
|
||||||
|
owners, driven by real SDL focus and visibility events, so ViewModels and
|
||||||
|
saved state behave as they do on Android.
|
||||||
|
|
||||||
|
### Building in this repo
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
commonMain.dependencies {
|
||||||
|
implementation(project(":window")) // window shell + main loop
|
||||||
|
implementation(project(":material3")) // Material 3 widgets
|
||||||
|
implementation(project(":material-symbols")) // icon-font composables (optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building from your own project: the bridge plugin
|
||||||
|
|
||||||
|
The klibs publish to GitHub Packages under `com.bitsycore.compose.sdl:*`. Apply
|
||||||
|
the bridge Gradle plugin once, declare the **official** Compose Multiplatform
|
||||||
|
coordinates, and the plugin swaps in the port's klibs on native desktop targets
|
||||||
|
while android, jvm, iOS, and wasm keep resolving the official artifacts.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
// build.gradle.kts, official coordinates everywhere
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
|
id("com.bitsycore.compose-desktop-native.bridge") version "<release>"
|
||||||
|
}
|
||||||
|
|
||||||
|
commonMain.dependencies {
|
||||||
|
// The plugin exposes the exact Compose versions the port tracks, so you
|
||||||
|
// never hand-match them (material3 is versioned separately upstream).
|
||||||
|
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
|
||||||
|
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
|
||||||
|
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Repositories, credentials, and version pinning:
|
||||||
|
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
|
||||||
|
|
||||||
|
For a complete project that applies the bridge and builds one shared UI for
|
||||||
|
Android, JVM, and native desktop, see the example repo:
|
||||||
|
[bitsycore/compose-desktop-native-bridge-example](https://github.com/bitsycore/compose-desktop-native-bridge-example).
|
||||||
|
|
||||||
|
## Sample apps
|
||||||
|
|
||||||
|
Both live in `commonMain` and also build for stock JVM Compose Desktop, which
|
||||||
|
serves as the visual and behavioural reference: any difference against the
|
||||||
|
native build is a porting bug.
|
||||||
|
|
||||||
|
**`:demo`** is a tour of the re-implemented Compose and Material 3 surface,
|
||||||
|
30-plus screens covering text, layout, shapes, images, state, lazy lists,
|
||||||
|
dialogs, canvas, graphics layers, animation, and gestures.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :demo:runDebugExecutableMacosArm64 # macOS (Skia / Metal)
|
||||||
|
./gradlew :demo:runDebugExecutableLinuxX64 # Linux (Skia / OpenGL)
|
||||||
|
gradlew.bat :demo:runDebugExecutableMingwX64 # Windows (SDL3)
|
||||||
|
./gradlew :demo:run # JVM Compose Desktop (reference)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`:apidemo`** is a Postman-style REST client built entirely on the library:
|
||||||
|
request collections, a session inheritance ladder, syntax-highlighted body
|
||||||
|
editors, a response viewer with timing and TLS-chain inspection, and mTLS
|
||||||
|
client certificates.
|
||||||
|
|
||||||
|
<img src="screenshots/apidemo.png" width="100%" alt="Compose Desktop Native API Manager" />
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :apidemo:runDebugExecutableMacosArm64
|
||||||
|
./gradlew :apidemo:run # JVM Compose Desktop (reference)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `-Prenderer=sdl3` on macOS or Linux to drop Skiko and use the pure SDL3
|
||||||
|
renderer everywhere.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
One Gradle module per upstream Compose artifact, mirroring the upstream
|
||||||
|
`compose/` tree. The renderer and cinterops live in `:ui`; `:window` is the SDL
|
||||||
|
integration layer. `:material-symbols`, `:components-resources`, and
|
||||||
|
`:navigation3-ui` are project or vendored modules that the official Maven
|
||||||
|
artifacts do not cover for Kotlin/Native desktop.
|
||||||
|
|
||||||
|
Most of the androidx architecture stack (lifecycle, viewmodel, navigation3,
|
||||||
|
savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on
|
||||||
|
the port unmodified. The full module map, dependency graph, and the list of
|
||||||
|
compatible artifacts are in [CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
- **macOS:** `brew install sdl3` (Skia is the default; Skiko klibs come from
|
Build the native libraries once per machine, then build any app target:
|
||||||
Maven).
|
|
||||||
- **Linux:** `sudo apt install libsdl3-dev`.
|
|
||||||
- **Windows:** SDL3, SDL3_ttf, SDL3_image and FreeType are built from source
|
|
||||||
as **static** libraries by `scripts/build-sdl/build-all.sh` into the gitignored
|
|
||||||
in-repo `libs/`, then linked into the executable — the Windows
|
|
||||||
distributable is just `<app>.exe` + `data.kres`, no runtime DLLs.
|
|
||||||
|
|
||||||
## Vendoring
|
|
||||||
|
|
||||||
The bulk of the `androidx.compose.*` code is vendored byte-for-byte from
|
|
||||||
`JetBrains/compose-multiplatform-core` and lives under
|
|
||||||
`<module>/src/vendor/` (gitignored — you re-sync on demand). Each module
|
|
||||||
carries a `compose-fork.txt` manifest listing which upstream files it pulls
|
|
||||||
in.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/compose-fork/sync.sh # sync every module
|
python3 scripts/build-sdl/build-all.py # SDL3, SDL3_ttf, SDL3_image, FreeType (static)
|
||||||
scripts/compose-fork/sync.sh compose/ui/compose-fork.txt # one module
|
./gradlew :demo:runDebugExecutableMacosArm64
|
||||||
```
|
```
|
||||||
|
|
||||||
Upstream ref pinned in `scripts/compose-fork/compose.properties`; manifests can
|
Every host needs `git`, `cmake`, and Python 3. Per host: macOS needs the Xcode
|
||||||
pin additional upstream repos inline (`SET_REPO=<url>@<ref>` — the resources
|
command line tools; Linux needs gcc/g++ and the X11 / Wayland / audio dev
|
||||||
runtime vendors from the compose-multiplatform umbrella repo this way).
|
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
|
||||||
|
the full build and verification workflow.
|
||||||
|
|
||||||
See [CLAUDE.md](CLAUDE.md) for the full architecture, source-set hierarchy,
|
## Documentation
|
||||||
vendoring rules, density flow (physical-pixel Option B), and per-area file
|
|
||||||
map.
|
|
||||||
|
|
||||||
## Known Compatible — official artifacts that just work
|
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
|
||||||
|
- [CLAUDE.md](CLAUDE.md): architecture, source-set hierarchy, vendoring rules,
|
||||||
A surprising amount of the androidx architecture stack ships real
|
density flow, and a per-area file map.
|
||||||
Kotlin/Native desktop klibs (mingwX64, linuxX64/arm64, macosArm64) and runs
|
- [gradle-plugin/compose-desktop-native-bridge](gradle-plugin/compose-desktop-native-bridge/README.md):
|
||||||
on this port **unmodified** — no reimplementation, no vendoring. Sometimes
|
consuming the port from another build.
|
||||||
the Google coordinates (`androidx.*`) carry the K/N variant, sometimes the
|
|
||||||
JetBrains ones (`org.jetbrains.*`) — check both.
|
|
||||||
|
|
||||||
Verified working in-tree:
|
|
||||||
|
|
||||||
| Artifact | Notes |
|
|
||||||
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
|
||||||
| `org.jetbrains.compose.runtime:runtime`, `runtime-saveable` | the Compose runtime itself — never reimplemented |
|
|
||||||
| `androidx.compose.runtime:runtime-retain` | google coordinates |
|
|
||||||
| `androidx.lifecycle:lifecycle-runtime-compose` | `LocalLifecycleOwner`, `rememberLifecycleOwner`, `repeatOnLifecycle` |
|
|
||||||
| `androidx.lifecycle:lifecycle-viewmodel(-compose, -savedstate)` | full ViewModel + SavedStateHandle stack |
|
|
||||||
| `androidx.lifecycle:lifecycle-viewmodel-navigation3` | per-entry ViewModel scoping for Navigation 3 |
|
|
||||||
| `androidx.savedstate:savedstate`, `savedstate-compose` | |
|
|
||||||
| `androidx.navigation3:navigation3-runtime` | backstack / NavEntry / decorators (only the UI layer needed vendoring → `:navigation3-ui`) |
|
|
||||||
| `androidx.navigationevent:navigationevent-compose` | predictive back / BackHandler plumbing |
|
|
||||||
| `androidx.collection:collection` | |
|
|
||||||
|
|
||||||
The window layer supplies what these expect from a host: per-window
|
|
||||||
`LifecycleOwner` (driven by real SDL focus/visibility events — focused →
|
|
||||||
RESUMED, unfocused → STARTED, minimized → CREATED), `ViewModelStoreOwner`
|
|
||||||
(the `activityViewModels()` analog, `SavedStateHandle`-enabled) and
|
|
||||||
`SavedStateRegistryOwner`, plus a `Dispatchers.Main` with a true `immediate`.
|
|
||||||
ViewModels scope per nav entry, per window, or anywhere in between — same
|
|
||||||
semantics as Android.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+263
@@ -0,0 +1,263 @@
|
|||||||
|
# RENDERER.md
|
||||||
|
|
||||||
|
The durable reference for the rendering layer: how it is structured, the
|
||||||
|
convergence work that shaped it, the decisions worth remembering, and what is
|
||||||
|
left. Read this before touching anything under `renderer/`, the graphics
|
||||||
|
actuals, or the layer engine.
|
||||||
|
|
||||||
|
For build and verification commands see [TOOLING.md](TOOLING.md). For module
|
||||||
|
layout, source-set hierarchy, and vendoring rules see [CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
|
## 1. The two renderer legs
|
||||||
|
|
||||||
|
Rendering is pluggable behind one `RenderBackend` interface. Exactly one leg is
|
||||||
|
active per target:
|
||||||
|
|
||||||
|
- **Skia leg** (`skikoRendererMain`, macOS + Linux). Draws through upstream
|
||||||
|
Compose Multiplatform's own Skia stack, vendored verbatim: `SkiaBackedCanvas`,
|
||||||
|
`SkiaBackedPaint`, `SkiaShader`, `actual class GraphicsLayer` backed by
|
||||||
|
`org.jetbrains.skiko.node.RenderNode`, `SkiaGraphicsContext`. On this leg the
|
||||||
|
layer and draw engine internals are literally upstream.
|
||||||
|
- **SDL leg** (`sdlRendererMain`, Windows always; macOS/Linux under
|
||||||
|
`-Prenderer=sdl3`). A from-scratch renderer on `SDL_RenderGeometry` +
|
||||||
|
`SDL3_ttf` + FreeType. SDL is a triangle blitter, so this leg carries a
|
||||||
|
permanent bespoke surface (about 3,800 lines): `Sdl3Canvas`,
|
||||||
|
`Sdl3TextRenderer`, `FreeTypeIcons`, and the SDL render nodes.
|
||||||
|
|
||||||
|
Both legs share one project-owned text and image pipeline (`SdlParagraph`,
|
||||||
|
`NativeTextMeasurer`, `IconFont`). Text layout, line-breaking, metrics, and
|
||||||
|
hit-test are the port's engine on both legs; only glyph rasterization differs
|
||||||
|
(Skia vs SDL3_ttf/FreeType). This is a deliberate axiom, not a gap: see the
|
||||||
|
text decision in section 4.
|
||||||
|
|
||||||
|
The seam is kept as narrow and low as possible. Code flows
|
||||||
|
`Common (upstream) -> shared native engine -> Skia actual / SDL actual`. What we
|
||||||
|
minimize is hand-rolled actual surface, not actual-side line count: a fat
|
||||||
|
vendored-upstream actual (the Skia leg carrying upstream's whole GraphicsLayer
|
||||||
|
stack) is preferred over a thin hand-written one.
|
||||||
|
|
||||||
|
## 2. The retained-layer engine (the model we copied)
|
||||||
|
|
||||||
|
Upstream skiko skips work at three levels. The port has all three.
|
||||||
|
|
||||||
|
- **L1 frame scheduling.** A frame schedules the next only if still dirty.
|
||||||
|
Ours: `ComposeWindow.shouldRender()` gates `renderFrame()`; the loop blocks on
|
||||||
|
`SDL_WaitEventTimeout` when idle.
|
||||||
|
- **L2 measure/layout only dirty nodes.** Vendored `MeasureAndLayoutDelegate`
|
||||||
|
used verbatim through `ComposeOwner.measureAndLayout()`.
|
||||||
|
- **L3 draw record-once / replay** (the expensive one). Each isolated
|
||||||
|
`LayoutNode` gets an `OwnedLayer` (`GraphicsLayerOwnerLayer`) owning a
|
||||||
|
`GraphicsLayer`. The display list re-records only when content is dirty;
|
||||||
|
otherwise it replays.
|
||||||
|
|
||||||
|
The critical property: **transform, alpha, and clip changes do NOT re-record.**
|
||||||
|
Moving, scaling, rotating, or fading a layer replays cached content under a new
|
||||||
|
transform. Only a genuine content change (a state read inside the draw block, or
|
||||||
|
a resize) re-records. Upstream replays the whole scene every frame with no
|
||||||
|
dirty-region present; the entire win is not re-recording clean layers, not
|
||||||
|
drawing less screen. Dirty-region rendering is therefore an explicit non-goal.
|
||||||
|
|
||||||
|
The compositing-strategy contract (`requiresLayer()`, from
|
||||||
|
`SkiaGraphicsLayer.skiko.kt`) must match on both legs. It decides when a layer
|
||||||
|
needs an offscreen:
|
||||||
|
|
||||||
|
| Condition | Auto | Offscreen | ModulateAlpha |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `alpha < 1` | offscreen | offscreen | per-op alpha multiply (no offscreen) |
|
||||||
|
| `colorFilter != null` | offscreen | offscreen | offscreen |
|
||||||
|
| `blendMode != SrcOver` | offscreen | offscreen | offscreen |
|
||||||
|
| `renderEffect != null` | offscreen | offscreen | offscreen |
|
||||||
|
| none of the above | replay in place | always offscreen | replay in place |
|
||||||
|
|
||||||
|
## 3. Current state
|
||||||
|
|
||||||
|
- **Skia leg runs upstream's engine.** Canvas (`SkiaBackedCanvas`) and
|
||||||
|
GraphicsLayer/GraphicsContext (`org.jetbrains.skiko.node.RenderNode`) are
|
||||||
|
vendored verbatim. The Skia leg gets real display-list caching and correct
|
||||||
|
clip/shadow/renderEffect for free from upstream. After this landed, Skia
|
||||||
|
`draw` on LazyColumn fell from 1.75 ms to 0.2 ms.
|
||||||
|
- **SDL leg ships the geo node as the Windows default.** `SdlDisplayListRenderNode`
|
||||||
|
records a leaf's layer-local tessellated geometry, text runs, and icon glyphs
|
||||||
|
by params (not texture pointers), then re-emits through the layer transform.
|
||||||
|
Crisp under any transform, bit-exact, deterministic (no render-target state),
|
||||||
|
and eviction-safe (replay re-looks-up via the run LRU / FreeType glyph cache).
|
||||||
|
0.000% across a 57-screen self-consistency sweep bar a sub-0.06% rotated-edge
|
||||||
|
AA fringe. LazyColumn steady-state `draw` fell 57% versus the un-cached
|
||||||
|
baseline. Fallbacks: `CDN_LAYERCACHE=off|texture`.
|
||||||
|
- **Parity is a golden-master on the Skia leg** (about 2% median, dominated by
|
||||||
|
the shared text engine's small metric delta) and a bounded, ranked signal on
|
||||||
|
the SDL leg. See TOOLING.md for how to read it.
|
||||||
|
- **Memory is stable.** The historical composition leak is fixed (see section 6)
|
||||||
|
and guarded by the `--soaktest` gate in the verify runbook.
|
||||||
|
|
||||||
|
## 4. Decisions to remember
|
||||||
|
|
||||||
|
- **Goal is G1, cheap upstream-tracking.** The target is low per-bump
|
||||||
|
reconciliation cost when following upstream, not Windows pixel-parity or
|
||||||
|
feature completeness. The JVM Compose Desktop target (`:demo:run`) is the
|
||||||
|
documented Windows fidelity/feature tier: real Skia, full fidelity, on any
|
||||||
|
host. That makes SDL-leg fidelity work (B3) and real-Skia-on-Windows (Track A)
|
||||||
|
optional polish, not obligations.
|
||||||
|
- **Vendor, do not hand-roll.** Copy upstream verbatim wherever it compiles.
|
||||||
|
Edit-to-compile becomes a manual vendor with a `// VENDOR-BASE:` header so the
|
||||||
|
drift tripwire can track it. The litmus test for any divergence: "Is this what
|
||||||
|
upstream does? If not, what real platform constraint forces the difference?"
|
||||||
|
Valid answers name a constraint (no Skiko on Windows K/N; SDL is a triangle
|
||||||
|
blitter). "It was easier" is not valid.
|
||||||
|
- **B2 convergence is complete.** The Skia leg was migrated off the port's
|
||||||
|
hand-rolled GraphicsLayer onto upstream's own. This was a source-set migration,
|
||||||
|
not a file-flip: the SDL node cluster relocated `nativeMain -> sdlRendererMain`,
|
||||||
|
`GraphicsContext` forked per-leg behind a `createGraphicsContext()` seam, and
|
||||||
|
the upstream skiko files were un-refused in `compose-fork.txt`. Measured
|
||||||
|
sync-tax reduction on the skiko path: about 8.5x (roughly 1240 hand-rolled
|
||||||
|
lines down to about 145 lines of drift-tracked edits; roughly 1006 lines now
|
||||||
|
auto-sync verbatim on a ref bump). The beta02 -> beta03 bump proved it: zero
|
||||||
|
reconciliation, all manual-vendor bases unchanged.
|
||||||
|
- **Text stays the port's engine (B6.3 skipped).** Making Skia-leg text truly
|
||||||
|
upstream (`SkiaParagraph`) is a font-subsystem replacement, not a canvas swap:
|
||||||
|
two coexisting font-identity models (the port's name-to-bytes `IconFont` +
|
||||||
|
no-op resolver vs upstream `PlatformFont`/`FontCache`/`FontCollection`), about
|
||||||
|
13 skiko files to re-vendor, about 10 shared actuals to split, a
|
||||||
|
`data.kres`-to-`FontCache` bridge, no green intermediate, and it discards the
|
||||||
|
P3.1 metrics work. ROI is low: the Skia leg already measures text with skiko
|
||||||
|
`Font` metrics (via `currentTextMeasurer` -> `SkiaTextRenderer`), which is how
|
||||||
|
parity reached about 2%. Revisit only if complex-script/bidi fidelity on the
|
||||||
|
native Skia leg becomes a hard requirement.
|
||||||
|
- **`GraphicsLayerOwnerLayer` stays shared.** The fork point is `GraphicsLayer` /
|
||||||
|
`GraphicsContext`, not the owner layer. The shared owner layer compiles
|
||||||
|
against both `GraphicsLayer` actuals (upstream skiko + port SDL) only if both
|
||||||
|
satisfy the commonMain `expect class GraphicsLayer` API. That API-parity
|
||||||
|
invariant is enforced by `:ui:compileCommonMainKotlinMetadata` on every build.
|
||||||
|
- **Lifetime model.** The SDL leg uses GC / release-queue for layer lifetime and
|
||||||
|
does NOT vendor `ChildLayerDependenciesTracker`. The Skia leg uses upstream's
|
||||||
|
`SkiaGraphicsLayer`, which inherently uses the tracker (already vendored in
|
||||||
|
commonMain). A per-leg lifetime divergence is the class of bug that caused an
|
||||||
|
early navigation crash, so any change here goes to both legs plus a soak gate.
|
||||||
|
- **Module split (`:ui-graphics` / `:ui-text`) is shelved as infeasible.** Not
|
||||||
|
cosmetic churn as originally scoped: the `sdl3` cinterop is a shared substrate
|
||||||
|
used by 11 graphics-side files AND 12 platform/windowing/node/resources files
|
||||||
|
that stay in `:ui`. Kotlin/Native cannot cleanly share a cinterop klib across a
|
||||||
|
module boundary, which is exactly why they were merged. A real split needs a
|
||||||
|
new non-upstream `:ui-cinterop` base module plus cross-module cinterop
|
||||||
|
api-exposure, which is bigger and more divergent than the "match upstream
|
||||||
|
artifacts" goal it was meant to serve. The one keeper from the attempt:
|
||||||
|
`RenderBackend.drawRoot` now takes `(Canvas) -> Unit`, decoupling the backends
|
||||||
|
from `ComposeRootHost` (a genuine improvement, retained).
|
||||||
|
- **Track A (real Skia on Windows K/N) is shelved.** Kotlin/Native mingwX64 is
|
||||||
|
GNU-ABI; skia-pack Windows is MSVC-ABI, which cannot be statically fused into a
|
||||||
|
GNU-ABI binary. Building current C++20 Skia to a GNU/mingw static archive is an
|
||||||
|
open-ended fork (only abandoned Mozilla-era precedent). The only working route
|
||||||
|
is a runtime DLL, which breaks the no-DLL invariant and adds tens of MB on the
|
||||||
|
one platform it targets. The SDL leg is the permanent Windows renderer; JVM is
|
||||||
|
the fidelity escape hatch.
|
||||||
|
|
||||||
|
## 5. Convergence status
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Guardrails (parity gate, verify-mac runbook, drift + vendor-clean checks) | Done |
|
||||||
|
| B2: Skia leg on upstream GraphicsLayer/GraphicsContext | Done |
|
||||||
|
| B6.1: Skia leg on upstream `SkiaBackedCanvas` + paint/shader | Done |
|
||||||
|
| B6.2: upstream `GraphicsLayer` + delete transient port cluster | Done |
|
||||||
|
| B6.3: upstream text (`SkiaParagraph`) on the Skia leg | Skipped (decision) |
|
||||||
|
| B5: engine-convergence deltas audit | Done (clean wins spent by B6) |
|
||||||
|
| P2.2: composition memory leak | Fixed + soak-gated |
|
||||||
|
| P2.3: outsets / blur / renderEffect | Done via upstream `SkiaGraphicsLayer` |
|
||||||
|
| P3.1: SDL text metrics parity (17% median -> 2%) | Done |
|
||||||
|
| B3: further SDL fidelity | Capped (parity-ranked wins only) |
|
||||||
|
| Module split | Shelved (infeasible as specified) |
|
||||||
|
| Track A: real Skia on Windows | Shelved |
|
||||||
|
|
||||||
|
## 6. Remaining and future work
|
||||||
|
|
||||||
|
Nothing here blocks day-to-day work. These are the open threads worth
|
||||||
|
remembering.
|
||||||
|
|
||||||
|
- **WIN-SMOKE (pre-ship, Windows only).** The Mac runbook covers both renderers
|
||||||
|
but cannot cover the shipped mingwX64 binary, the Windows-only `PrintWindow`
|
||||||
|
probe, or the common-metadata publish job. Run these on a Windows host before
|
||||||
|
any release. This is the only outstanding verification.
|
||||||
|
- **Stabilization at Compose 1.12.0 stable.** The vendored refs are pinned to
|
||||||
|
`v1.12.0-beta03+dev4483` (no clean beta03 tag exists yet, and it is not on
|
||||||
|
Maven). The native side leads the JVM parity leg (forced to beta02, the latest
|
||||||
|
published) by a documented skew. When 1.12.0 stable ships: re-pin both refs to
|
||||||
|
the clean tag, bump the `vComposeJvmVersion` forcing in demo/apidemo/
|
||||||
|
material-symbols to close the skew, then cut the release. The concrete
|
||||||
|
step-by-step (the version map, the ref-bump flow, and the release flow) lives
|
||||||
|
in [TOOLING.md](TOOLING.md#versioning-and-releasing).
|
||||||
|
- **Deferred engine deltas (D2-D6, all low ROI under G1).**
|
||||||
|
- D2: split `GraphicsLayerOwnerLayer` per-leg to restore upstream's exact
|
||||||
|
`setLightingInfo` shadow lighting. Feasible (the old RootNodeOwner-coupling
|
||||||
|
blocker was outdated) but high blast radius for a marginal gain; elevation
|
||||||
|
shadows already match JVM.
|
||||||
|
- D3: dedupe `LayerTransformationMatrix.kt` against the now-vendored
|
||||||
|
`Matrices.skiko`. Blocked on D2 (the shared owner-layer hit-test needs a
|
||||||
|
both-legs matrix fn).
|
||||||
|
- D4/D5: `SemanticsRegion` intersect/difference are stubs, and `CharHelpers`
|
||||||
|
is naive grapheme/bidi vs upstream ICU. Real fidelity gaps, gated on an
|
||||||
|
accessibility or complex-script roadmap.
|
||||||
|
- D6: `Focusability` / `PlatformVelocityTracker` are byte-equal to upstream;
|
||||||
|
vendoring them saves nothing.
|
||||||
|
- **SDL performance (opportunistic).** Shared glyph atlas to replace per-run
|
||||||
|
textures; `drawImageRect` capture in the geo node (journal rates it low-ROI).
|
||||||
|
- **SDL_GPU backend (long-term).** Real stencil clipping, pipelined batching,
|
||||||
|
shader gradients. The `NativeRenderNode` seam makes a GPU node a clean
|
||||||
|
drop-in.
|
||||||
|
- **Native-resource lifecycle.** Wire `GraphicsLayer`/RenderNode +
|
||||||
|
`SdlImageBitmap.close()` fully into cache eviction and the renderer
|
||||||
|
`destroy()` chain; demote the periodic GC nudge once ownership covers it.
|
||||||
|
|
||||||
|
## 7. Hard-won learnings (do not relearn)
|
||||||
|
|
||||||
|
- **Minimize-divergence is load-bearing.** A hand-written `notifyLayerIsDirty`
|
||||||
|
that diverged from upstream `OwnedLayerManagerImpl` removed a layer from
|
||||||
|
`dirtyLayers` mid-loop and crashed on any navigation. Screenshots missed it;
|
||||||
|
the interaction probe caught it. Match upstream verbatim; do not hand-roll
|
||||||
|
engine plumbing from a summary.
|
||||||
|
- **The composition memory leak (root cause).** The port never called
|
||||||
|
`OwnerSnapshotObserver.clearInvalidObservations()`, which upstream
|
||||||
|
`RootNodeOwner` runs after every measure. Snapshot read-observations for
|
||||||
|
scopes invalidated on dispose lingered forever, each pinning its observed
|
||||||
|
object graph via a Kotlin/Native `ExternalRCRef` (a K/N-heap leak, vmmap
|
||||||
|
Memory Tag 246). Most visible on ripple/indication draws. Fix:
|
||||||
|
`ComposeRootHost.measureAndLayout()` sweeps `clearInvalidObservations()` after
|
||||||
|
layout, plus `ComposeOwner.onDetach` clears the detached node's observations.
|
||||||
|
Found by exact live-counters, static-mode isolation, macOS `leaks`/`heap`/
|
||||||
|
`vmmap`, and component bisection. RSS alone cannot pinpoint a referenced leak;
|
||||||
|
budget heap tooling for this class.
|
||||||
|
- **Offscreen-texture caching is timing-nondeterministic on complex screens.**
|
||||||
|
This is why the geo (no-render-target) node is the robust default, not the
|
||||||
|
texture node.
|
||||||
|
- **Rounded/path layer clips must be applied or deferred on the fast path.** The
|
||||||
|
geo path submits raw geometry, which clips only to a rect; rounded clips are a
|
||||||
|
lazy offscreen mask the fast replay bypasses. Rect clip on the fast path;
|
||||||
|
rounded/generic clips fall back to block-replay.
|
||||||
|
- **Screenshots miss crashes and settle-timing.** Free-running screenshots on
|
||||||
|
animated/settling screens give false signals. Use render-to-quiescence +
|
||||||
|
virtual frame time for parity, the probe for interaction/crash coverage.
|
||||||
|
- **The profiler `present` phase is vsync-capped by the display refresh.**
|
||||||
|
Profile on the target monitor before concluding a frame-rate gap.
|
||||||
|
|
||||||
|
## 8. Explicit non-goals
|
||||||
|
|
||||||
|
- **Dirty-region / partial present.** Upstream replays the whole scene; the win
|
||||||
|
is not re-recording, not redrawing less.
|
||||||
|
- **A custom `cacheKey` API.** Superseded by the real per-node display list.
|
||||||
|
- **Vendoring `RootNodeOwner` / the `ComposeScene` stack.** Coupled to skiko's
|
||||||
|
`SkiaLayer` and windowing; it fights the `:window` SDL loop. The port borrows
|
||||||
|
only the layer engine.
|
||||||
|
|
||||||
|
## 9. Key files
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../RenderBackend.kt`: the interface.
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../GpuMode.kt`: renderer / driver picker.
|
||||||
|
- `compose/ui/ui/src/skikoRendererMain/.../renderer/skia/SkiaRenderBackend.kt`.
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3RenderBackend.kt`.
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/FreeTypeIcons.kt`:
|
||||||
|
variable-font axis rasterization.
|
||||||
|
- `compose/ui/ui/src/commonMain/.../node/ComposeRootHost.kt`: root host,
|
||||||
|
hit-test, event dispatch, snapshot observer sweep.
|
||||||
|
- `compose/ui/ui/src/commonMain/.../node/impl/ComposeOwner.kt`: the project
|
||||||
|
`Owner` + `GraphicsLayerOwnerLayer` bridge.
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../ui/text/SdlParagraph.native.kt`: the bridged
|
||||||
|
`Paragraph` (measurement, hit-test, line metrics, span painting).
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# TODO.md
|
||||||
|
|
||||||
|
Everything the port currently leaves as a no-op, stub, hardcode, or partial
|
||||||
|
implementation, plus the gaps worth closing before a stable 1.12 release. This
|
||||||
|
is an audit of PROJECT code only (the `*.native.kt` / `*.sdl.kt` /
|
||||||
|
`com.compose.sdl.*` actuals); vendored upstream under `src/vendor/` is out of
|
||||||
|
scope.
|
||||||
|
|
||||||
|
Severity is a rough guide, not a mandate:
|
||||||
|
|
||||||
|
- **Blocker** for stable: a capability a typical desktop app expects that is
|
||||||
|
currently absent or broken.
|
||||||
|
- **Nice-to-have**: real gap, but the app works without it.
|
||||||
|
- **Cosmetic**: minor fidelity or edge-case behavior.
|
||||||
|
|
||||||
|
Context for prioritizing: the project goal is **G1, cheap upstream-tracking**
|
||||||
|
(see [RENDERER.md](RENDERER.md)). The **JVM Compose Desktop target is the
|
||||||
|
documented Windows fidelity/feature tier**, so SDL-leg visual gaps (section J)
|
||||||
|
are lower priority than the cross-cutting platform gaps (input, locale, a11y,
|
||||||
|
cursor) that affect every backend including Skia on macOS/Linux.
|
||||||
|
|
||||||
|
Renderer-internal cleanups (the deferred D2 shadow-lighting split, D3 matrix
|
||||||
|
dedupe) live in [RENDERER.md](RENDERER.md#6-remaining-and-future-work), not
|
||||||
|
here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release-blocking shortlist for stable 1.12
|
||||||
|
|
||||||
|
The cross-cutting items to weigh first (all detailed below):
|
||||||
|
|
||||||
|
1. **Locale is hardcoded to `en-US`** (section E). One fix unlocks the 40+
|
||||||
|
vendored Material 3 translations. Cheapest high-value win in this file.
|
||||||
|
2. **No IME / text composition** (section B). Only committed Latin text works;
|
||||||
|
CJK, dead keys, and accented input do not.
|
||||||
|
3. **No cursor changes** (section D). The OS cursor never becomes an I-beam over
|
||||||
|
text or a hand over clickables.
|
||||||
|
4. **No copy/paste context menu** (section C). Keyboard copy works; there is no
|
||||||
|
right-click or selection toolbar.
|
||||||
|
5. **Accessibility is entirely absent** (section A). Decide whether stable
|
||||||
|
requires any screen-reader support at all.
|
||||||
|
6. **Standard `Font(...)` / resource-font loading is a no-op** (section F). Only
|
||||||
|
pre-registered named fonts render. A blocker only if apps use the standard
|
||||||
|
font APIs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. Accessibility (entirely absent)
|
||||||
|
|
||||||
|
No accessibility bridge exists. A real `SemanticsOwner` is built but never
|
||||||
|
surfaced to any OS accessibility API (no NSAccessibility / UIA / AT-SPI, no SDL
|
||||||
|
a11y). Screen readers get nothing.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:340` · `semanticsOwner` is instantiated but never read; no traversal into a platform a11y tree. A real a11y controller would walk the semantics tree and map roles/labels/actions to a native AT bridge. **Blocker for a11y.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../platform/CompositionLocals.native.kt:31` · `LocalPlatformScreenReader` default throws (`error(...)`); never provided by any window. Screen-reader-active queries are unavailable. **Blocker for a11y.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:297` · `accessibilityManager` implements only `calculateRecommendedTimeoutMillis` (passthrough); no announce/focus/event surface. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../semantics/SemanticsRegion.native.kt:14` · `StubSemanticsRegion.intersect()`/`difference()` hardcode `false`; only `set`/`bounds` work. Region math for semantics culling is inert. **Nice-to-have.**
|
||||||
|
|
||||||
|
## B. Text input / IME
|
||||||
|
|
||||||
|
Committed text, arrow/backspace/enter editing, and mouse-drag selection work.
|
||||||
|
Composition (preedit) does not.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:407` · `textInputSession` awaits cancellation forever (`awaitCancellation()` at :414); no real `PlatformTextInputSession`. The modern `PlatformTextInputModifierNode` pipeline (BasicTextField2) has no IME start/stop, composition, or cursor-rect reporting. **Blocker.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../SDL3EventMapper.kt:126` · maps `SDL_EVENT_TEXT_INPUT` (committed) only; `SDL_EVENT_TEXT_EDITING` (preedit/composition) is never mapped, so there is no underlined composing region for dead keys / CJK. **Blocker for CJK and dead-key input.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../SDL3Backend.kt:111` · `SDL_StartTextInput` is called once at window init and never scoped to the focused field; `SDL_SetTextInputArea` is never called, so the IME candidate window renders at the wrong location. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/commonMain/.../text/input/NoOpPlatformTextInputService.kt:24,32` · legacy `PlatformTextInputService` `startInput`/`stopInput`/`updateState` + software-keyboard toggles are all NOP. Superseded by the modern path above. **Nice-to-have / cosmetic (desktop).**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:328` · `softwareKeyboardController.show/hide` NOP. **Cosmetic (desktop).**
|
||||||
|
|
||||||
|
## C. Text toolbar and context menus (copy / paste / select-all)
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:311` · `textToolbar` is a stub: `showMenu`/`hide` NOP, `status` always `Hidden`. No selection or right-click copy/paste popup anywhere. Keyboard copy still works. **Blocker for text UX.**
|
||||||
|
- `compose/foundation/.../text/selection/SelectionActuals.native.kt:42` · `addSelectionContainerTextContextMenuComponents` NOP (upstream TODO CMP-7819). No copy/select-all menu for `SelectionContainer`. **Nice-to-have.**
|
||||||
|
- `compose/foundation/.../text/selection/TextFieldSelectionManager.native.kt:24` and `compose/foundation/.../text/input/internal/selection/TextFieldSelectionState.native.kt:36` · `addBasicTextFieldTextContextMenuComponents` NOP for both the legacy and state-based `BasicTextField`. **Nice-to-have.**
|
||||||
|
|
||||||
|
## D. Pointer / cursor icons
|
||||||
|
|
||||||
|
The OS cursor never changes. No SDL cursor API is called anywhere in the port.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:333` · `pointerIconService.getIcon()` always returns `Default`; `setIcon()` body is empty. This is the hook `Modifier.pointerHoverIcon` drives, so hover cursor changes are swallowed. Should store the requested icon and call `SDL_SetCursor`. **Blocker for UX.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../input/pointer/PointerIcon.native.kt:18` · `pointerIconDefault|Crosshair|Text|Hand` are inert marker objects carrying only a name; nothing maps them to `SDL_SYSTEM_CURSOR_*`. Needs `SDL_CreateSystemCursor` + `SDL_SetCursor` (and cursor caching in the `:window` loop). **Blocker (paired with above).**
|
||||||
|
|
||||||
|
## E. Locale and internationalization
|
||||||
|
|
||||||
|
System-locale plumbing already exists in exactly one place:
|
||||||
|
`components/resources/.../ResourceEnvironment.native.kt:38` reads
|
||||||
|
`SDL_GetPreferredLocales()` for resource-qualifier resolution. Every other i18n
|
||||||
|
consumer ignores it and hardcodes English. Wiring `Locale.current` to that same
|
||||||
|
call is the flagship cheap win.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/intl/Locale.native.kt:57` · `DefaultLocale` is a fixed `Locale("en-US")`, so `Locale.current` can never change. This one value keys Material 3 string translation selection, stranding the 40+ vendored `material3/internal/l10n/*.kt` tables (they are present and reachable by tag, but the key is always en-US). Fix by querying `SDL_GetPreferredLocales()` (already bound in the `sdl3` cinterop). **Blocker; fixing it transitively localizes all of Material 3.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/intl/PlatformLocale.native.kt:18` · `createPlatformLocaleDelegate().current` hardcodes `LocaleList(listOf(Locale("en-US")))`; should reflect the full ordered preferred-locale list SDL returns (the resources module currently reads only `[0]`). **Blocker.**
|
||||||
|
- `compose/material3/.../CalendarLocale.native.kt:20` · locale-agnostic stub: `toString()` fixed `"en"`, single shared instance, `equals` treats all instances as equal. A separate type from `ui.text.intl.Locale`, so it needs its own wiring so `CalendarModel`/`PlatformDateFormat` can vary by locale. **Blocker (DatePicker/TimePicker).**
|
||||||
|
- `compose/material3/.../internal/PlatformDateFormat.native.kt` · multiple hardcodes:
|
||||||
|
- `:27` `weekdayNames` hardcoded English full/short names (DatePicker shows English day initials regardless of locale). **Blocker.**
|
||||||
|
- `:32` `formatWithPattern`/`formatWithSkeleton` ignore the pattern/skeleton and always emit ISO `yyyy-MM-dd`. **Blocker.**
|
||||||
|
- `:25` `firstDayOfWeek` hardcoded `1` (Sunday); locale-dependent. **Nice-to-have.**
|
||||||
|
- `:44` `parse()` only accepts ISO `yyyy-MM-dd`, ignoring pattern/locale. **Nice-to-have.**
|
||||||
|
- `:62` `getDateInputFormat()` hardcoded `yyyy-MM-dd` / `'-'`. **Nice-to-have.**
|
||||||
|
- `:65` `is24HourFormat()` hardcoded `true`. **Cosmetic.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/platform/NativeStringDelegate.native.kt:17` · `toUpperCase`/`toLowerCase` use locale-independent stdlib casing; the `locale` param is ignored (wrong for Turkish dotted/dotless i, etc.). **Cosmetic.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/intl/Locale.native.kt:17` · `Locale.isRtl()` not implemented; the pipeline is LTR-only (`SdlParagraph.native.kt:333` `getBidiRunDirection` always returns `Ltr`; owners/DrawScopes default `LayoutDirection.Ltr`). No RTL shaping. **Nice-to-have.**
|
||||||
|
- No `NumberFormat` / currency / decimal-grouping actual exists; numbers render via `toString()` with `.` decimal and no grouping regardless of locale. **Cosmetic.**
|
||||||
|
|
||||||
|
**Beyond a real `getLocale`:** SDL reports language + country only (no script subtag, no calendar/clock/number preferences), and Kotlin/Native bundles no ICU. Localized month/weekday name tables, date/number/currency patterns, first-day-of-week, locale-aware casing, and RTL all still require a bundled CLDR subset or a third-party K/N i18n library. The SDL wiring is the cheap 80%; these are the expensive tail.
|
||||||
|
|
||||||
|
## F. Font resolution
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:32` · `SdlPlatformFontLoader.loadBlocking`/`awaitLoad` are NOP (return `Unit`); font bytes for `ResourceFont` / `Font(bytes)` / `LoadedFontFamily` are never loaded. Only pre-registered `NamedFont` names reach the renderer. **Blocker if the app uses standard `Font(...)` / resource-font APIs; otherwise nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:38` · `PlatformFontFamilyTypefaceAdapter.resolve` returns `TypefaceResult.Immutable(Unit)`; any code reading the resolved typeface gets `Unit` (the renderer bypasses this via name lookup, so usually harmless). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/commonMain/.../text/NamedFont.kt:62` · `FontFamily.projectFontName` resolves only a `FontListFontFamily` whose first font is a `NamedFont`; returns `null` for `Serif`/`SansSerif`/`Monospace`/`Cursive`/`Default`, `LoadedFontFamily`, and resource-backed families, so all of those collapse to the single default font (no serif/mono/cursive distinction). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/font/FontFamilyResolver.native.kt:69` · deprecated `createFontFamilyResolver(fontResourceLoader)` ignores the supplied loader. **Cosmetic.**
|
||||||
|
|
||||||
|
## G. Text rendering details
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../text/SdlParagraph.native.kt:376` · `drawNativeText` accepts and ignores text `Shadow`, stroke `DrawStyle`, and non-`SrcOver` blend, so those never render on text. **Cosmetic.**
|
||||||
|
- `compose/foundation/.../text/StringHelpers.native.kt:31` and `compose/ui/ui/src/nativeMain/.../text/CharHelpers.native.kt:14` · `findPrecedingBreak`/`findFollowingBreak` (and `offsetByCodePoints`) do a codepoint/surrogate walk only, with no ICU grapheme clusters, so caret movement and backspace split combining marks and emoji ZWJ sequences. **Nice-to-have.**
|
||||||
|
|
||||||
|
## H. Drag and drop
|
||||||
|
|
||||||
|
Drop INTO the window (files + text) is fully wired and works. Drag OUT does not.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../draganddrop/Sdl3DragAndDropOwner.kt:38` · `requestDragAndDropTransfer` is a NOP and `isRequestDragAndDropTransferRequired` returns `false`; `Modifier.dragAndDropSource` compiles but no OS-level drag leaves the window. Needs per-OS native work (NSDraggingSession / DoDragDrop / XDND); SDL3 has no portable start-drag. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../draganddrop/Sdl3DragAndDropOwner.kt:119` · incoming transfer carries only file paths + a text blob; no image/URL/custom-MIME payloads. **Cosmetic.**
|
||||||
|
- `compose/foundation/.../draganddrop/DragAndDropSource.native.kt:36` · no drag-shadow / drag-image feedback for internal DnD (upstream skiko renders one). **Cosmetic.**
|
||||||
|
|
||||||
|
## I. Window and platform info
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:354` · `windowInfo.containerSize`/`containerDpSize` return `IntSize.Zero`/`DpSize.Zero`; `LocalWindowInfo.containerSize` readers (popup sizing, window-size-aware layout) get 0. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:353` · `windowInfo.isWindowFocused` hardcoded `true`; not fed by SDL focus events (which the window already tracks). Focus-reactive UI will not react. **Nice-to-have.**
|
||||||
|
- Multi-monitor: no display enumeration or per-monitor placement anywhere (`SDL_GetDisplays`/`SDL_GetDisplayBounds`/`SDL_GetDisplayForWindow` unused). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../platform/PrefetchLocals.native.kt:18` · `LocalPlatformPrefetchScheduler` default is a NOP scheduler; lazy lists skip ahead-of-time item composition (correctness fine, possible scroll-in jank). **Nice-to-have (perf).**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:324` · `autofill`/`autofillManager` are null; `requestAutofill` NOP. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:279` · `hapticFeedBack.performHapticFeedback` NOP (no desktop pointer haptics API). **Cosmetic.**
|
||||||
|
- `compose/ui/ui/src/nativeMain/.../node/impl/ComposeOwner.kt:287` · deprecated `clipboardManager` `setText`/`getText` NOP, and `clipboard.getClipEntry` null. Harmless: the real `LocalClipboard` (text + PNG image via SDL3) works. **Cosmetic.**
|
||||||
|
- `compose/foundation/.../text/input/internal/selection/TextFieldSelectionState.native.kt:44` · `ClipboardPasteState.hasClip` aliased to `hasText`, so image-only clipboard is not detected for the paste affordance. **Cosmetic.**
|
||||||
|
|
||||||
|
## J. SDL renderer graphics (Windows / `-Prenderer=sdl3` only)
|
||||||
|
|
||||||
|
These affect only the SDL leg. On macOS/Linux the Skia leg implements them
|
||||||
|
correctly, and JVM is the documented Windows fidelity tier, so these are lower
|
||||||
|
priority under G1. Listed because SDL is the shipped Windows renderer.
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../graphics/PathEffect.sdl.kt:15` · `dash`/`corner`/`chain`/`stamped` path effects are all NOP; dashed/dotted strokes render solid. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../graphics/RenderEffect.sdl.kt:16` · `RenderEffect.isSupported()` is `false`; `Modifier.blur()` and `graphicsLayer{renderEffect=...}` do nothing. `compose/ui/ui/src/sdlRendererMain/.../graphics/shadow/Blur.native.kt:33` · `Paint.setBlurFilter` NOP. **Nice-to-have (Windows visual parity).**
|
||||||
|
- Gradients render real ramps, but `TileMode` is dropped: `t` is clamped, so `Repeated`/`Mirror`/`Decal` all behave as `Clamp` (`Sdl3DrawScope.kt:1070` samplers; `CanvasPaintActuals.native.kt:66`). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../graphics/CanvasPaintActuals.native.kt:106` · `ActualImageShader`/`ActualCompositeShader` are stubs; image/composite `ShaderBrush` degrades to solid fill (white/black). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../graphics/ColorFilter.sdl.kt:13` · tint/color-matrix/lighting color filters are empty stubs; only `BlendModeColorFilter` tint on image blits is honored (`Sdl3Canvas.kt:1251`). ColorMatrix (grayscale/saturation) and all filters on shapes are inert. **Nice-to-have.**
|
||||||
|
- BlendMode: only `SrcOver` (plus a `Clear` special case) composites; `Multiply`/`Screen`/`Plus`/etc. are ignored (`Sdl3DrawScope.kt:206`, `Sdl3Canvas.kt:926`). Note `BlendMode.isSupported()` misleadingly returns `true`. **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:436` · `saveLayer` has no offscreen buffer; layer alpha is multiplied into each primitive, so overlapping content double-composites (wrong group opacity) and the layer paint's colorFilter/blendMode/renderEffect are dropped. **Nice-to-have (blocker for correct group-alpha over overlapping content).**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:509` · `clipPath` degrades an arbitrary path to its bounding box; rotated/sheared `clipRect` collapses to an AABB. Non-rect clips leak. (Rounded/difference clips are real, with feathered AA.) **Nice-to-have.**
|
||||||
|
- Stroke joins (`StrokeJoin`) are never applied, so polyline corners notch; `StrokeCap.Square` falls back to butt (`Sdl3DrawScope.kt:380,342`). **Nice-to-have / cosmetic.**
|
||||||
|
- `PathFillType` (NonZero vs EvenOdd) is mostly ignored beyond a 2-contour border case; interior holes and self-intersections are not cut out, and concave fills self-overlap (`Sdl3DrawScope.kt:366,717`). **Nice-to-have.**
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../renderer/sdl/Sdl3Canvas.kt:1276` · `drawPoints`/`drawRawPoints`/`drawVertices` are NOP; point-mode drawing and custom vertex meshes render nothing. **Nice-to-have.**
|
||||||
|
- Images under a rotated layer are not rotated (`SDL_RenderTexture` is axis-aligned, `Sdl3Canvas.kt:1264`); stroked non-square ovals stroke as a circular ring (`Sdl3DrawScope.kt:527`); `SdlImageBitmap.readPixels` is a no-op (`Sdl3Offscreen.kt:117`). **Cosmetic / nice-to-have.**
|
||||||
|
- Drop shadow is approximated (9-slice / stacked rings, not a gaussian blur; ambient vs spot largely collapsed) at `Sdl3Canvas.kt:785`; AA is a ~1px geometry fringe rather than analytic coverage. **Cosmetic.**
|
||||||
|
|
||||||
|
## K. GraphicsLayer / image
|
||||||
|
|
||||||
|
- `compose/ui/ui/src/sdlRendererMain/.../graphics/CanvasPaintActuals.native.kt:151` · `createImageBitmap(bytes)` throws `UnsupportedOperationException`. (`GraphicsLayer.toImageBitmap` and `snapshotBgra` do work on both renderers.) **Nice-to-have.**
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
# Tooling
|
||||||
|
|
||||||
|
Helper scripts and workflows used to build, vendor, and verify Compose Desktop
|
||||||
|
Native. Each has a focused job; the deeper ones link to their own README.
|
||||||
|
|
||||||
|
For architecture, source-set layout, and vendoring rules, see
|
||||||
|
[CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
|
## Native libraries
|
||||||
|
|
||||||
|
SDL3, SDL3_ttf, SDL3_image, and FreeType are built from source as static
|
||||||
|
libraries and linked straight into the executable. One script does it on every
|
||||||
|
OS. Output lands in the gitignored `libs/`; versions are pinned in
|
||||||
|
`scripts/build-sdl/build-sdl.properties`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/build-sdl/build-all.py # freetype, sdl3, sdl3-image, sdl3-ttf
|
||||||
|
python3 scripts/build-sdl/build-all.py sdl3-ttf # rebuild one step
|
||||||
|
```
|
||||||
|
|
||||||
|
Needs `git`, `cmake`, and Python 3 on every host (`ninja` is fetched when
|
||||||
|
absent). Run it once per machine, or after bumping the pinned versions.
|
||||||
|
|
||||||
|
## Vendoring upstream Compose
|
||||||
|
|
||||||
|
Most `androidx.compose.*` code is copied byte for byte from
|
||||||
|
`JetBrains/compose-multiplatform-core` into each module's gitignored
|
||||||
|
`src/vendor/`. A `compose-fork.txt` manifest per module lists which upstream
|
||||||
|
files it pulls; the pinned ref lives in `scripts/compose-fork/compose.properties`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/compose-fork/sync.sh # re-sync every module
|
||||||
|
scripts/compose-fork/sync.sh compose/ui/ui/compose-fork.txt # one module
|
||||||
|
```
|
||||||
|
|
||||||
|
Reach for this after a fresh checkout or a ref bump. Details:
|
||||||
|
[scripts/compose-fork/README.md](scripts/compose-fork/README.md).
|
||||||
|
|
||||||
|
Guardrails that keep the vendor tree honest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs
|
||||||
|
python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin
|
||||||
|
```
|
||||||
|
|
||||||
|
`check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`.
|
||||||
|
`check-vendor-drift` reads the `// VENDOR-BASE:` header on every edited copy and,
|
||||||
|
using the local upstream clone, reports whether the base actually changed since
|
||||||
|
the pin (needs reconciling) or is merely stale (safe to re-stamp). Run both on
|
||||||
|
every ref bump.
|
||||||
|
|
||||||
|
### Coverage against upstream
|
||||||
|
|
||||||
|
How much of upstream's public API the port covers is measured, not guessed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew apiDump && python3 scripts/compose-coverage.py # per-module coverage tables
|
||||||
|
python3 scripts/compose-coverage.py --missing ui-text # list uncovered declarations
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### One-command gate
|
||||||
|
|
||||||
|
`verify-mac.sh` is the runbook to run after any renderer or layout change. On
|
||||||
|
macOS or Linux it exercises BOTH renderer legs (Skia by default, then
|
||||||
|
`-Prenderer=sdl3`) and exits non-zero on any failure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/verify-mac.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It runs, per leg: the vendor drift and clean checks, a build of `:demo` and
|
||||||
|
`:apidemo`, the interaction probes, the parity sweep, a memory soak, and a
|
||||||
|
frame-time spot check. The Windows target is verified separately (see below).
|
||||||
|
|
||||||
|
### Parity: native vs JVM
|
||||||
|
|
||||||
|
`:demo` renders the same shared screens on two stacks: this port, and stock JVM
|
||||||
|
Compose Desktop. `parity.py` screenshots every screen on both and pixel-diffs
|
||||||
|
them, so a screen that diverges is a porting bug. It gates against per-screen
|
||||||
|
golden baselines.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/parity/parity.py # all screens
|
||||||
|
python3 scripts/parity/parity.py Buttons Shapes # a subset
|
||||||
|
python3 scripts/parity/parity.py --no-build # reuse the last renders
|
||||||
|
```
|
||||||
|
|
||||||
|
How to read the diff and the percentage: [scripts/parity/README.md](scripts/parity/README.md).
|
||||||
|
|
||||||
|
### Interaction probe
|
||||||
|
|
||||||
|
`probe.py` launches a native window, sends window-relative input (click, hover,
|
||||||
|
hold), and captures the client area. It packages the rigs used to reproduce
|
||||||
|
interaction bugs. Windows-only. Details:
|
||||||
|
[scripts/probe/README.md](scripts/probe/README.md).
|
||||||
|
|
||||||
|
### Frame profiler
|
||||||
|
|
||||||
|
Set `CDN_PROFILE=1` (or `CDN_PROFILE=<path>`) and run any app. Every couple of
|
||||||
|
seconds it writes per-phase main-loop timings (events, app pump, render, with
|
||||||
|
draw sub-phases and per-frame draw counters) to a file. `CDN_FORCERENDER=1`
|
||||||
|
renders every frame so steady-state timings read on an otherwise idle screen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CDN_PROFILE=1 CDN_FORCERENDER=1 ./demo/build/bin/macosArm64/debugExecutable/demo.kexe
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `present` is vsync-blocking, so profile on the target refresh rate before
|
||||||
|
drawing conclusions about a frame-rate gap.
|
||||||
|
|
||||||
|
### Demo probes
|
||||||
|
|
||||||
|
`:demo` ships headless regression probes driven by CLI flags, used by the
|
||||||
|
verify runbook and in isolation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
demo.kexe --nav3test # Navigation 3 + ViewModel + lifecycle
|
||||||
|
demo.kexe --backtest # predictive-back / BackHandler
|
||||||
|
demo.kexe --clicktest # pointer -> upstream clickable
|
||||||
|
demo.kexe --scrolltest # wheel -> scrollable
|
||||||
|
demo.kexe --multiwintest # multi-window lifecycle
|
||||||
|
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows smoke
|
||||||
|
|
||||||
|
The Mac runbook cannot cover the Windows target. Before shipping, on a Windows
|
||||||
|
host: build the mingwX64 executables, run `scripts/probe/`, and run a publish
|
||||||
|
dry run (only Windows compiles the full common-metadata variant table).
|
||||||
|
|
||||||
|
```bat
|
||||||
|
gradlew.bat :demo:runDebugExecutableMingwX64
|
||||||
|
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consuming the port
|
||||||
|
|
||||||
|
To build a third-party app against the published klibs, apply the bridge Gradle
|
||||||
|
plugin and declare official Compose Multiplatform coordinates; the plugin swaps
|
||||||
|
in the port's klibs on native desktop targets. Setup:
|
||||||
|
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
|
||||||
|
|
||||||
|
## Versioning and releasing
|
||||||
|
|
||||||
|
### Version map
|
||||||
|
|
||||||
|
Versions live in a few places, some of which move in lockstep. There is no
|
||||||
|
single file to edit; know which axis you are changing.
|
||||||
|
|
||||||
|
| Version | Where | Notes |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| Project release version | The git tag `vX.Y.Z`. `PUBLISH_VERSION` (from the tag) feeds `vPublishVersion` in `build.gradle.kts`, which strips the leading `v`. Group is `com.bitsycore.compose.sdl`. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
|
||||||
|
| Vendored Compose (native side) | `COMPOSE_CORE_REF` and `COMPOSE_REF` in `scripts/compose-fork/compose.properties`, plus `compose` in `gradle/libs.versions.toml`. | Pin to a durable tag (not a `+dev` commit upstream may GC). Re-sync after changing. |
|
||||||
|
| JVM parity forcing | `vComposeJvmVersion` in `demo`, `apidemo`, and `material-symbols` `build.gradle.kts`. | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published. |
|
||||||
|
| Skiko | `skiko` in `gradle/libs.versions.toml`. | Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses. Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
||||||
|
| SDL3 / SDL3_ttf / SDL3_image / FreeType | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
|
||||||
|
| Bridge substituted version | Defaults to the bridge plugin's own published version; consumers override with the `composeDesktopNative.version` Gradle property. | The plugin publishes with the release tag, so a consumer on the matching plugin version resolves the right klibs automatically. |
|
||||||
|
|
||||||
|
### Bump the upstream Compose ref
|
||||||
|
|
||||||
|
Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
||||||
|
|
||||||
|
1. Edit `COMPOSE_CORE_REF` / `COMPOSE_REF` in `compose.properties` (and `compose`
|
||||||
|
in `libs.versions.toml` if the coordinate version moved).
|
||||||
|
2. `scripts/compose-fork/sync.sh`
|
||||||
|
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
|
||||||
|
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
|
||||||
|
its `// VENDOR-BASE:` header to the new ref.
|
||||||
|
4. Build both legs and fix any fallout.
|
||||||
|
5. `scripts/verify-mac.sh` (both legs green, including the soak and parity gates).
|
||||||
|
6. If a matching Compose version is now published to Maven, bump
|
||||||
|
`vComposeJvmVersion` in demo/apidemo/material-symbols to close the skew.
|
||||||
|
7. Run WIN-SMOKE on a Windows host.
|
||||||
|
8. Re-seed parity baselines only if metrics legitimately moved
|
||||||
|
(`parity.py --update-baselines`), and record why.
|
||||||
|
|
||||||
|
### Cut a release
|
||||||
|
|
||||||
|
1. Complete the ref-bump flow above and confirm it is green. For a STABLE
|
||||||
|
release, `vComposeJvmVersion` should match the vendored ref (no skew).
|
||||||
|
2. On a Windows host, run `gradlew :window:compileCommonMainKotlinMetadata`.
|
||||||
|
Only Windows compiles the full common-metadata variant table, and the root
|
||||||
|
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
||||||
|
roots without mingwX64 variants (this bit v0.1.15).
|
||||||
|
3. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
|
||||||
|
|
||||||
|
What the publish job does, so you can read a failure:
|
||||||
|
|
||||||
|
- Each host publishes only its own target's publications to GitHub Packages
|
||||||
|
under `com.bitsycore.compose.sdl:*`. Windows additionally publishes the root
|
||||||
|
KotlinMultiplatform metadata, the `jvm` publication, and the bridge plugin
|
||||||
|
(only Windows declares every target).
|
||||||
|
- The `MODULES` list in the workflow must stay in sync with the modules in
|
||||||
|
`settings.gradle.kts`.
|
||||||
|
- A partial publish (for example a dropped connection mid-upload) deletes this
|
||||||
|
host's half-published versions for the tag and retries, so a rerun is safe.
|
||||||
|
- The demo and apidemo release binaries are zipped and attached to the GitHub
|
||||||
|
Release for the tag.
|
||||||
|
|
||||||
|
4. Verify a consumer build against the new version resolves through the bridge.
|
||||||
+101
-13
@@ -16,12 +16,38 @@ plugins {
|
|||||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||||
|
|
||||||
|
// JVM parity target's Compose version — the dev build matching the pinned
|
||||||
|
// COMPOSE_CORE_REF (see scripts/compose-fork/compose.properties); mirrors
|
||||||
|
// :demo's forcing (Gradle orders "+dev" BELOW the plain version, so the
|
||||||
|
// plugin's beta01 would win conflict resolution without it).
|
||||||
|
val vComposeJvmVersion = libs.versions.compose.get()
|
||||||
|
val vComposeM3JvmVersion = libs.versions.composeMaterial3.get()
|
||||||
|
val vComposeJvmForced = mapOf(
|
||||||
|
"org.jetbrains.compose.runtime" to vComposeJvmVersion,
|
||||||
|
"org.jetbrains.compose.ui" to vComposeJvmVersion,
|
||||||
|
"org.jetbrains.compose.foundation" to vComposeJvmVersion,
|
||||||
|
"org.jetbrains.compose.animation" to vComposeJvmVersion,
|
||||||
|
"org.jetbrains.compose.material" to vComposeJvmVersion,
|
||||||
|
"org.jetbrains.compose.material3" to vComposeM3JvmVersion,
|
||||||
|
)
|
||||||
|
configurations.configureEach {
|
||||||
|
if (name.startsWith("jvm")) {
|
||||||
|
resolutionStrategy.eachDependency {
|
||||||
|
vComposeJvmForced[requested.group]?.let { useVersion(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
|
||||||
linuxArm64()
|
linuxArm64()
|
||||||
linuxX64()
|
linuxX64()
|
||||||
macosArm64()
|
macosArm64()
|
||||||
if (vHostSupportsMingw) mingwX64()
|
if (vHostSupportsMingw) mingwX64()
|
||||||
|
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
targets.withType<KotlinNativeTarget>().all {
|
targets.withType<KotlinNativeTarget>().all {
|
||||||
val isMingw = name == "mingwX64"
|
val isMingw = name == "mingwX64"
|
||||||
val isLinux = name.startsWith("linux")
|
val isLinux = name.startsWith("linux")
|
||||||
@@ -68,9 +94,31 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":window"))
|
// Common API on both stacks — usable from shared code.
|
||||||
implementation(project(":material3"))
|
|
||||||
implementation(project(":material-symbols"))
|
implementation(project(":material-symbols"))
|
||||||
|
|
||||||
|
// Official Maven coords for everything the shared UI touches,
|
||||||
|
// so commonMain metadata (and the IDE's common analysis)
|
||||||
|
// resolve. Metadata + jvm resolve them from Maven; the NATIVE
|
||||||
|
// configurations substitute each for its port module (root
|
||||||
|
// build's FULL-COMMONIZATION BRIDGE). Every artifact must be
|
||||||
|
// declared DIRECTLY: transitives of a substituted module are
|
||||||
|
// invisible to the granular-metadata visibility check. The
|
||||||
|
// runtime is the official klib everywhere, never substituted
|
||||||
|
// (jvm configs force the +dev build above).
|
||||||
|
implementation("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui-graphics:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui-text:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui-unit:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui-geometry:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.ui:ui-util:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.foundation:foundation:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.foundation:foundation-layout:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.animation:animation:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.animation:animation-core:$vComposeJvmVersion")
|
||||||
|
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
||||||
|
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
@@ -80,17 +128,32 @@ kotlin {
|
|||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Single HTTP engine on every desktop target: Ktor's Curl engine. It
|
nativeMain {
|
||||||
// bundles a static libcurl per target (Schannel on Windows, OpenSSL on
|
dependencies {
|
||||||
// macOS/Linux) embedded in its cinterop klib — no runtime DLL, one copy.
|
// The SDL window shell + main loop (native-only).
|
||||||
// We use Curl everywhere (not WinHttp/Darwin) so that an upcoming
|
implementation(project(":window"))
|
||||||
// client-certificate (mTLS) path — which calls the same bundled libcurl
|
// Single HTTP engine on every native target: Ktor's Curl
|
||||||
// directly, since Ktor's engines expose no client-cert API — shares the
|
// engine. It bundles a static libcurl per target (Schannel on
|
||||||
// exact same TLS stack as ordinary requests.
|
// Windows, OpenSSL on macOS/Linux) embedded in its cinterop
|
||||||
if (vHostSupportsMingw) getByName("mingwX64Main").dependencies { implementation(libs.ktor.client.curl) }
|
// klib — no runtime DLL, one copy. Curl everywhere (not
|
||||||
getByName("macosArm64Main").dependencies { implementation(libs.ktor.client.curl) }
|
// WinHttp/Darwin) so the client-certificate (mTLS) path —
|
||||||
getByName("linuxX64Main").dependencies { implementation(libs.ktor.client.curl) }
|
// which calls the same bundled libcurl directly, since Ktor's
|
||||||
getByName("linuxArm64Main").dependencies { implementation(libs.ktor.client.curl) }
|
// engines expose no client-cert API — shares the exact same
|
||||||
|
// TLS stack as ordinary requests.
|
||||||
|
implementation(libs.ktor.client.curl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
// Upstream Compose Desktop — the parity stack.
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
// Coroutine-based engine on JVM; native targets keep Curl.
|
||||||
|
implementation(libs.ktor.client.cio)
|
||||||
|
// Dispatchers.Main provider on desktop JVM (must match
|
||||||
|
// kotlinx-coroutines-core's version — the catalog pins both).
|
||||||
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
@@ -99,6 +162,12 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compose.desktop {
|
||||||
|
application {
|
||||||
|
mainClass = "apidemo.MainJvmKt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SDL3 / SDL3_ttf / SDL3_image / FreeType are linked statically into the
|
// SDL3 / SDL3_ttf / SDL3_image / FreeType are linked statically into the
|
||||||
// executable (see the mingw linkerOpts above), so there are no runtime DLLs to
|
// executable (see the mingw linkerOpts above), so there are no runtime DLLs to
|
||||||
// bundle — the distributable is just <app>.exe + data.kres.
|
// bundle — the distributable is just <app>.exe + data.kres.
|
||||||
@@ -277,3 +346,22 @@ for (variant in variants) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Stage fonts onto the JVM classpath
|
||||||
|
// ==================
|
||||||
|
// The JVM analog of the data.kres font/ entries: :material-symbols' JVM actual
|
||||||
|
// loads each style's font from the classpath at font/<Style>.ttf, and the
|
||||||
|
// jvm Fonts actual loads the mono body font at font/NotoSansMono.ttf.
|
||||||
|
tasks.named<ProcessResources>("jvmProcessResources") {
|
||||||
|
from(notoMonoFile) { into("font") }
|
||||||
|
dependsOn(":ui:downloadNotoFonts")
|
||||||
|
val vSymbolsProject = rootProject.project(":material-symbols")
|
||||||
|
for (vStyle in vUsedStyles) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as org.gradle.api.provider.Provider<org.gradle.api.file.RegularFile>
|
||||||
|
val vDownloadTask = vSymbolsProject.extra["iconFontDownloadTask$vStyle"] as TaskProvider<*>
|
||||||
|
from(vFontFile) { into("font") }
|
||||||
|
dependsOn(vDownloadTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Monospace body font (seam)
|
||||||
|
// ==================
|
||||||
|
// Family name the body editor passes for monospace text. Resolves to the
|
||||||
|
// bundled Noto Sans Mono once registered; null means the font isn't shipped
|
||||||
|
// (data.kres on native, classpath font/ on jvm), in which case the body falls
|
||||||
|
// back to the default proportional font.
|
||||||
|
const val kMonoFamily = "noto-mono"
|
||||||
|
|
||||||
|
/* The raw family name string — used by wrappedRowCount, which measures via the
|
||||||
|
platform text pipeline (accepts a name string). Null when not bundled. */
|
||||||
|
expect val monoFontFamilyName: String?
|
||||||
|
|
||||||
|
/* The material3 Text / BasicTextField-shaped FontFamily for the mono family.
|
||||||
|
Null when not bundled. */
|
||||||
|
expect val monoFontFamily: FontFamily?
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.engine.curl.Curl
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
@@ -23,7 +22,7 @@ import kotlin.time.TimeSource
|
|||||||
run() is a suspend fun — call it off the UI dispatcher. */
|
run() is a suspend fun — call it off the UI dispatcher. */
|
||||||
class HttpRunner {
|
class HttpRunner {
|
||||||
|
|
||||||
private val fClient = HttpClient(Curl)
|
private val fClient = createApiHttpClient()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Clear any temporary client certs a prior crash left in the Windows
|
// Clear any temporary client certs a prior crash left in the Windows
|
||||||
@@ -140,7 +139,7 @@ private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
|
|||||||
|
|
||||||
/* Read a file's raw bytes (used for a FILE request body). */
|
/* Read a file's raw bytes (used for a FILE request body). */
|
||||||
internal fun readFileBytes(inPath: String): ByteArray =
|
internal fun readFileBytes(inPath: String): ByteArray =
|
||||||
FileSystem.SYSTEM.read(inPath.trim().toPath()) { readByteArray() }
|
systemFileSystem.read(inPath.trim().toPath()) { readByteArray() }
|
||||||
|
|
||||||
/* Inflate a gzip stream to its original bytes via okio's GzipSource. */
|
/* Inflate a gzip stream to its original bytes via okio's GzipSource. */
|
||||||
private fun gunzip(inBytes: ByteArray): ByteArray {
|
private fun gunzip(inBytes: ByteArray): ByteArray {
|
||||||
@@ -19,7 +19,7 @@ private val fJson = Json {
|
|||||||
/* Write the pack as pretty JSON to inPath. Returns null on success, else the
|
/* Write the pack as pretty JSON to inPath. Returns null on success, else the
|
||||||
error message. */
|
error message. */
|
||||||
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
||||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inPack))
|
writeUtf8(fJson.encodeToString(inPack))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
@@ -29,7 +29,7 @@ fun exportPack(inPack: Pack, inPath: String): String? = try {
|
|||||||
|
|
||||||
/* Read a pack back from inPath. */
|
/* Read a pack back from inPath. */
|
||||||
fun importPack(inPath: String): Result<Pack> = try {
|
fun importPack(inPath: String): Result<Pack> = try {
|
||||||
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Pack>(vText))
|
Result.success(fJson.decodeFromString<Pack>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -37,7 +37,7 @@ fun importPack(inPath: String): Result<Pack> = try {
|
|||||||
|
|
||||||
/* Write a whole self-contained session to inPath. Returns null on success. */
|
/* Write a whole self-contained session to inPath. Returns null on success. */
|
||||||
fun exportSession(inSession: Session, inPath: String): String? = try {
|
fun exportSession(inSession: Session, inPath: String): String? = try {
|
||||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inSession))
|
writeUtf8(fJson.encodeToString(inSession))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
@@ -47,7 +47,7 @@ fun exportSession(inSession: Session, inPath: String): String? = try {
|
|||||||
|
|
||||||
/* Read a session back from inPath. */
|
/* Read a session back from inPath. */
|
||||||
fun importSession(inPath: String): Result<Session> = try {
|
fun importSession(inPath: String): Result<Session> = try {
|
||||||
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Session>(vText))
|
Result.success(fJson.decodeFromString<Session>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -59,7 +59,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
|
|||||||
val vSrc = inPath.trim().toPath()
|
val vSrc = inPath.trim().toPath()
|
||||||
val vLeaf = inNewName.trim().let { if (it.contains('.')) it else "$it.json" }
|
val vLeaf = inNewName.trim().let { if (it.contains('.')) it else "$it.json" }
|
||||||
val vDst = vSrc.parent?.div(vLeaf) ?: vLeaf.toPath()
|
val vDst = vSrc.parent?.div(vLeaf) ?: vLeaf.toPath()
|
||||||
FileSystem.SYSTEM.atomicMove(vSrc, vDst)
|
systemFileSystem.atomicMove(vSrc, vDst)
|
||||||
Result.success(vDst.toString())
|
Result.success(vDst.toString())
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -68,7 +68,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
|
|||||||
/* Write arbitrary text (a response body / headers dump) to inPath. Returns null
|
/* Write arbitrary text (a response body / headers dump) to inPath. Returns null
|
||||||
on success, else the error message. */
|
on success, else the error message. */
|
||||||
fun writeTextFile(inPath: String, inText: String): String? = try {
|
fun writeTextFile(inPath: String, inText: String): String? = try {
|
||||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) { writeUtf8(inText) }
|
systemFileSystem.write(inPath.trim().toPath()) { writeUtf8(inText) }
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.message ?: e.toString()
|
e.message ?: e.toString()
|
||||||
@@ -76,7 +76,7 @@ fun writeTextFile(inPath: String, inText: String): String? = try {
|
|||||||
|
|
||||||
/* Write raw bytes (a binary response body, e.g. an image) to inPath. */
|
/* Write raw bytes (a binary response body, e.g. an image) to inPath. */
|
||||||
fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
|
fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
|
||||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) { write(inBytes) }
|
systemFileSystem.write(inPath.trim().toPath()) { write(inBytes) }
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.message ?: e.toString()
|
e.message ?: e.toString()
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
import com.compose.sdl.appDataDir
|
|
||||||
import com.compose.sdl.revealInFileManager
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
@@ -86,7 +84,7 @@ private fun stateFilePath(): String? {
|
|||||||
fun loadAppState(): AppState {
|
fun loadAppState(): AppState {
|
||||||
val vPath = stateFilePath() ?: return AppState()
|
val vPath = stateFilePath() ?: return AppState()
|
||||||
return try {
|
return try {
|
||||||
val vText = FileSystem.SYSTEM.read(vPath.toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
||||||
fStateJson.decodeFromString<AppState>(vText)
|
fStateJson.decodeFromString<AppState>(vText)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
AppState()
|
AppState()
|
||||||
@@ -98,7 +96,7 @@ fun loadAppState(): AppState {
|
|||||||
fun saveAppState(inState: AppState) {
|
fun saveAppState(inState: AppState) {
|
||||||
val vPath = stateFilePath() ?: return
|
val vPath = stateFilePath() ?: return
|
||||||
try {
|
try {
|
||||||
FileSystem.SYSTEM.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// ignore — nothing the user can act on, and packs can still be exported
|
// ignore — nothing the user can act on, and packs can still be exported
|
||||||
}
|
}
|
||||||
+2
-4
@@ -17,10 +17,8 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.compose.sdl.TextLayoutConfig
|
|
||||||
import com.compose.sdl.icons.MaterialSymbols
|
import com.compose.sdl.icons.MaterialSymbols
|
||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
import com.compose.sdl.showOpenFileDialog
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Panel 3 — request builder (Query / Headers / Body)
|
// MARK: Panel 3 — request builder (Query / Headers / Body)
|
||||||
@@ -219,7 +217,7 @@ internal fun TabSizeSelector() {
|
|||||||
var vOpen by remember { mutableStateOf(false) }
|
var vOpen by remember { mutableStateOf(false) }
|
||||||
val vHoverSrc = remember { MutableInteractionSource() }
|
val vHoverSrc = remember { MutableInteractionSource() }
|
||||||
val vHover by vHoverSrc.collectIsHoveredAsState()
|
val vHover by vHoverSrc.collectIsHoveredAsState()
|
||||||
val vSize = TextLayoutConfig.tabWidth
|
val vSize = editorTabWidth
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
tooltip = { PlainTooltip { Text("Editor tab size") } },
|
tooltip = { PlainTooltip { Text("Editor tab size") } },
|
||||||
@@ -243,7 +241,7 @@ internal fun TabSizeSelector() {
|
|||||||
for (vN in listOf(2, 4, 8)) {
|
for (vN in listOf(2, 4, 8)) {
|
||||||
DropdownMenuItem(text = {
|
DropdownMenuItem(text = {
|
||||||
Text("$vN spaces", color = if (vN == vSize) c.accent else c.text)
|
Text("$vN spaces", color = if (vN == vSize) c.accent else c.text)
|
||||||
}, onClick = { TextLayoutConfig.tabWidth = vN; vOpen = false })
|
}, onClick = { editorTabWidth = vN; vOpen = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-1
@@ -25,7 +25,6 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.compose.sdl.icons.MaterialSymbols
|
import com.compose.sdl.icons.MaterialSymbols
|
||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
import com.compose.sdl.layout.x
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Request tab strip (open requests; drag a tab to reorder)
|
// MARK: Request tab strip (open requests; drag a tab to reorder)
|
||||||
@@ -31,11 +31,8 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.compose.sdl.fileManagerName
|
|
||||||
import com.compose.sdl.icons.MaterialSymbols
|
import com.compose.sdl.icons.MaterialSymbols
|
||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
import com.compose.sdl.layout.y
|
|
||||||
import com.compose.sdl.revealInFileManager
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Pack colour (palette dot + picker)
|
// MARK: Pack colour (palette dot + picker)
|
||||||
+1
-3
@@ -23,8 +23,6 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import com.compose.sdl.icons.MaterialSymbols
|
import com.compose.sdl.icons.MaterialSymbols
|
||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
import com.compose.sdl.layout.x
|
|
||||||
import com.compose.sdl.layout.y
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -141,7 +139,7 @@ internal fun CopyButton(inText: String, inLabel: String? = null) {
|
|||||||
.background(if (vHover) c.accent.copy(alpha = 0.18f) else Color.Transparent, RoundedCornerShape(7.dp))
|
.background(if (vHover) c.accent.copy(alpha = 0.18f) else Color.Transparent, RoundedCornerShape(7.dp))
|
||||||
.hoverable(vHoverSrc)
|
.hoverable(vHoverSrc)
|
||||||
.clickable {
|
.clickable {
|
||||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(inText)) }
|
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(inText)) }
|
||||||
vCopied = true
|
vCopied = true
|
||||||
}
|
}
|
||||||
.padding(horizontal = if (inLabel != null) 10.dp else 5.dp, vertical = 5.dp),
|
.padding(horizontal = if (inLabel != null) 10.dp else 5.dp, vertical = 5.dp),
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: TLS chain / client-certificate seam
|
||||||
|
// ==================
|
||||||
|
// The real implementations drive the bundled libcurl directly (CurlMtls.kt,
|
||||||
|
// nativeMain) — Ktor's engines expose no client-cert / CERTINFO API. The jvm
|
||||||
|
// parity app has no bundled libcurl, so its actuals report the feature as
|
||||||
|
// native-only instead.
|
||||||
|
|
||||||
|
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
||||||
|
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
||||||
|
presented it. Derived certs — an issuer resolved from the OS store, or a
|
||||||
|
name-only placeholder — have fromServer = false and are drawn dotted. */
|
||||||
|
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
|
||||||
|
|
||||||
|
/* The server's TLS certificate chain. error is set instead when it couldn't be
|
||||||
|
fetched; fields vary by TLS backend. */
|
||||||
|
class TlsChain(val certs: List<ChainCert>, val error: String?)
|
||||||
|
|
||||||
|
/* Fetch the TLS certificate chain the request's server presents. */
|
||||||
|
expect fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
||||||
|
|
||||||
|
/* Send the request with its client certificate attached (mTLS). */
|
||||||
|
expect fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
||||||
|
|
||||||
|
/* Remove temp client certs a previous crashed run left behind. Call once at
|
||||||
|
startup; no-op where the OS store isn't used. */
|
||||||
|
expect fun sweepTempClientCerts()
|
||||||
+10
-18
@@ -39,14 +39,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.compose.sdl.icons.MaterialSymbols
|
import com.compose.sdl.icons.MaterialSymbols
|
||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
import com.compose.sdl.removeMemoryResource
|
|
||||||
import com.compose.sdl.res.ResourceKind
|
|
||||||
import com.compose.sdl.res.painterResource
|
|
||||||
import com.compose.sdl.scrollbar.ScrollbarStyle
|
|
||||||
import com.compose.sdl.scrollbar.VerticalScrollbar
|
|
||||||
import com.compose.sdl.scrollbar.rememberScrollbarAdapter
|
|
||||||
import com.compose.sdl.showSaveFileDialog
|
|
||||||
import com.compose.sdl.text.currentTextMeasurer
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -169,8 +161,8 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
inBody = if (vRespImage) null else vBody,
|
inBody = if (vRespImage) null else vBody,
|
||||||
inIsImage = vRespImage && !vLoading,
|
inIsImage = vRespImage && !vLoading,
|
||||||
inImagePainter = if (vRespImage && !vLoading) {
|
inImagePainter = if (vRespImage && !vLoading) {
|
||||||
val vKind = if (vResp?.contentType?.contains("svg", ignoreCase = true) == true) ResourceKind.Svg else ResourceKind.Raster
|
val vSvg = vResp?.contentType?.contains("svg", ignoreCase = true) == true
|
||||||
painterResource(inRs.imageKey!!, vKind)
|
memoryImagePainter(inRs.imageKey!!, vSvg)
|
||||||
} else null,
|
} else null,
|
||||||
inHeadersCollapsed = vHeadersCollapsed,
|
inHeadersCollapsed = vHeadersCollapsed,
|
||||||
inOnToggleCollapse = { vHeadersCollapsed = !vHeadersCollapsed },
|
inOnToggleCollapse = { vHeadersCollapsed = !vHeadersCollapsed },
|
||||||
@@ -247,7 +239,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
inResponseBody = vRespBody,
|
inResponseBody = vRespBody,
|
||||||
inOnMessage = { vMsg = it },
|
inOnMessage = { vMsg = it },
|
||||||
inOnClear = {
|
inOnClear = {
|
||||||
inRs.imageKey?.let { removeMemoryResource(it) }
|
inRs.imageKey?.let { removeMemoryImage(it) }
|
||||||
inRs.imageKey = null
|
inRs.imageKey = null
|
||||||
inRs.response = null
|
inRs.response = null
|
||||||
inRs.sentReq = null
|
inRs.sentReq = null
|
||||||
@@ -508,7 +500,7 @@ internal fun HttpFlowView(
|
|||||||
val vS = vSel
|
val vS = vSel
|
||||||
if (vS != null && !vS.isEmpty) {
|
if (vS != null && !vS.isEmpty) {
|
||||||
val vText = vBody.substring(vS.start, vS.end)
|
val vText = vBody.substring(vS.start, vS.end)
|
||||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vText)) }
|
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vText)) }
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
@@ -974,18 +966,18 @@ internal fun BodyView(
|
|||||||
// also use it for the first frame, before the body reports its width).
|
// also use it for the first frame, before the body reports its width).
|
||||||
val vNumColor = c.dim.copy(alpha = 0.45f)
|
val vNumColor = c.dim.copy(alpha = 0.45f)
|
||||||
val vNumbers = remember(inText, vBodyWidthPx, inSoftWrap, vFontPx) {
|
val vNumbers = remember(inText, vBodyWidthPx, inSoftWrap, vFontPx) {
|
||||||
val vM = if (inSoftWrap && vBodyWidthPx > 0) currentTextMeasurer else null
|
val vWrapRows = inSoftWrap && vBodyWidthPx > 0
|
||||||
buildString {
|
buildString {
|
||||||
for ((vIdx, vSrc) in vLines.withIndex()) {
|
for ((vIdx, vSrc) in vLines.withIndex()) {
|
||||||
if (vIdx > 0) append('\n')
|
if (vIdx > 0) append('\n')
|
||||||
append(vIdx + 1)
|
append(vIdx + 1)
|
||||||
if (vM != null) {
|
if (vWrapRows) {
|
||||||
// Pass the density-scaled font size so this wrap matches the body's
|
// Pass the density-scaled font size so this wrap matches the body's
|
||||||
// actual glyph pixel widths — passing 12 (sp) here counted each
|
// actual glyph pixel widths — passing 12 (sp) here counted each
|
||||||
// char as half its rendered size, so long lines that visually
|
// char as half its rendered size, so long lines that visually
|
||||||
// wrapped into 3 rows in the body were estimated as 1 or 2 rows
|
// wrapped into 3 rows in the body were estimated as 1 or 2 rows
|
||||||
// here and the gutter numbers drifted upward.
|
// here and the gutter numbers drifted upward.
|
||||||
val vRows = vM.wrap(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).lines.size.coerceAtLeast(1)
|
val vRows = wrappedRowCount(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).coerceAtLeast(1)
|
||||||
repeat(vRows - 1) { append('\n') }
|
repeat(vRows - 1) { append('\n') }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1309,18 +1301,18 @@ internal fun ViewerOverflowMenu(
|
|||||||
}
|
}
|
||||||
DropdownMenu(expanded = vOpen, onDismissRequest = { vOpen = false }) {
|
DropdownMenu(expanded = vOpen, onDismissRequest = { vOpen = false }) {
|
||||||
DropdownMenuItem(text = { Text("Copy all") }, onClick = {
|
DropdownMenuItem(text = { Text("Copy all") }, onClick = {
|
||||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(copyAll())) }
|
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(copyAll())) }
|
||||||
inOnMessage("Copied all.")
|
inOnMessage("Copied all.")
|
||||||
vOpen = false
|
vOpen = false
|
||||||
})
|
})
|
||||||
DropdownMenuItem(text = { Text("Copy headers") }, onClick = {
|
DropdownMenuItem(text = { Text("Copy headers") }, onClick = {
|
||||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vHeadersText)) }
|
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vHeadersText)) }
|
||||||
inOnMessage("Copied headers.")
|
inOnMessage("Copied headers.")
|
||||||
vOpen = false
|
vOpen = false
|
||||||
})
|
})
|
||||||
if (vCanCopyBody) {
|
if (vCanCopyBody) {
|
||||||
DropdownMenuItem(text = { Text("Copy body") }, onClick = {
|
DropdownMenuItem(text = { Text("Copy body") }, onClick = {
|
||||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vBodyText)) }
|
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vBodyText)) }
|
||||||
inOnMessage("Copied body.")
|
inOnMessage("Copied body.")
|
||||||
vOpen = false
|
vOpen = false
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import okio.FileSystem
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop)
|
||||||
|
// ==================
|
||||||
|
// The port modules are native-only, so shared code can't call com.compose.sdl
|
||||||
|
// directly — every SDL-backed API the app uses goes through these expects.
|
||||||
|
// Signatures mirror the native originals so the native actuals are pure
|
||||||
|
// delegation (same pattern as :demo's demo.shim package).
|
||||||
|
|
||||||
|
/* Per-user app-data dir (created on demand, ends with a separator) or null. */
|
||||||
|
expect fun appDataDir(inOrg: String, inApp: String): String?
|
||||||
|
|
||||||
|
/* Open the OS file manager showing the given path. Fire-and-forget. */
|
||||||
|
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
|
||||||
|
|
||||||
|
/* "Explorer" / "Finder" / "Files" — for menu labels. */
|
||||||
|
expect fun fileManagerName(): String
|
||||||
|
|
||||||
|
/* Async save-file dialog; the callback gets the chosen absolute path or null. */
|
||||||
|
expect fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> Unit)
|
||||||
|
|
||||||
|
/* Async open-file dialog; the callback gets the chosen absolute path or null. */
|
||||||
|
expect fun showOpenFileDialog(inOnResult: (String?) -> Unit)
|
||||||
|
|
||||||
|
/* A plain-text clipboard entry (ClipEntry construction differs per stack). */
|
||||||
|
expect fun clipEntryOfText(inText: String): ClipEntry
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// In-memory response images
|
||||||
|
|
||||||
|
/* Register/replace decoded-image source bytes under a key (resp-image://N). */
|
||||||
|
expect fun registerMemoryImage(inKey: String, inBytes: ByteArray)
|
||||||
|
|
||||||
|
/* Drop a registered image's bytes. */
|
||||||
|
expect fun removeMemoryImage(inKey: String)
|
||||||
|
|
||||||
|
/* Painter for a registered image key (raster or SVG). */
|
||||||
|
@Composable
|
||||||
|
expect fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Text layout
|
||||||
|
|
||||||
|
/* Rows the given line occupies when soft-wrapped to inMaxWidthPx at
|
||||||
|
inFontPx (already density-scaled). Used by the body gutter so its line
|
||||||
|
numbers stay aligned with the wrapped body text. */
|
||||||
|
expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int
|
||||||
|
|
||||||
|
/* Editor tab width in spaces — how wide a typed '\t' renders. On the native
|
||||||
|
stack this drives the project text pipeline (TextLayoutConfig); the jvm
|
||||||
|
parity app only stores the preference. */
|
||||||
|
expect var editorTabWidth: Int
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Infrastructure
|
||||||
|
|
||||||
|
/* okio's FileSystem.SYSTEM is declared per-platform, not in its common
|
||||||
|
metadata — surface it through a seam. */
|
||||||
|
internal expect val systemFileSystem: FileSystem
|
||||||
|
|
||||||
|
/* The app's Ktor client: Curl on native (bundled libcurl — same TLS stack as
|
||||||
|
the mTLS path), CIO on the jvm parity target. */
|
||||||
|
expect fun createApiHttpClient(): HttpClient
|
||||||
|
|
||||||
|
/* Window-shell integration: an intercepting close handler (return true to
|
||||||
|
allow the close) and an app-wide key-shortcut handler; both cleared when
|
||||||
|
the composition leaves. */
|
||||||
|
@Composable
|
||||||
|
expect fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
|
import androidx.compose.ui.layout.positionInWindow
|
||||||
|
|
||||||
|
// COPY of the port's com.compose.sdl.layout LayoutCoordinates sugar so the
|
||||||
|
// same helpers resolve on the jvm parity target (the port modules are
|
||||||
|
// native-only). Guards `isAttached`: a menu/tooltip anchor can be read while
|
||||||
|
// its node is mid-recycle, and positionInWindow() throws on a detached
|
||||||
|
// coordinator.
|
||||||
|
|
||||||
|
/* Logical-point X of this layout in the window. */
|
||||||
|
val LayoutCoordinates.x: Int get() = if (isAttached) positionInWindow().x.toInt() else 0
|
||||||
|
|
||||||
|
/* Logical-point Y of this layout in the window. */
|
||||||
|
val LayoutCoordinates.y: Int get() = if (isAttached) positionInWindow().y.toInt() else 0
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
// COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME
|
||||||
|
// implementation runs on the jvm parity target — the port modules are
|
||||||
|
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||||
|
// Keep in sync with the original when it changes.
|
||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
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.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Scrollbar (Compose Desktop-style)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
|
||||||
|
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
|
||||||
|
it a ScrollbarAdapter built from the same ScrollState. This project's Box has
|
||||||
|
no BoxScope (so no Modifier.align) — pin the bar with the Box's
|
||||||
|
contentAlignment; the fillMaxSize content fills the rest:
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||||
|
val state = rememberScrollState()
|
||||||
|
Column(Modifier.fillMaxSize().verticalScroll(state)) { ... }
|
||||||
|
VerticalScrollbar(
|
||||||
|
adapter = rememberScrollbarAdapter(state),
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Always visible (no auto-hide); the thumb tweens unhoverColor -> hoverColor on
|
||||||
|
hover over hoverDurationMillis, matching upstream. Drag the thumb to scroll;
|
||||||
|
click the track to page. */
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class ScrollbarStyle(
|
||||||
|
val minimalHeight: Dp,
|
||||||
|
val thickness: Dp,
|
||||||
|
val shape: Shape,
|
||||||
|
val hoverDurationMillis: Int,
|
||||||
|
val unhoverColor: Color,
|
||||||
|
val hoverColor: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Upstream default: an 8dp black thumb that's faint until hovered. Dark UIs
|
||||||
|
provide a white-tinted style via LocalScrollbarStyle or the `style` param. */
|
||||||
|
fun defaultScrollbarStyle(): ScrollbarStyle = ScrollbarStyle(
|
||||||
|
minimalHeight = 16.dp,
|
||||||
|
thickness = 8.dp,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
hoverDurationMillis = 300,
|
||||||
|
unhoverColor = Color.Black.copy(alpha = 0.12f),
|
||||||
|
hoverColor = Color.Black.copy(alpha = 0.50f),
|
||||||
|
)
|
||||||
|
|
||||||
|
val LocalScrollbarStyle = compositionLocalOf { defaultScrollbarStyle() }
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: ScrollbarAdapter
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* Bridges a scrollable's state to the scrollbar in pixels. scrollOffset /
|
||||||
|
contentSize / viewportSize are along the scroll axis; the thumb is sized by
|
||||||
|
viewportSize / contentSize and positioned by scrollOffset / (content - viewport). */
|
||||||
|
interface ScrollbarAdapter {
|
||||||
|
val scrollOffset: Double
|
||||||
|
val contentSize: Double
|
||||||
|
val viewportSize: Double
|
||||||
|
suspend fun scrollTo(scrollOffset: Double)
|
||||||
|
}
|
||||||
|
|
||||||
|
// (The port also ships a ScrollState-based adapter; it reads foundation
|
||||||
|
// internals (ScrollState.contentSize/viewportSize) that a copy outside
|
||||||
|
// :foundation cannot access. apidemo only scrollbars a LazyColumn.)
|
||||||
|
|
||||||
|
/* Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the
|
||||||
|
size of its VISIBLE items, so exact pixel geometry of off-screen content is
|
||||||
|
unknowable — we estimate it from the average visible item size × total item
|
||||||
|
count (the same approach as Compose Desktop). Accurate enough for a thumb
|
||||||
|
whose length/position track the scroll position; the estimate self-corrects
|
||||||
|
as items of different sizes scroll through the viewport. */
|
||||||
|
@Composable
|
||||||
|
fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter =
|
||||||
|
remember(scrollState) { LazyListScrollbarAdapter(scrollState) }
|
||||||
|
|
||||||
|
private class LazyListScrollbarAdapter(private val state: LazyListState) : ScrollbarAdapter {
|
||||||
|
// Mean main-axis size of the currently-visible items, in px (>= 1 to avoid /0).
|
||||||
|
private fun averageItemSize(): Double {
|
||||||
|
val vVisible = state.layoutInfo.visibleItemsInfo
|
||||||
|
if (vVisible.isEmpty()) return 0.0
|
||||||
|
val vSpan = (vVisible.last().offset + vVisible.last().size) - vVisible.first().offset
|
||||||
|
return (vSpan.toDouble() / vVisible.size).coerceAtLeast(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val scrollOffset: Double
|
||||||
|
get() = averageItemSize() * state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset
|
||||||
|
|
||||||
|
override val contentSize: Double
|
||||||
|
get() = averageItemSize() * state.layoutInfo.totalItemsCount
|
||||||
|
|
||||||
|
override val viewportSize: Double
|
||||||
|
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
|
||||||
|
|
||||||
|
override suspend fun scrollTo(scrollOffset: Double) {
|
||||||
|
val vDistance = scrollOffset - this.scrollOffset
|
||||||
|
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
|
||||||
|
// list's pixel geometry is only ESTIMATED (average visible item size), and
|
||||||
|
// that estimate drifts as differently-sized items scroll through the
|
||||||
|
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
|
||||||
|
// sidesteps the estimate entirely. This is what Compose Desktop's own
|
||||||
|
// LazyListScrollbarAdapter does, and for the same reason.
|
||||||
|
if (abs(vDistance) <= viewportSize) {
|
||||||
|
state.scrollBy(vDistance.toFloat())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Far jump (track-click paging past a viewport): no exact geometry to lean
|
||||||
|
// on, so estimate an index/offset from the average and snap to it.
|
||||||
|
val vAvg = averageItemSize()
|
||||||
|
if (vAvg <= 0.0) return
|
||||||
|
val vTarget = scrollOffset.coerceAtLeast(0.0)
|
||||||
|
val vTotal = state.layoutInfo.totalItemsCount
|
||||||
|
val vIndex = (vTarget / vAvg).toInt().coerceIn(0, (vTotal - 1).coerceAtLeast(0))
|
||||||
|
val vItemOffset = (vTarget - vIndex * vAvg).toInt().coerceAtLeast(0)
|
||||||
|
state.scrollToItem(vIndex, vItemOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: VerticalScrollbar / HorizontalScrollbar
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalScrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = true)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HorizontalScrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = false)
|
||||||
|
|
||||||
|
/* Shared impl. The root Box is the track (fixed thickness on the cross axis,
|
||||||
|
filling the main axis from the caller's modifier); a child Box is the thumb,
|
||||||
|
positioned with an offset. Drag + track-click are handled on the track in
|
||||||
|
track-relative coordinates (a stable origin), so the grab point stays under
|
||||||
|
the pointer with no feedback from the thumb moving. interactionSource is
|
||||||
|
accepted for API parity; hover here is driven by Modifier.hoverable. */
|
||||||
|
@Composable
|
||||||
|
private fun Scrollbar(
|
||||||
|
adapter: ScrollbarAdapter,
|
||||||
|
modifier: Modifier,
|
||||||
|
reverseLayout: Boolean,
|
||||||
|
style: ScrollbarStyle,
|
||||||
|
@Suppress("UNUSED_PARAMETER") interactionSource: MutableInteractionSource,
|
||||||
|
isVertical: Boolean,
|
||||||
|
) {
|
||||||
|
val vScope = rememberCoroutineScope()
|
||||||
|
var vTrackPx by remember { mutableStateOf(0) }
|
||||||
|
val vHoverSource = remember { MutableInteractionSource() }
|
||||||
|
val vHovered by vHoverSource.collectIsHoveredAsState()
|
||||||
|
var vDraggingThumb by remember { mutableStateOf(false) }
|
||||||
|
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
|
||||||
|
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
|
||||||
|
// physical pixels. Passing them into `.dp` doubles again through
|
||||||
|
// density.toPx() on Retina, which visibly bloated the thumb (filled the
|
||||||
|
// whole track) and made drag/track-click positions land 2× too far down.
|
||||||
|
// Route pixel-sized modifiers through the current density so `.dp` and the
|
||||||
|
// pixel value stay in the same coord space.
|
||||||
|
val vDensity = LocalDensity.current
|
||||||
|
|
||||||
|
val vContent = adapter.contentSize
|
||||||
|
val vViewport = adapter.viewportSize
|
||||||
|
val vOffset = adapter.scrollOffset
|
||||||
|
val vMaxScroll = vContent - vViewport
|
||||||
|
val vScrollable = vMaxScroll > 0.5 && vViewport > 0.0
|
||||||
|
|
||||||
|
val vColor by animateColorAsState(
|
||||||
|
targetValue = if (vHovered || vDraggingThumb) style.hoverColor else style.unhoverColor,
|
||||||
|
animationSpec = tween(style.hoverDurationMillis),
|
||||||
|
)
|
||||||
|
|
||||||
|
val vRoot = (if (isVertical) modifier.width(style.thickness) else modifier.height(style.thickness))
|
||||||
|
.onSizeChanged { vTrackPx = if (isVertical) it.height else it.width }
|
||||||
|
.hoverable(vHoverSource)
|
||||||
|
|
||||||
|
Box(modifier = vRoot) {
|
||||||
|
if (!vScrollable || vTrackPx <= 0) return@Box
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Thumb metrics (px)
|
||||||
|
val vMinLen = style.minimalHeight.value.toInt()
|
||||||
|
val vThumbLen = (vTrackPx * (vViewport / vContent)).roundToInt()
|
||||||
|
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
|
||||||
|
val vTravel = vTrackPx - vThumbLen
|
||||||
|
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
|
||||||
|
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
|
||||||
|
|
||||||
|
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
|
||||||
|
val vThumbMod = (
|
||||||
|
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
|
||||||
|
else Modifier.offset { IntOffset(vThumbPos, 0) }.height(style.thickness).width(vThumbLenDp)
|
||||||
|
)
|
||||||
|
.clip(style.shape)
|
||||||
|
.background(vColor)
|
||||||
|
|
||||||
|
Box(modifier = vThumbMod)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Drag the thumb / page on track click — handled on the track so the
|
||||||
|
// coordinates have a stable (non-moving) origin. The press is CONSUMED:
|
||||||
|
// a scrollbar owns the pointer input it handles, so content sharing this
|
||||||
|
// region (the scrollable beneath, or a parent that also reads pointer
|
||||||
|
// input) never reacts to the same press. Geometry is read LIVE from the
|
||||||
|
// adapter at press time — not from the captured render metrics above,
|
||||||
|
// which go stale between pointerInput relaunches — so a grab after a
|
||||||
|
// wheel-scroll still hit-tests against the thumb's current position.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
|
||||||
|
.pointerInput(isVertical, adapter, vTrackPx, reverseLayout) {
|
||||||
|
awaitEachGesture {
|
||||||
|
val vDown = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
vDown.consume()
|
||||||
|
|
||||||
|
// Live geometry — stable for the drag's duration (content /
|
||||||
|
// viewport / track sizes don't change mid-drag).
|
||||||
|
val vContentPx = adapter.contentSize
|
||||||
|
val vViewportPx = adapter.viewportSize
|
||||||
|
val vMaxScrollPx = vContentPx - vViewportPx
|
||||||
|
if (vMaxScrollPx <= 0.0 || vTrackPx <= 0) return@awaitEachGesture
|
||||||
|
val vMinLen = style.minimalHeight.value.toInt()
|
||||||
|
val vLen = (vTrackPx * (vViewportPx / vContentPx)).roundToInt()
|
||||||
|
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
|
||||||
|
val vTravelPx = vTrackPx - vLen
|
||||||
|
val vRaw = if (vTravelPx <= 0) 0
|
||||||
|
else (vTravelPx * (adapter.scrollOffset / vMaxScrollPx)).roundToInt()
|
||||||
|
val vPos = (if (reverseLayout) vTravelPx - vRaw else vRaw).coerceIn(0, vTravelPx)
|
||||||
|
|
||||||
|
// Map a track-relative thumb-top to a scroll offset. The
|
||||||
|
// target uses the LIVE max scroll (read fresh each move),
|
||||||
|
// not the press-time value — the adapter compares it against
|
||||||
|
// its live scrollOffset, and mixing a stale scale with a live
|
||||||
|
// one makes the delta run away (grab at the bottom, drag up,
|
||||||
|
// and the list overshoots to the middle).
|
||||||
|
fun scrollToTrackPos(inThumbTop: Int) {
|
||||||
|
if (vTravelPx <= 0) return
|
||||||
|
val vLiveMax = adapter.contentSize - adapter.viewportSize
|
||||||
|
if (vLiveMax <= 0.0) return
|
||||||
|
val vClamped = inThumbTop.coerceIn(0, vTravelPx)
|
||||||
|
val vFrac = (if (reverseLayout) vTravelPx - vClamped else vClamped).toDouble() / vTravelPx
|
||||||
|
vScope.launch { adapter.scrollTo(vFrac * vLiveMax) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val vPress = (if (isVertical) vDown.position.y else vDown.position.x).toInt()
|
||||||
|
val vGrab: Int
|
||||||
|
if (vPress in vPos..(vPos + vLen)) {
|
||||||
|
vDraggingThumb = true
|
||||||
|
vGrab = vPress - vPos
|
||||||
|
} else {
|
||||||
|
// Page toward the click by one viewport.
|
||||||
|
vDraggingThumb = false
|
||||||
|
vGrab = 0
|
||||||
|
val vDir = if (vPress < vPos) -1.0 else 1.0
|
||||||
|
vScope.launch { adapter.scrollTo(adapter.scrollOffset + vDir * vViewportPx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow the pressed pointer to release, consuming every
|
||||||
|
// change so the interaction can't leak. Only a thumb grab
|
||||||
|
// drags-to-scroll; a track press just consumes.
|
||||||
|
do {
|
||||||
|
val vEvent = awaitPointerEvent()
|
||||||
|
val vChange = vEvent.changes.firstOrNull { it.id == vDown.id } ?: break
|
||||||
|
if (vDraggingThumb) {
|
||||||
|
val vCur = (if (isVertical) vChange.position.y else vChange.position.x).toInt()
|
||||||
|
scrollToTrackPos(vCur - vGrab)
|
||||||
|
}
|
||||||
|
vChange.consume()
|
||||||
|
} while (vEvent.changes.any { it.id == vDown.id && it.pressed })
|
||||||
|
|
||||||
|
vDraggingThumb = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes the invisible drag-capture overlay to the full track. `inTrackDp`
|
||||||
|
comes from the caller pre-converted via LocalDensity (Option-B: layout
|
||||||
|
coords are physical px, so px-to-dp goes through the current density). */
|
||||||
|
private fun Modifier.matchTrackSize(inVertical: Boolean, inTrackDp: Dp, inThickness: Dp): Modifier =
|
||||||
|
if (inVertical) this.width(inThickness).height(inTrackDp)
|
||||||
|
else this.height(inThickness).width(inTrackDp)
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME
|
||||||
|
// implementation runs on the jvm parity target — the port modules are
|
||||||
|
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||||
|
// Keep in sync with the original when it changes.
|
||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: SplitPane
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/** Two-pane resizable split. `initialFirstSize` seeds the first pane's width
|
||||||
|
* (horizontal split) or height (vertical split); after first layout it tracks
|
||||||
|
* the user's drag. `minFirstSize` / `minSecondSize` clamp so neither pane can
|
||||||
|
* shrink past its minimum.
|
||||||
|
*
|
||||||
|
* Internal sizing is in **pixels** to match this port's pixel-space layout
|
||||||
|
* tree (Option-B density flow). `Modifier.width((px / density).dp)` converts
|
||||||
|
* back at the modifier boundary. */
|
||||||
|
@Composable
|
||||||
|
fun HorizontalSplitPane(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
initialFirstSize: Dp = 200.dp,
|
||||||
|
minFirstSize: Dp = 50.dp,
|
||||||
|
minSecondSize: Dp = 50.dp,
|
||||||
|
dividerColor: Color = Color(0x33FFFFFFL),
|
||||||
|
dividerHoverColor: Color = Color(0x66FFFFFFL),
|
||||||
|
first: @Composable () -> Unit,
|
||||||
|
second: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val vDensity = LocalDensity.current
|
||||||
|
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
|
||||||
|
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
|
||||||
|
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
|
||||||
|
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
|
||||||
|
|
||||||
|
var vTotalWidthPx by remember { mutableStateOf(0) }
|
||||||
|
var vFirstWidthPx by remember { mutableStateOf(vInitialFirstPx) }
|
||||||
|
val vDividerSource = remember { MutableInteractionSource() }
|
||||||
|
val vDividerHover by vDividerSource.collectIsHoveredAsState()
|
||||||
|
var vDragging by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onSizeChanged { vTotalWidthPx = it.width },
|
||||||
|
) {
|
||||||
|
val vClampedFirstPx = if (vTotalWidthPx > 0)
|
||||||
|
vFirstWidthPx.coerceIn(vMinFirstPx, (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
|
||||||
|
else vFirstWidthPx
|
||||||
|
val vSecondWidthPx = (vTotalWidthPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
|
||||||
|
|
||||||
|
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
||||||
|
|
||||||
|
// Solid divider that fills its own slot — what you see is exactly what
|
||||||
|
// you can grab. Hover or an in-progress drag just changes the colour (no
|
||||||
|
// size change → no layout shift, and no flicker when the pointer briefly
|
||||||
|
// leaves the slot mid-drag).
|
||||||
|
val vActive = vDividerHover || vDragging
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(SplitPaneDefaults.DividerThickness)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(if (vActive) dividerHoverColor else dividerColor)
|
||||||
|
.hoverable(vDividerSource)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { vDragging = true },
|
||||||
|
onDragEnd = { vDragging = false },
|
||||||
|
onDragCancel = { vDragging = false },
|
||||||
|
onDrag = { _, delta ->
|
||||||
|
val vMax = (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
|
||||||
|
vFirstWidthPx = (vFirstWidthPx + delta.x.toInt()).coerceIn(vMinFirstPx, vMax)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = Modifier.width(with(vDensity) { vSecondWidthPx.toDp() }).fillMaxHeight()) { second() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalSplitPane(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
initialFirstSize: Dp = 150.dp,
|
||||||
|
minFirstSize: Dp = 50.dp,
|
||||||
|
minSecondSize: Dp = 50.dp,
|
||||||
|
dividerColor: Color = Color(0x33FFFFFFL),
|
||||||
|
dividerHoverColor: Color = Color(0x66FFFFFFL),
|
||||||
|
first: @Composable () -> Unit,
|
||||||
|
second: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val vDensity = LocalDensity.current
|
||||||
|
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
|
||||||
|
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
|
||||||
|
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
|
||||||
|
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
|
||||||
|
|
||||||
|
var vTotalHeightPx by remember { mutableStateOf(0) }
|
||||||
|
var vFirstHeightPx by remember { mutableStateOf(vInitialFirstPx) }
|
||||||
|
val vDividerSource = remember { MutableInteractionSource() }
|
||||||
|
val vDividerHover by vDividerSource.collectIsHoveredAsState()
|
||||||
|
var vDragging by remember { mutableStateOf(false) }
|
||||||
|
var vPressStartFirstPx by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onSizeChanged { vTotalHeightPx = it.height },
|
||||||
|
) {
|
||||||
|
val vClampedFirstPx = if (vTotalHeightPx > 0)
|
||||||
|
vFirstHeightPx.coerceIn(vMinFirstPx, (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
|
||||||
|
else vFirstHeightPx
|
||||||
|
val vSecondHeightPx = (vTotalHeightPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
|
||||||
|
|
||||||
|
Box(modifier = Modifier.height(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxWidth()) { first() }
|
||||||
|
|
||||||
|
// Solid divider that fills its slot; hover/drag only changes the colour
|
||||||
|
// (no size change → no layout shift, no flicker mid-drag).
|
||||||
|
val vActive = vDividerHover || vDragging
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(SplitPaneDefaults.DividerThickness)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(if (vActive) dividerHoverColor else dividerColor)
|
||||||
|
.hoverable(vDividerSource)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { vDragging = true },
|
||||||
|
onDragEnd = { vDragging = false },
|
||||||
|
onDragCancel = { vDragging = false },
|
||||||
|
onDrag = { _, delta ->
|
||||||
|
val vMax = (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
|
||||||
|
vFirstHeightPx = (vFirstHeightPx + delta.y.toInt()).coerceIn(vMinFirstPx, vMax)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = Modifier.height(with(vDensity) { vSecondHeightPx.toDp() }).fillMaxWidth()) { second() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SplitPaneDefaults {
|
||||||
|
val DividerThickness: Dp = 4.dp // divider width = hit/drag area (solid; what you see is what you grab)
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
@file:OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||||
|
|
||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.loadSvgPainter
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import java.awt.Desktop
|
||||||
|
import java.awt.EventQueue
|
||||||
|
import java.awt.FileDialog
|
||||||
|
import java.awt.Frame
|
||||||
|
import java.awt.datatransfer.StringSelection
|
||||||
|
import java.io.File
|
||||||
|
import org.jetbrains.skia.Image
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: JVM actuals — AWT + upstream Compose Desktop
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
|
||||||
|
private val kIsMac = System.getProperty("os.name").startsWith("Mac")
|
||||||
|
|
||||||
|
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
|
||||||
|
|
||||||
|
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
|
||||||
|
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Window hooks — the shared App installs them; MainJvm wires them into the
|
||||||
|
// upstream Window() (onCloseRequest / onPreviewKeyEvent).
|
||||||
|
|
||||||
|
internal var jvmOnCloseRequest: (() -> Boolean)? = null
|
||||||
|
internal var jvmOnKeyShortcut: ((KeyEvent) -> Boolean)? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
|
||||||
|
androidx.compose.runtime.DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
|
||||||
|
jvmOnCloseRequest = inOnCloseRequest
|
||||||
|
jvmOnKeyShortcut = inOnKeyShortcut
|
||||||
|
onDispose { jvmOnCloseRequest = null; jvmOnKeyShortcut = null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirrors SDL_GetPrefPath's layout: <os user-data dir>/<org>/<app>/ with a
|
||||||
|
trailing separator, created on demand. */
|
||||||
|
actual fun appDataDir(inOrg: String, inApp: String): String? {
|
||||||
|
val vHome = System.getProperty("user.home") ?: return null
|
||||||
|
val vBase = when {
|
||||||
|
kIsWindows -> System.getenv("APPDATA") ?: "$vHome/AppData/Roaming"
|
||||||
|
kIsMac -> "$vHome/Library/Application Support"
|
||||||
|
else -> System.getenv("XDG_DATA_HOME") ?: "$vHome/.local/share"
|
||||||
|
}
|
||||||
|
val vDir = File(File(vBase, inOrg), inApp)
|
||||||
|
if (!vDir.exists() && !vDir.mkdirs()) return null
|
||||||
|
return vDir.absolutePath + File.separator
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Opens the path's containing folder (AWT has no cross-platform "select in
|
||||||
|
file manager"); fire-and-forget off the EDT. */
|
||||||
|
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) {
|
||||||
|
Thread {
|
||||||
|
val vOk = runCatching {
|
||||||
|
val vFile = File(inPath)
|
||||||
|
Desktop.getDesktop().open(if (vFile.isDirectory) vFile else vFile.parentFile ?: vFile)
|
||||||
|
}.isSuccess
|
||||||
|
inOnResult?.invoke(vOk)
|
||||||
|
}.apply { isDaemon = true }.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun fileManagerName(): String = when {
|
||||||
|
kIsWindows -> "Explorer"
|
||||||
|
kIsMac -> "Finder"
|
||||||
|
else -> "Files"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AWT FileDialog runs its own nested event loop, so invoking from the EDT
|
||||||
|
(where Compose Desktop also runs) keeps the UI serviced while modal. */
|
||||||
|
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) {
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
val vDlg = FileDialog(null as Frame?, "Save", FileDialog.SAVE)
|
||||||
|
inDefaultName?.let { vDlg.file = it }
|
||||||
|
vDlg.isVisible = true
|
||||||
|
inOnResult(vDlg.file?.let { vDlg.directory + it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) {
|
||||||
|
EventQueue.invokeLater {
|
||||||
|
val vDlg = FileDialog(null as Frame?, "Open", FileDialog.LOAD)
|
||||||
|
vDlg.isVisible = true
|
||||||
|
inOnResult(vDlg.file?.let { vDlg.directory + it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upstream desktop's ClipEntry wraps an AWT Transferable. */
|
||||||
|
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry(StringSelection(inText))
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// In-memory response images
|
||||||
|
|
||||||
|
private val kMemoryImages = HashMap<String, ByteArray>()
|
||||||
|
|
||||||
|
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) { kMemoryImages[inKey] = inBytes }
|
||||||
|
|
||||||
|
actual fun removeMemoryImage(inKey: String) { kMemoryImages.remove(inKey) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter {
|
||||||
|
val vDensity = LocalDensity.current
|
||||||
|
return remember(inKey, inSvg) {
|
||||||
|
val vBytes = kMemoryImages[inKey]
|
||||||
|
runCatching {
|
||||||
|
if (vBytes == null) error("no bytes for $inKey")
|
||||||
|
if (inSvg) loadSvgPainter(vBytes.inputStream(), vDensity)
|
||||||
|
else BitmapPainter(Image.makeFromEncoded(vBytes).toComposeImageBitmap())
|
||||||
|
}.getOrElse { BitmapPainter(ImageBitmap(1, 1)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Text layout
|
||||||
|
|
||||||
|
/* Upstream TextMeasurer at density 1 so the px-valued font size maps 1:1
|
||||||
|
(the caller passes an already density-scaled size). */
|
||||||
|
private val kRowMeasurer: TextMeasurer by lazy {
|
||||||
|
TextMeasurer(createFontFamilyResolver(), Density(1f), LayoutDirection.Ltr)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
|
||||||
|
runCatching {
|
||||||
|
kRowMeasurer.measure(
|
||||||
|
AnnotatedString(inText),
|
||||||
|
TextStyle(fontSize = inFontPx.sp, fontFamily = monoFontFamily ?: FontFamily.Monospace),
|
||||||
|
constraints = Constraints(maxWidth = inMaxWidthPx),
|
||||||
|
).lineCount
|
||||||
|
}.getOrDefault(1)
|
||||||
|
|
||||||
|
/* The jvm parity app only stores the preference — upstream's text pipeline has
|
||||||
|
no global tab-width knob. Backed by state so menu checkmarks recompose. */
|
||||||
|
private var fTabWidth by mutableStateOf(4)
|
||||||
|
actual var editorTabWidth: Int
|
||||||
|
get() = fTabWidth
|
||||||
|
set(value) { fTabWidth = value }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.platform.Font
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Monospace body font — JVM actuals (classpath font/ staged by
|
||||||
|
// jvmProcessResources)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
actual val monoFontFamily: FontFamily? by lazy {
|
||||||
|
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
|
||||||
|
if (vBytes == null) {
|
||||||
|
println("apidemo: NotoSansMono.ttf not on the classpath — body uses the default font")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
FontFamily(Font(identity = kMonoFamily, data = vBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val monoFontFamilyName: String? by lazy {
|
||||||
|
if (monoFontFamily != null) kMonoFamily else null
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
|
||||||
|
// The JVM comparison app's entry point. Runs the SAME shared App() as the
|
||||||
|
// native :apidemo, but on Compose Desktop (JVM) against upstream
|
||||||
|
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only
|
||||||
|
// (they drive the bundled libcurl); the jvm actuals report that instead.
|
||||||
|
fun main() = application {
|
||||||
|
Window(
|
||||||
|
// The shared App installs a persist-then-close hook (InstallWindowHooks).
|
||||||
|
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
|
||||||
|
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
|
||||||
|
title = "API Manager — JVM (upstream Compose)",
|
||||||
|
state = rememberWindowState(width = 1240.dp, height = 820.dp),
|
||||||
|
) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: TLS chain / client-certificate — JVM actuals (native-only feature)
|
||||||
|
// ==================
|
||||||
|
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
|
||||||
|
// there is no libcurl on the jvm parity stack, so both entry points report
|
||||||
|
// the limitation instead of half-implementing it over JSSE.
|
||||||
|
|
||||||
|
private const val kNativeOnly =
|
||||||
|
"Client-certificate (mTLS) features need the native build — they drive the bundled libcurl directly."
|
||||||
|
|
||||||
|
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly)
|
||||||
|
|
||||||
|
actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse = ApiResponse(
|
||||||
|
ok = false,
|
||||||
|
status = 0,
|
||||||
|
statusText = "",
|
||||||
|
timeMs = 0,
|
||||||
|
sizeBytes = 0,
|
||||||
|
headers = emptyList(),
|
||||||
|
body = "",
|
||||||
|
error = kNativeOnly,
|
||||||
|
)
|
||||||
|
|
||||||
|
actual fun sweepTempClientCerts() {}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import com.compose.sdl.LocalComposeNativeWindow
|
||||||
|
import com.compose.sdl.TextLayoutConfig
|
||||||
|
import com.compose.sdl.registerMemoryResource
|
||||||
|
import com.compose.sdl.removeMemoryResource
|
||||||
|
import com.compose.sdl.res.ResourceKind
|
||||||
|
import com.compose.sdl.text.currentTextMeasurer
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Native actuals — pure delegation to the port's SDL-backed APIs
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
actual fun appDataDir(inOrg: String, inApp: String): String? =
|
||||||
|
com.compose.sdl.appDataDir(inOrg, inApp)
|
||||||
|
|
||||||
|
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) =
|
||||||
|
com.compose.sdl.revealInFileManager(inPath, inOnResult)
|
||||||
|
|
||||||
|
actual fun fileManagerName(): String = com.compose.sdl.fileManagerName()
|
||||||
|
|
||||||
|
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) =
|
||||||
|
com.compose.sdl.showSaveFileDialog(inDefaultName, inOnResult)
|
||||||
|
|
||||||
|
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) =
|
||||||
|
com.compose.sdl.showOpenFileDialog(inOnResult)
|
||||||
|
|
||||||
|
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry.withPlainText(inText)
|
||||||
|
|
||||||
|
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) = registerMemoryResource(inKey, inBytes)
|
||||||
|
|
||||||
|
actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
||||||
|
com.compose.sdl.res.painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
||||||
|
|
||||||
|
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
|
||||||
|
currentTextMeasurer.wrap(inText, inFontPx, inMaxWidthPx, inFamilyName).lines.size
|
||||||
|
|
||||||
|
actual var editorTabWidth: Int
|
||||||
|
get() = TextLayoutConfig.tabWidth
|
||||||
|
set(value) { TextLayoutConfig.tabWidth = value }
|
||||||
|
|
||||||
|
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
|
||||||
|
|
||||||
|
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
|
||||||
|
io.ktor.client.HttpClient(io.ktor.client.engine.curl.Curl)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
|
||||||
|
val vWindow = LocalComposeNativeWindow.current
|
||||||
|
DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
|
||||||
|
vWindow.setOnCloseRequest(inOnCloseRequest)
|
||||||
|
vWindow.setOnKeyShortcut(inOnKeyShortcut)
|
||||||
|
onDispose { vWindow.setOnCloseRequest(null); vWindow.setOnKeyShortcut(null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,21 +39,10 @@ class PreparedCert(
|
|||||||
Throws with a user-facing message if the cert/key can't be loaded. */
|
Throws with a user-facing message if the cert/key can't be loaded. */
|
||||||
expect fun prepareClientCert(inReq: ApiRequest): PreparedCert
|
expect fun prepareClientCert(inReq: ApiRequest): PreparedCert
|
||||||
|
|
||||||
/* Remove any temporary client certs (and their key containers) a previous run
|
// ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
|
||||||
left behind in the Windows store after a crash — identified by our prefix, so
|
// curlSendWithClientCert expects live in commonMain (TlsCommon.kt) — the UI
|
||||||
the user's own certificates are never touched. No-op off Windows. Call once
|
// consumes them from shared code; the per-OS ClientCert*.kt actuals below the
|
||||||
at startup. */
|
// native tree actualize sweepTempClientCerts directly.
|
||||||
expect fun sweepTempClientCerts()
|
|
||||||
|
|
||||||
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
|
||||||
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
|
||||||
presented it. Derived certs — an issuer resolved from the OS store, or a
|
|
||||||
name-only placeholder — have fromServer = false and are drawn dotted. */
|
|
||||||
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
|
|
||||||
|
|
||||||
/* The server's TLS certificate chain. error is set instead when it couldn't be
|
|
||||||
fetched; fields vary by TLS backend. */
|
|
||||||
class TlsChain(val certs: List<ChainCert>, val error: String?)
|
|
||||||
|
|
||||||
/* Continue a server-presented chain with its issuer(s). Where possible the
|
/* Continue a server-presented chain with its issuer(s). Where possible the
|
||||||
issuer is resolved from the OS certificate store with full info; otherwise the
|
issuer is resolved from the OS certificate store with full info; otherwise the
|
||||||
@@ -120,7 +109,7 @@ private fun onCurlDiscard(inBuffer: CPointer<ByteVar>, inSize: size_t, inCount:
|
|||||||
request's client certificate if it has one (so mTLS endpoints work too).
|
request's client certificate if it has one (so mTLS endpoints work too).
|
||||||
inReq is already var-resolved. */
|
inReq is already var-resolved. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
||||||
{
|
{
|
||||||
val vCert = try { if (inReq.hasClientCert) prepareClientCert(inReq) else null }
|
val vCert = try { if (inReq.hasClientCert) prepareClientCert(inReq) else null }
|
||||||
catch (e: Throwable) { return TlsChain(emptyList(), e.message ?: "Client certificate error") }
|
catch (e: Throwable) { return TlsChain(emptyList(), e.message ?: "Client certificate error") }
|
||||||
@@ -185,7 +174,7 @@ private fun readCertInfo(inCurl: COpaquePointer): List<List<Pair<String, String>
|
|||||||
/* Send a client-certificate request through libcurl and adapt the result into
|
/* Send a client-certificate request through libcurl and adapt the result into
|
||||||
the same ApiResponse the Ktor path produces. inReq is already var-resolved. */
|
the same ApiResponse the Ktor path produces. inReq is already var-resolved. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
||||||
{
|
{
|
||||||
val vMark = TimeSource.Monotonic.markNow()
|
val vMark = TimeSource.Monotonic.markNow()
|
||||||
val vCert = try { prepareClientCert(inReq) }
|
val vCert = try { prepareClientCert(inReq) }
|
||||||
|
|||||||
@@ -6,17 +6,10 @@ import com.compose.sdl.loadComposeResourceBytes
|
|||||||
import com.compose.sdl.text.namedFontFamily
|
import com.compose.sdl.text.namedFontFamily
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Monospace body font
|
// MARK: Monospace body font — native actuals (data.kres + IconFont)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
// Family name the body editor passes for monospace text. Resolves to the bundled
|
actual val monoFontFamilyName: String? by lazy {
|
||||||
// Noto Sans Mono once registered; null means the font isn't in data.kres, in
|
|
||||||
// which case the body falls back to the default (proportional Noto Sans).
|
|
||||||
const val kMonoFamily = "noto-mono"
|
|
||||||
|
|
||||||
// The raw project family name string — used by the SyntaxHighlight tokeniser's
|
|
||||||
// wrap() call which measures via NativeTextMeasurer (accepts a name string).
|
|
||||||
val monoFontFamilyName: String? by lazy {
|
|
||||||
val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf")
|
val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf")
|
||||||
if (vBytes == null) {
|
if (vBytes == null) {
|
||||||
println("apidemo: NotoSansMono.ttf not bundled — body uses the default font")
|
println("apidemo: NotoSansMono.ttf not bundled — body uses the default font")
|
||||||
@@ -27,9 +20,6 @@ val monoFontFamilyName: String? by lazy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The material3 Text / BasicTextField-shaped FontFamily wrapper. Wraps the
|
actual val monoFontFamily: FontFamily? by lazy {
|
||||||
// registered project family in a `NamedFont`-backed FontFamily so the m3 Text
|
|
||||||
// signature (which takes `FontFamily?` proper) can be given the mono family.
|
|
||||||
val monoFontFamily: FontFamily? by lazy {
|
|
||||||
monoFontFamilyName?.let { namedFontFamily(it) }
|
monoFontFamilyName?.let { namedFontFamily(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+41
-4
@@ -5,11 +5,12 @@
|
|||||||
// apply the multiplatform plugin. Each subproject still applies the ones it needs.
|
// apply the multiplatform plugin. Each subproject still applies the ones it needs.
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||||
|
alias(libs.plugins.kotlin.jvm) apply false
|
||||||
alias(libs.plugins.kotlin.plugin.compose) apply false
|
alias(libs.plugins.kotlin.plugin.compose) apply false
|
||||||
alias(libs.plugins.kotlin.plugin.serialization) apply false
|
alias(libs.plugins.kotlin.plugin.serialization) apply false
|
||||||
alias(libs.plugins.compose.multiplatform) apply false
|
alias(libs.plugins.compose.multiplatform) apply false
|
||||||
// Public-ABI dumps (api/<module>.klib.api). Used by scripts/compose-fidelity-check.py
|
// Public-ABI dumps (api/<module>.klib.api). Used by scripts/compose-coverage.py
|
||||||
// to diff our androidx.compose.* surface against the official Compose klib ABI.
|
// to diff our mirrored API surface against the official upstream klib ABI dumps.
|
||||||
alias(libs.plugins.binary.compatibility.validator)
|
alias(libs.plugins.binary.compatibility.validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ plugins {
|
|||||||
// surface worth tracking, so skip them.
|
// surface worth tracking, so skip them.
|
||||||
apiValidation {
|
apiValidation {
|
||||||
klib { enabled = true }
|
klib { enabled = true }
|
||||||
ignoredProjects.addAll(listOf("demo", "apidemo"))
|
// compose-desktop-native-bridge is a plain JVM Gradle plugin — no klib ABI to track.
|
||||||
|
ignoredProjects.addAll(listOf("demo", "apidemo", "compose-desktop-native-bridge"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project coordinates. `group` is baked into every module's Kotlin/Native klib
|
// Project coordinates. `group` is baked into every module's Kotlin/Native klib
|
||||||
@@ -47,6 +49,7 @@ allprojects {
|
|||||||
val kAppModules = setOf(":demo", ":apidemo")
|
val kAppModules = setOf(":demo", ":apidemo")
|
||||||
val kPublishedLibs = setOf(
|
val kPublishedLibs = setOf(
|
||||||
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
|
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
|
||||||
|
":ui-tooling-preview",
|
||||||
":animation-core", ":animation", ":animation-graphics",
|
":animation-core", ":animation", ":animation-graphics",
|
||||||
":foundation", ":foundation-layout",
|
":foundation", ":foundation-layout",
|
||||||
":material3", ":material-ripple",
|
":material3", ":material-ripple",
|
||||||
@@ -108,10 +111,44 @@ subprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL
|
||||||
|
// Maven Compose artifacts in its commonMain so metadata + jvm resolve them
|
||||||
|
// (e.g. :material-symbols' common API); every NATIVE target configuration
|
||||||
|
// swaps those modules for the port's project equivalents — the Maven
|
||||||
|
// artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is
|
||||||
|
// deliberately NOT here: the port uses the official runtime klibs everywhere.
|
||||||
|
// (:demo carries the same pattern locally for components-resources.)
|
||||||
|
val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64")
|
||||||
|
allprojects {
|
||||||
|
configurations.configureEach {
|
||||||
|
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
||||||
|
resolutionStrategy.dependencySubstitution {
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
|
||||||
|
// ui's split artifacts: ui-graphics + ui-text are merged INTO
|
||||||
|
// :ui in the port; ui-unit / ui-geometry have their own modules.
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui"))
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui"))
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit"))
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
|
||||||
|
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":ui-tooling-preview"))
|
||||||
|
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":foundation"))
|
||||||
|
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":foundation-layout"))
|
||||||
|
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":animation"))
|
||||||
|
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":animation-core"))
|
||||||
|
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":material3"))
|
||||||
|
// navigation3-ui: the JB Maven artifact has no K/N desktop
|
||||||
|
// klibs — the port vendors it as :navigation3-ui.
|
||||||
|
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3-ui"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Whether the current host can build the mingwX64 target. Kotlin/Native can
|
// Whether the current host can build the mingwX64 target. Kotlin/Native can
|
||||||
// only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf /
|
// only cross-compile mingwX64 cinterops from a Windows host — the sdl3_ttf /
|
||||||
// sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by
|
// sdl3_image cinterops need Windows SDL3 headers under libs/ (produced by
|
||||||
// scripts/build-sdl/build-*.sh, which only runs on Windows). Declaring `mingwX64()` on
|
// scripts/build-sdl/build-all.py run on a Windows host). Declaring `mingwX64()` on
|
||||||
// a non-Windows host is safe for pure-Kotlin modules but blows up the moment
|
// a non-Windows host is safe for pure-Kotlin modules but blows up the moment
|
||||||
// a `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h.
|
// a `depends = sdl3` cinterop tries to include SDL3_ttf/SDL_ttf.h.
|
||||||
// Override with `-PforceMingw=true` if you actually have the headers wired.
|
// Override with `-PforceMingw=true` if you actually have the headers wired.
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// Klib ABI Dump
|
||||||
|
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
|
||||||
|
// Rendering settings:
|
||||||
|
// - Signature version: 2
|
||||||
|
// - Show manifest properties: true
|
||||||
|
// - Show declarations: true
|
||||||
|
|
||||||
|
// Library unique name: <com.bitsycore.compose.sdl:components-resources>
|
||||||
|
open annotation class org.jetbrains.compose.resources/ExperimentalResourceApi : kotlin/Annotation { // org.jetbrains.compose.resources/ExperimentalResourceApi|null[0]
|
||||||
|
constructor <init>() // org.jetbrains.compose.resources/ExperimentalResourceApi.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class org.jetbrains.compose.resources/InternalResourceApi : kotlin/Annotation { // org.jetbrains.compose.resources/InternalResourceApi|null[0]
|
||||||
|
constructor <init>() // org.jetbrains.compose.resources/InternalResourceApi.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class org.jetbrains.compose.resources/ResourceContentHash : kotlin/Annotation { // org.jetbrains.compose.resources/ResourceContentHash|null[0]
|
||||||
|
constructor <init>(kotlin/Int) // org.jetbrains.compose.resources/ResourceContentHash.<init>|<init>(kotlin.Int){}[0]
|
||||||
|
|
||||||
|
final val hash // org.jetbrains.compose.resources/ResourceContentHash.hash|{}hash[0]
|
||||||
|
final fun <get-hash>(): kotlin/Int // org.jetbrains.compose.resources/ResourceContentHash.hash.<get-hash>|<get-hash>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final enum class org.jetbrains.compose.resources/DensityQualifier : kotlin/Enum<org.jetbrains.compose.resources/DensityQualifier>, org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/DensityQualifier|null[0]
|
||||||
|
enum entry HDPI // org.jetbrains.compose.resources/DensityQualifier.HDPI|null[0]
|
||||||
|
enum entry LDPI // org.jetbrains.compose.resources/DensityQualifier.LDPI|null[0]
|
||||||
|
enum entry MDPI // org.jetbrains.compose.resources/DensityQualifier.MDPI|null[0]
|
||||||
|
enum entry XHDPI // org.jetbrains.compose.resources/DensityQualifier.XHDPI|null[0]
|
||||||
|
enum entry XXHDPI // org.jetbrains.compose.resources/DensityQualifier.XXHDPI|null[0]
|
||||||
|
enum entry XXXHDPI // org.jetbrains.compose.resources/DensityQualifier.XXXHDPI|null[0]
|
||||||
|
|
||||||
|
final val dpi // org.jetbrains.compose.resources/DensityQualifier.dpi|{}dpi[0]
|
||||||
|
final fun <get-dpi>(): kotlin/Int // org.jetbrains.compose.resources/DensityQualifier.dpi.<get-dpi>|<get-dpi>(){}[0]
|
||||||
|
final val entries // org.jetbrains.compose.resources/DensityQualifier.entries|#static{}entries[0]
|
||||||
|
final fun <get-entries>(): kotlin.enums/EnumEntries<org.jetbrains.compose.resources/DensityQualifier> // org.jetbrains.compose.resources/DensityQualifier.entries.<get-entries>|<get-entries>#static(){}[0]
|
||||||
|
|
||||||
|
final fun valueOf(kotlin/String): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.valueOf|valueOf#static(kotlin.String){}[0]
|
||||||
|
final fun values(): kotlin/Array<org.jetbrains.compose.resources/DensityQualifier> // org.jetbrains.compose.resources/DensityQualifier.values|values#static(){}[0]
|
||||||
|
|
||||||
|
final object Companion { // org.jetbrains.compose.resources/DensityQualifier.Companion|null[0]
|
||||||
|
final fun selectByDensity(kotlin/Float): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.Companion.selectByDensity|selectByDensity(kotlin.Float){}[0]
|
||||||
|
final fun selectByValue(kotlin/Int): org.jetbrains.compose.resources/DensityQualifier // org.jetbrains.compose.resources/DensityQualifier.Companion.selectByValue|selectByValue(kotlin.Int){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final enum class org.jetbrains.compose.resources/ThemeQualifier : kotlin/Enum<org.jetbrains.compose.resources/ThemeQualifier>, org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/ThemeQualifier|null[0]
|
||||||
|
enum entry DARK // org.jetbrains.compose.resources/ThemeQualifier.DARK|null[0]
|
||||||
|
enum entry LIGHT // org.jetbrains.compose.resources/ThemeQualifier.LIGHT|null[0]
|
||||||
|
|
||||||
|
final val entries // org.jetbrains.compose.resources/ThemeQualifier.entries|#static{}entries[0]
|
||||||
|
final fun <get-entries>(): kotlin.enums/EnumEntries<org.jetbrains.compose.resources/ThemeQualifier> // org.jetbrains.compose.resources/ThemeQualifier.entries.<get-entries>|<get-entries>#static(){}[0]
|
||||||
|
|
||||||
|
final fun valueOf(kotlin/String): org.jetbrains.compose.resources/ThemeQualifier // org.jetbrains.compose.resources/ThemeQualifier.valueOf|valueOf#static(kotlin.String){}[0]
|
||||||
|
final fun values(): kotlin/Array<org.jetbrains.compose.resources/ThemeQualifier> // org.jetbrains.compose.resources/ThemeQualifier.values|values#static(){}[0]
|
||||||
|
|
||||||
|
final object Companion { // org.jetbrains.compose.resources/ThemeQualifier.Companion|null[0]
|
||||||
|
final fun selectByValue(kotlin/Boolean): org.jetbrains.compose.resources/ThemeQualifier // org.jetbrains.compose.resources/ThemeQualifier.Companion.selectByValue|selectByValue(kotlin.Boolean){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface org.jetbrains.compose.resources/Qualifier // org.jetbrains.compose.resources/Qualifier|null[0]
|
||||||
|
|
||||||
|
abstract interface org.jetbrains.compose.resources/ResourceReader { // org.jetbrains.compose.resources/ResourceReader|null[0]
|
||||||
|
abstract fun getUri(kotlin/String): kotlin/String // org.jetbrains.compose.resources/ResourceReader.getUri|getUri(kotlin.String){}[0]
|
||||||
|
abstract suspend fun read(kotlin/String): kotlin/ByteArray // org.jetbrains.compose.resources/ResourceReader.read|read(kotlin.String){}[0]
|
||||||
|
abstract suspend fun readPart(kotlin/String, kotlin/Long, kotlin/Long): kotlin/ByteArray // org.jetbrains.compose.resources/ResourceReader.readPart|readPart(kotlin.String;kotlin.Long;kotlin.Long){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException : kotlin/Exception { // org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException|null[0]
|
||||||
|
constructor <init>(kotlin/String?) // org.jetbrains.compose.resources.vector.xmldom/MalformedXMLException.<init>|<init>(kotlin.String?){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/DrawableResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/DrawableResource|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/DrawableResource.<init>|<init>(kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/FontResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/FontResource|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/FontResource.<init>|<init>(kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/LanguageQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/LanguageQualifier|null[0]
|
||||||
|
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/LanguageQualifier.<init>|<init>(kotlin.String){}[0]
|
||||||
|
|
||||||
|
final val language // org.jetbrains.compose.resources/LanguageQualifier.language|{}language[0]
|
||||||
|
final fun <get-language>(): kotlin/String // org.jetbrains.compose.resources/LanguageQualifier.language.<get-language>|<get-language>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/LanguageQualifier.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/LanguageQualifier.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // org.jetbrains.compose.resources/LanguageQualifier.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/MissingResourceException : kotlin/Exception { // org.jetbrains.compose.resources/MissingResourceException|null[0]
|
||||||
|
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/MissingResourceException.<init>|<init>(kotlin.String){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/PluralStringResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/PluralStringResource|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/PluralStringResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
|
||||||
|
|
||||||
|
final val key // org.jetbrains.compose.resources/PluralStringResource.key|{}key[0]
|
||||||
|
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/PluralStringResource.key.<get-key>|<get-key>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/RegionQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/RegionQualifier|null[0]
|
||||||
|
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/RegionQualifier.<init>|<init>(kotlin.String){}[0]
|
||||||
|
|
||||||
|
final val region // org.jetbrains.compose.resources/RegionQualifier.region|{}region[0]
|
||||||
|
final fun <get-region>(): kotlin/String // org.jetbrains.compose.resources/RegionQualifier.region.<get-region>|<get-region>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/RegionQualifier.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/RegionQualifier.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // org.jetbrains.compose.resources/RegionQualifier.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/ResourceEnvironment { // org.jetbrains.compose.resources/ResourceEnvironment|null[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ResourceEnvironment.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ResourceEnvironment.hashCode|hashCode(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/ResourceItem { // org.jetbrains.compose.resources/ResourceItem|null[0]
|
||||||
|
constructor <init>(kotlin.collections/Set<org.jetbrains.compose.resources/Qualifier>, kotlin/String, kotlin/Long, kotlin/Long) // org.jetbrains.compose.resources/ResourceItem.<init>|<init>(kotlin.collections.Set<org.jetbrains.compose.resources.Qualifier>;kotlin.String;kotlin.Long;kotlin.Long){}[0]
|
||||||
|
|
||||||
|
final fun copy(kotlin.collections/Set<org.jetbrains.compose.resources/Qualifier> = ..., kotlin/String = ..., kotlin/Long = ..., kotlin/Long = ...): org.jetbrains.compose.resources/ResourceItem // org.jetbrains.compose.resources/ResourceItem.copy|copy(kotlin.collections.Set<org.jetbrains.compose.resources.Qualifier>;kotlin.String;kotlin.Long;kotlin.Long){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ResourceItem.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ResourceItem.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // org.jetbrains.compose.resources/ResourceItem.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/ScriptQualifier : org.jetbrains.compose.resources/Qualifier { // org.jetbrains.compose.resources/ScriptQualifier|null[0]
|
||||||
|
constructor <init>(kotlin/String) // org.jetbrains.compose.resources/ScriptQualifier.<init>|<init>(kotlin.String){}[0]
|
||||||
|
|
||||||
|
final val script // org.jetbrains.compose.resources/ScriptQualifier.script|{}script[0]
|
||||||
|
final fun <get-script>(): kotlin/String // org.jetbrains.compose.resources/ScriptQualifier.script.<get-script>|<get-script>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/ScriptQualifier.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/ScriptQualifier.hashCode|hashCode(){}[0]
|
||||||
|
final fun isEmpty(): kotlin/Boolean // org.jetbrains.compose.resources/ScriptQualifier.isEmpty|isEmpty(){}[0]
|
||||||
|
final fun toString(): kotlin/String // org.jetbrains.compose.resources/ScriptQualifier.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/StringArrayResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/StringArrayResource|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/StringArrayResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
|
||||||
|
|
||||||
|
final val key // org.jetbrains.compose.resources/StringArrayResource.key|{}key[0]
|
||||||
|
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/StringArrayResource.key.<get-key>|<get-key>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class org.jetbrains.compose.resources/StringResource : org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/StringResource|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin/String, kotlin.collections/Set<org.jetbrains.compose.resources/ResourceItem>) // org.jetbrains.compose.resources/StringResource.<init>|<init>(kotlin.String;kotlin.String;kotlin.collections.Set<org.jetbrains.compose.resources.ResourceItem>){}[0]
|
||||||
|
|
||||||
|
final val key // org.jetbrains.compose.resources/StringResource.key|{}key[0]
|
||||||
|
final fun <get-key>(): kotlin/String // org.jetbrains.compose.resources/StringResource.key.<get-key>|<get-key>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class org.jetbrains.compose.resources/Resource { // org.jetbrains.compose.resources/Resource|null[0]
|
||||||
|
open fun equals(kotlin/Any?): kotlin/Boolean // org.jetbrains.compose.resources/Resource.equals|equals(kotlin.Any?){}[0]
|
||||||
|
open fun hashCode(): kotlin/Int // org.jetbrains.compose.resources/Resource.hashCode|hashCode(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object org.jetbrains.compose.resources/ResourceCaches // org.jetbrains.compose.resources/ResourceCaches|null[0]
|
||||||
|
|
||||||
|
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop|#static{}org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop // org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop|#static{}org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/LocalResourceReader // org.jetbrains.compose.resources/LocalResourceReader|{}LocalResourceReader[0]
|
||||||
|
final fun <get-LocalResourceReader>(): androidx.compose.runtime/ProvidableCompositionLocal<org.jetbrains.compose.resources/ResourceReader> // org.jetbrains.compose.resources/LocalResourceReader.<get-LocalResourceReader>|<get-LocalResourceReader>(){}[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop|#static{}org_jetbrains_compose_resources_DrawableResource$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop|#static{}org_jetbrains_compose_resources_FontResource$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Svg$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop|#static{}org_jetbrains_compose_resources_ImageCache_Vector$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop|#static{}org_jetbrains_compose_resources_LanguageQualifier$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop|#static{}org_jetbrains_compose_resources_MissingResourceException$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop|#static{}org_jetbrains_compose_resources_PluralStringResource$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop|#static{}org_jetbrains_compose_resources_RegionQualifier$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop|#static{}org_jetbrains_compose_resources_Resource$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop|#static{}org_jetbrains_compose_resources_ResourceCaches$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop|#static{}org_jetbrains_compose_resources_ResourceEnvironment$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop|#static{}org_jetbrains_compose_resources_ResourceItem$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop|#static{}org_jetbrains_compose_resources_ScriptQualifier$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop|#static{}org_jetbrains_compose_resources_StringArrayResource$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Array$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Plurals$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop|#static{}org_jetbrains_compose_resources_StringItem_Value$stableprop[0]
|
||||||
|
final val org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop|#static{}org_jetbrains_compose_resources_StringResource$stableprop[0]
|
||||||
|
|
||||||
|
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // org.jetbrains.compose.resources/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0]
|
||||||
|
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToImageVector(androidx.compose.ui.unit/Density): androidx.compose.ui.graphics.vector/ImageVector // org.jetbrains.compose.resources/decodeToImageVector|decodeToImageVector@kotlin.ByteArray(androidx.compose.ui.unit.Density){}[0]
|
||||||
|
final fun (kotlin/ByteArray).org.jetbrains.compose.resources/decodeToSvgPainter(androidx.compose.ui.unit/Density): androidx.compose.ui.graphics.painter/Painter // org.jetbrains.compose.resources/decodeToSvgPainter|decodeToSvgPainter@kotlin.ByteArray(androidx.compose.ui.unit.Density){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_And$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_Or$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.plural/org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter|org_jetbrains_compose_resources_plural_PluralRule_Condition_Relation$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources.vector.xmldom/org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter|org_jetbrains_compose_resources_vector_xmldom_MalformedXMLException$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/Font(org.jetbrains.compose.resources/FontResource, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontStyle, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.ui.text.font/Font // org.jetbrains.compose.resources/Font|Font(org.jetbrains.compose.resources.FontResource;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontStyle;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/Font(org.jetbrains.compose.resources/FontResource, androidx.compose.ui.text.font/FontWeight?, androidx.compose.ui.text.font/FontStyle, androidx.compose.ui.text.font/FontVariation.Settings?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.ui.text.font/Font // org.jetbrains.compose.resources/Font|Font(org.jetbrains.compose.resources.FontResource;androidx.compose.ui.text.font.FontWeight?;androidx.compose.ui.text.font.FontStyle;androidx.compose.ui.text.font.FontVariation.Settings?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/getResourceUri(kotlin/String): kotlin/String // org.jetbrains.compose.resources/getResourceUri|getResourceUri(kotlin.String){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/getSystemResourceEnvironment(): org.jetbrains.compose.resources/ResourceEnvironment // org.jetbrains.compose.resources/getSystemResourceEnvironment|getSystemResourceEnvironment(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/imageResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/ImageBitmap // org.jetbrains.compose.resources/imageResource|imageResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_DrawableResource$stableprop_getter|org_jetbrains_compose_resources_DrawableResource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_FontResource$stableprop_getter|org_jetbrains_compose_resources_FontResource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Bitmap$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Svg$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter|org_jetbrains_compose_resources_ImageCache_Vector$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter|org_jetbrains_compose_resources_LanguageQualifier$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_MissingResourceException$stableprop_getter|org_jetbrains_compose_resources_MissingResourceException$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_PluralStringResource$stableprop_getter|org_jetbrains_compose_resources_PluralStringResource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_RegionQualifier$stableprop_getter|org_jetbrains_compose_resources_RegionQualifier$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_Resource$stableprop_getter|org_jetbrains_compose_resources_Resource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceCaches$stableprop_getter|org_jetbrains_compose_resources_ResourceCaches$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter|org_jetbrains_compose_resources_ResourceEnvironment$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ResourceItem$stableprop_getter|org_jetbrains_compose_resources_ResourceItem$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter|org_jetbrains_compose_resources_ScriptQualifier$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringArrayResource$stableprop_getter|org_jetbrains_compose_resources_StringArrayResource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Array$stableprop_getter|org_jetbrains_compose_resources_StringItem_Array$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter|org_jetbrains_compose_resources_StringItem_Plurals$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringItem_Value$stableprop_getter|org_jetbrains_compose_resources_StringItem_Value$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop_getter(): kotlin/Int // org.jetbrains.compose.resources/org_jetbrains_compose_resources_StringResource$stableprop_getter|org_jetbrains_compose_resources_StringResource$stableprop_getter(){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/painterResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics.painter/Painter // org.jetbrains.compose.resources/painterResource|painterResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/pluralStringResource(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/pluralStringResource|pluralStringResource(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/pluralStringResource(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>..., androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/pluralStringResource|pluralStringResource(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/rememberResourceEnvironment(androidx.compose.runtime/Composer?, kotlin/Int): org.jetbrains.compose.resources/ResourceEnvironment // org.jetbrains.compose.resources/rememberResourceEnvironment|rememberResourceEnvironment(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/stringArrayResource(org.jetbrains.compose.resources/StringArrayResource, androidx.compose.runtime/Composer?, kotlin/Int): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/stringArrayResource|stringArrayResource(org.jetbrains.compose.resources.StringArrayResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/stringResource(org.jetbrains.compose.resources/StringResource, androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/stringResource|stringResource(org.jetbrains.compose.resources.StringResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/stringResource(org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>..., androidx.compose.runtime/Composer?, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/stringResource|stringResource(org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun org.jetbrains.compose.resources/vectorResource(org.jetbrains.compose.resources/DrawableResource, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics.vector/ImageVector // org.jetbrains.compose.resources/vectorResource|vectorResource(org.jetbrains.compose.resources.DrawableResource;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getDrawableResourceBytes(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/DrawableResource): kotlin/ByteArray // org.jetbrains.compose.resources/getDrawableResourceBytes|getDrawableResourceBytes(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.DrawableResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getFontResourceBytes(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/FontResource): kotlin/ByteArray // org.jetbrains.compose.resources/getFontResourceBytes|getFontResourceBytes(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.FontResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/PluralStringResource, kotlin/Int): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.PluralStringResource;kotlin.Int){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getPluralString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/PluralStringResource, kotlin/Int, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getPluralString|getPluralString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.PluralStringResource;kotlin.Int;kotlin.Array<out|kotlin.Any>...){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringResource): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/StringResource): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.StringResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getString(org.jetbrains.compose.resources/StringResource, kotlin/Array<out kotlin/Any>...): kotlin/String // org.jetbrains.compose.resources/getString|getString(org.jetbrains.compose.resources.StringResource;kotlin.Array<out|kotlin.Any>...){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getStringArray(org.jetbrains.compose.resources/ResourceEnvironment, org.jetbrains.compose.resources/StringArrayResource): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/getStringArray|getStringArray(org.jetbrains.compose.resources.ResourceEnvironment;org.jetbrains.compose.resources.StringArrayResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/getStringArray(org.jetbrains.compose.resources/StringArrayResource): kotlin.collections/List<kotlin/String> // org.jetbrains.compose.resources/getStringArray|getStringArray(org.jetbrains.compose.resources.StringArrayResource){}[0]
|
||||||
|
final suspend fun org.jetbrains.compose.resources/readResourceBytes(kotlin/String): kotlin/ByteArray // org.jetbrains.compose.resources/readResourceBytes|readResourceBytes(kotlin.String){}[0]
|
||||||
+2
@@ -16,6 +16,8 @@ package org.jetbrains.compose.resources.vector.xmldom
|
|||||||
CDATA skipping, and the five predefined entities plus numeric character
|
CDATA skipping, and the five predefined entities plus numeric character
|
||||||
references. No DTD expansion, no processing-instruction handling beyond
|
references. No DTD expansion, no processing-instruction handling beyond
|
||||||
skipping — malformed input throws MalformedXMLException like upstream. */
|
skipping — malformed input throws MalformedXMLException like upstream. */
|
||||||
|
// VENDOR-BASE(COMPOSE_REF): components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @ v1.12.0-beta03+dev4483
|
||||||
|
// (fresh REIMPL, not a copy-edit — the base ref marks the upstream API it tracks)
|
||||||
|
|
||||||
internal fun parse(xml: String): Element {
|
internal fun parse(xml: String): Element {
|
||||||
val vParser = XmlDomParser(xml)
|
val vParser = XmlDomParser(xml)
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ import com.compose.sdl.graphics.decodeEncodedImageBitmap
|
|||||||
drawables ship at a single density, so no decode-time rescale applies. */
|
drawables ship at a single density, so no decode-time rescale applies. */
|
||||||
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
|
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
|
||||||
decodeEncodedImageBitmap(this)
|
decodeEncodedImageBitmap(this)
|
||||||
?: error("Image decode failed — is the SDL renderer initialised before painterResource ran?")
|
?: error("Image decode failed — is the render backend initialised before painterResource ran?")
|
||||||
|
|
||||||
/* SVG on SDL: SDL3_image rasterises SVG at its intrinsic size (IMG_LoadSVG),
|
/* SVG on SDL: SDL3_image rasterises SVG at its intrinsic size (IMG_LoadSVG),
|
||||||
so the "element" is just the raw bytes and the painter is the rasterised
|
so the "element" is just the raw bytes and the painter is the rasterised
|
||||||
@@ -34,5 +34,5 @@ internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
|
|||||||
internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
|
internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
|
||||||
BitmapPainter(
|
BitmapPainter(
|
||||||
decodeEncodedImageBitmap(bytes)
|
decodeEncodedImageBitmap(bytes)
|
||||||
?: error("SVG rasterisation failed — is the SDL renderer initialised before painterResource ran?")
|
?: error("SVG rasterisation failed — is the render backend initialised before painterResource ran?")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2078,17 +2078,6 @@ final object com.compose.sdl.icons/IconDefaults { // com.compose.sdl.icons/IconD
|
|||||||
final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0]
|
final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object com.compose.sdl.icons/MaterialIconAxisDefaults { // com.compose.sdl.icons/MaterialIconAxisDefaults|null[0]
|
|
||||||
final const val Fill // com.compose.sdl.icons/MaterialIconAxisDefaults.Fill|{}Fill[0]
|
|
||||||
final fun <get-Fill>(): kotlin/Float // com.compose.sdl.icons/MaterialIconAxisDefaults.Fill.<get-Fill>|<get-Fill>(){}[0]
|
|
||||||
final const val Grade // com.compose.sdl.icons/MaterialIconAxisDefaults.Grade|{}Grade[0]
|
|
||||||
final fun <get-Grade>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.Grade.<get-Grade>|<get-Grade>(){}[0]
|
|
||||||
final const val OpticalSize // com.compose.sdl.icons/MaterialIconAxisDefaults.OpticalSize|{}OpticalSize[0]
|
|
||||||
final fun <get-OpticalSize>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.OpticalSize.<get-OpticalSize>|<get-OpticalSize>(){}[0]
|
|
||||||
final const val Weight // com.compose.sdl.icons/MaterialIconAxisDefaults.Weight|{}Weight[0]
|
|
||||||
final fun <get-Weight>(): kotlin/Int // com.compose.sdl.icons/MaterialIconAxisDefaults.Weight.<get-Weight>|<get-Weight>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
|
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
|
||||||
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
|
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
|
||||||
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
|
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
|
||||||
@@ -2220,7 +2209,6 @@ final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefault
|
|||||||
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
|
||||||
final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0]
|
final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0]
|
||||||
final val com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop|#static{}com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop[0]
|
|
||||||
final val com.compose.sdl.scrollbar/LocalScrollbarStyle // com.compose.sdl.scrollbar/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
|
final val com.compose.sdl.scrollbar/LocalScrollbarStyle // com.compose.sdl.scrollbar/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
|
||||||
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.scrollbar/ScrollbarStyle> // com.compose.sdl.scrollbar/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
|
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.scrollbar/ScrollbarStyle> // com.compose.sdl.scrollbar/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
|
||||||
final val com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop|#static{}com_compose_sdl_scrollbar_ScrollbarStyle$stableprop[0]
|
final val com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop|#static{}com_compose_sdl_scrollbar_ScrollbarStyle$stableprop[0]
|
||||||
@@ -2294,6 +2282,8 @@ final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foun
|
|||||||
final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foundation.gestures/calculateZoom(): kotlin/Float // androidx.compose.foundation.gestures/calculateZoom|calculateZoom@androidx.compose.ui.input.pointer.PointerEvent(){}[0]
|
final fun (androidx.compose.ui.input.pointer/PointerEvent).androidx.compose.foundation.gestures/calculateZoom(): kotlin/Float // androidx.compose.foundation.gestures/calculateZoom|calculateZoom@androidx.compose.ui.input.pointer.PointerEvent(){}[0]
|
||||||
final fun (androidx.compose.ui.text/AnnotatedString.Builder).androidx.compose.foundation.text/appendInlineContent(kotlin/String, kotlin/String = ...) // androidx.compose.foundation.text/appendInlineContent|appendInlineContent@androidx.compose.ui.text.AnnotatedString.Builder(kotlin.String;kotlin.String){}[0]
|
final fun (androidx.compose.ui.text/AnnotatedString.Builder).androidx.compose.foundation.text/appendInlineContent(kotlin/String, kotlin/String = ...) // androidx.compose.foundation.text/appendInlineContent|appendInlineContent@androidx.compose.ui.text.AnnotatedString.Builder(kotlin.String;kotlin.String){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.content/contentReceiver(androidx.compose.foundation.content/ReceiveContentListener): androidx.compose.ui/Modifier // androidx.compose.foundation.content/contentReceiver|contentReceiver@androidx.compose.ui.Modifier(androidx.compose.foundation.content.ReceiveContentListener){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.content/contentReceiver(androidx.compose.foundation.content/ReceiveContentListener): androidx.compose.ui/Modifier // androidx.compose.foundation.content/contentReceiver|contentReceiver@androidx.compose.ui.Modifier(androidx.compose.foundation.content.ReceiveContentListener){}[0]
|
||||||
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropSource(kotlin/Function1<androidx.compose.ui.geometry/Offset, androidx.compose.ui.draganddrop/DragAndDropTransferData?>): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropSource|dragAndDropSource@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?>){}[0]
|
||||||
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropSource(kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, kotlin/Function1<androidx.compose.ui.geometry/Offset, androidx.compose.ui.draganddrop/DragAndDropTransferData?>): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropSource|dragAndDropSource@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.geometry.Offset,androidx.compose.ui.draganddrop.DragAndDropTransferData?>){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropTarget(kotlin/Function1<androidx.compose.ui.draganddrop/DragAndDropEvent, kotlin/Boolean>, androidx.compose.ui.draganddrop/DragAndDropTarget): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropTarget|dragAndDropTarget@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.draganddrop.DragAndDropEvent,kotlin.Boolean>;androidx.compose.ui.draganddrop.DragAndDropTarget){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.draganddrop/dragAndDropTarget(kotlin/Function1<androidx.compose.ui.draganddrop/DragAndDropEvent, kotlin/Boolean>, androidx.compose.ui.draganddrop/DragAndDropTarget): androidx.compose.ui/Modifier // androidx.compose.foundation.draganddrop/dragAndDropTarget|dragAndDropTarget@androidx.compose.ui.Modifier(kotlin.Function1<androidx.compose.ui.draganddrop.DragAndDropEvent,kotlin.Boolean>;androidx.compose.ui.draganddrop.DragAndDropTarget){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable(androidx.compose.foundation.gestures/DraggableState, androidx.compose.foundation.gestures/Orientation, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, kotlin/Float, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable|draggable@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.DraggableState;androidx.compose.foundation.gestures.Orientation;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,kotlin.Float,kotlin.Unit>;kotlin.Boolean){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable(androidx.compose.foundation.gestures/DraggableState, androidx.compose.foundation.gestures/Orientation, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin.coroutines/SuspendFunction2<kotlinx.coroutines/CoroutineScope, kotlin/Float, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable|draggable@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.DraggableState;androidx.compose.foundation.gestures.Orientation;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.coroutines.SuspendFunction2<kotlinx.coroutines.CoroutineScope,kotlin.Float,kotlin.Unit>;kotlin.Boolean){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable2D(androidx.compose.foundation.gestures/Draggable2DState, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin/Function1<androidx.compose.ui.unit/Velocity, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable2D|draggable2D@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.Draggable2DState;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.unit.Velocity,kotlin.Unit>;kotlin.Boolean){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.foundation.gestures/draggable2D(androidx.compose.foundation.gestures/Draggable2DState, kotlin/Boolean = ..., androidx.compose.foundation.interaction/MutableInteractionSource? = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.geometry/Offset, kotlin/Unit> = ..., kotlin/Function1<androidx.compose.ui.unit/Velocity, kotlin/Unit> = ..., kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.foundation.gestures/draggable2D|draggable2D@androidx.compose.ui.Modifier(androidx.compose.foundation.gestures.Draggable2DState;kotlin.Boolean;androidx.compose.foundation.interaction.MutableInteractionSource?;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.geometry.Offset,kotlin.Unit>;kotlin.Function1<androidx.compose.ui.unit.Velocity,kotlin.Unit>;kotlin.Boolean){}[0]
|
||||||
@@ -2620,14 +2610,13 @@ final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean,
|
|||||||
final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/rememberOverscrollEffect(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/OverscrollEffect? // androidx.compose.foundation/rememberOverscrollEffect|rememberOverscrollEffect(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/ScrollState // androidx.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.icons/MaterialIconAxes(kotlin/Float = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ...): kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting> // com.compose.sdl.icons/MaterialIconAxes|MaterialIconAxes(kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
|
||||||
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0]
|
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter|com_compose_sdl_icons_MaterialIconAxisDefaults$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.scrollbar/HorizontalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/HorizontalScrollbar|HorizontalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.scrollbar/HorizontalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/HorizontalScrollbar|HorizontalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.scrollbar/VerticalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/VerticalScrollbar|VerticalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.scrollbar/VerticalScrollbar(com.compose.sdl.scrollbar/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, com.compose.sdl.scrollbar/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.scrollbar/VerticalScrollbar|VerticalScrollbar(com.compose.sdl.scrollbar.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;com.compose.sdl.scrollbar.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(): kotlin/Int // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter|com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(){}[0]
|
final fun com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(): kotlin/Int // com.compose.sdl.scrollbar/com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter|com_compose_sdl_scrollbar_ScrollbarStyle$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.scrollbar/defaultScrollbarStyle(): com.compose.sdl.scrollbar/ScrollbarStyle // com.compose.sdl.scrollbar/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
|
final fun com.compose.sdl.scrollbar/defaultScrollbarStyle(): com.compose.sdl.scrollbar/ScrollbarStyle // com.compose.sdl.scrollbar/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
|
||||||
|
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun com.compose.sdl.scrollbar/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): com.compose.sdl.scrollbar/ScrollbarAdapter // com.compose.sdl.scrollbar/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
|||||||
+1
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
// MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/
|
// MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/
|
||||||
// androidx/compose/foundation/text/selection/SelectionLayout.kt.
|
// androidx/compose/foundation/text/selection/SelectionLayout.kt.
|
||||||
|
// VENDOR-BASE: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt @ v1.12.0-beta03+dev4483
|
||||||
//
|
//
|
||||||
// Change vs upstream: MultiSelectionLayout.crossStatus and slotToIndex clamp their
|
// Change vs upstream: MultiSelectionLayout.crossStatus and slotToIndex clamp their
|
||||||
// infoList index to the valid range. Upstream assumes that if the drag position never
|
// infoList index to the valid range. Upstream assumes that if the drag position never
|
||||||
|
|||||||
+83
-32
@@ -30,9 +30,12 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -141,6 +144,19 @@ private class LazyListScrollbarAdapter(private val state: LazyListState) : Scrol
|
|||||||
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
|
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
|
||||||
|
|
||||||
override suspend fun scrollTo(scrollOffset: Double) {
|
override suspend fun scrollTo(scrollOffset: Double) {
|
||||||
|
val vDistance = scrollOffset - this.scrollOffset
|
||||||
|
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
|
||||||
|
// list's pixel geometry is only ESTIMATED (average visible item size), and
|
||||||
|
// that estimate drifts as differently-sized items scroll through the
|
||||||
|
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
|
||||||
|
// sidesteps the estimate entirely. This is what Compose Desktop's own
|
||||||
|
// LazyListScrollbarAdapter does, and for the same reason.
|
||||||
|
if (abs(vDistance) <= viewportSize) {
|
||||||
|
state.scrollBy(vDistance.toFloat())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Far jump (track-click paging past a viewport): no exact geometry to lean
|
||||||
|
// on, so estimate an index/offset from the average and snap to it.
|
||||||
val vAvg = averageItemSize()
|
val vAvg = averageItemSize()
|
||||||
if (vAvg <= 0.0) return
|
if (vAvg <= 0.0) return
|
||||||
val vTarget = scrollOffset.coerceAtLeast(0.0)
|
val vTarget = scrollOffset.coerceAtLeast(0.0)
|
||||||
@@ -193,7 +209,6 @@ private fun Scrollbar(
|
|||||||
val vHoverSource = remember { MutableInteractionSource() }
|
val vHoverSource = remember { MutableInteractionSource() }
|
||||||
val vHovered by vHoverSource.collectIsHoveredAsState()
|
val vHovered by vHoverSource.collectIsHoveredAsState()
|
||||||
var vDraggingThumb by remember { mutableStateOf(false) }
|
var vDraggingThumb by remember { mutableStateOf(false) }
|
||||||
var vGrab by remember { mutableStateOf(0) }
|
|
||||||
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
|
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
|
||||||
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
|
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
|
||||||
// physical pixels. Passing them into `.dp` doubles again through
|
// physical pixels. Passing them into `.dp` doubles again through
|
||||||
@@ -230,15 +245,6 @@ private fun Scrollbar(
|
|||||||
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
|
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
|
||||||
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
|
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
|
||||||
|
|
||||||
// ============
|
|
||||||
// Map a track-relative position to a scroll offset and apply it.
|
|
||||||
fun scrollToTrackPos(inThumbTop: Int) {
|
|
||||||
if (vTravel <= 0) return
|
|
||||||
val vPos = inThumbTop.coerceIn(0, vTravel)
|
|
||||||
val vFrac = (if (reverseLayout) vTravel - vPos else vPos).toDouble() / vTravel
|
|
||||||
vScope.launch { adapter.scrollTo(vFrac * vMaxScroll) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
|
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
|
||||||
val vThumbMod = (
|
val vThumbMod = (
|
||||||
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
|
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
|
||||||
@@ -251,33 +257,78 @@ private fun Scrollbar(
|
|||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Drag the thumb / page on track click — handled on the track so the
|
// Drag the thumb / page on track click — handled on the track so the
|
||||||
// coordinates have a stable (non-moving) origin.
|
// coordinates have a stable (non-moving) origin. The press is CONSUMED:
|
||||||
|
// a scrollbar owns the pointer input it handles, so content sharing this
|
||||||
|
// region (the scrollable beneath, or a parent that also reads pointer
|
||||||
|
// input) never reacts to the same press. Geometry is read LIVE from the
|
||||||
|
// adapter at press time — not from the captured render metrics above,
|
||||||
|
// which go stale between pointerInput relaunches — so a grab after a
|
||||||
|
// wheel-scroll still hit-tests against the thumb's current position.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
|
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
|
||||||
.pointerInput(isVertical, adapter, vTrackPx) {
|
.pointerInput(isVertical, adapter, vTrackPx, reverseLayout) {
|
||||||
detectDragGestures(
|
awaitEachGesture {
|
||||||
onDragStart = { offset ->
|
val vDown = awaitFirstDown(requireUnconsumed = false)
|
||||||
val vPress = (if (isVertical) offset.y else offset.x).toInt()
|
vDown.consume()
|
||||||
if (vPress in vThumbPos..(vThumbPos + vThumbLen)) {
|
|
||||||
vDraggingThumb = true
|
// Live geometry — stable for the drag's duration (content /
|
||||||
vGrab = vPress - vThumbPos
|
// viewport / track sizes don't change mid-drag).
|
||||||
} else {
|
val vContentPx = adapter.contentSize
|
||||||
// Page toward the click by one viewport.
|
val vViewportPx = adapter.viewportSize
|
||||||
vDraggingThumb = false
|
val vMaxScrollPx = vContentPx - vViewportPx
|
||||||
val vDir = if (vPress < vThumbPos) -1.0 else 1.0
|
if (vMaxScrollPx <= 0.0 || vTrackPx <= 0) return@awaitEachGesture
|
||||||
vScope.launch { adapter.scrollTo(vOffset + vDir * vViewport) }
|
val vMinLen = style.minimalHeight.value.toInt()
|
||||||
}
|
val vLen = (vTrackPx * (vViewportPx / vContentPx)).roundToInt()
|
||||||
},
|
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
|
||||||
onDrag = { change, _ ->
|
val vTravelPx = vTrackPx - vLen
|
||||||
|
val vRaw = if (vTravelPx <= 0) 0
|
||||||
|
else (vTravelPx * (adapter.scrollOffset / vMaxScrollPx)).roundToInt()
|
||||||
|
val vPos = (if (reverseLayout) vTravelPx - vRaw else vRaw).coerceIn(0, vTravelPx)
|
||||||
|
|
||||||
|
// Map a track-relative thumb-top to a scroll offset. The
|
||||||
|
// target uses the LIVE max scroll (read fresh each move),
|
||||||
|
// not the press-time value — the adapter compares it against
|
||||||
|
// its live scrollOffset, and mixing a stale scale with a live
|
||||||
|
// one makes the delta run away (grab at the bottom, drag up,
|
||||||
|
// and the list overshoots to the middle).
|
||||||
|
fun scrollToTrackPos(inThumbTop: Int) {
|
||||||
|
if (vTravelPx <= 0) return
|
||||||
|
val vLiveMax = adapter.contentSize - adapter.viewportSize
|
||||||
|
if (vLiveMax <= 0.0) return
|
||||||
|
val vClamped = inThumbTop.coerceIn(0, vTravelPx)
|
||||||
|
val vFrac = (if (reverseLayout) vTravelPx - vClamped else vClamped).toDouble() / vTravelPx
|
||||||
|
vScope.launch { adapter.scrollTo(vFrac * vLiveMax) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val vPress = (if (isVertical) vDown.position.y else vDown.position.x).toInt()
|
||||||
|
val vGrab: Int
|
||||||
|
if (vPress in vPos..(vPos + vLen)) {
|
||||||
|
vDraggingThumb = true
|
||||||
|
vGrab = vPress - vPos
|
||||||
|
} else {
|
||||||
|
// Page toward the click by one viewport.
|
||||||
|
vDraggingThumb = false
|
||||||
|
vGrab = 0
|
||||||
|
val vDir = if (vPress < vPos) -1.0 else 1.0
|
||||||
|
vScope.launch { adapter.scrollTo(adapter.scrollOffset + vDir * vViewportPx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow the pressed pointer to release, consuming every
|
||||||
|
// change so the interaction can't leak. Only a thumb grab
|
||||||
|
// drags-to-scroll; a track press just consumes.
|
||||||
|
do {
|
||||||
|
val vEvent = awaitPointerEvent()
|
||||||
|
val vChange = vEvent.changes.firstOrNull { it.id == vDown.id } ?: break
|
||||||
if (vDraggingThumb) {
|
if (vDraggingThumb) {
|
||||||
val vCur = (if (isVertical) change.position.y else change.position.x).toInt()
|
val vCur = (if (isVertical) vChange.position.y else vChange.position.x).toInt()
|
||||||
scrollToTrackPos(vCur - vGrab)
|
scrollToTrackPos(vCur - vGrab)
|
||||||
}
|
}
|
||||||
},
|
vChange.consume()
|
||||||
onDragEnd = { vDraggingThumb = false },
|
} while (vEvent.changes.any { it.id == vDown.id && it.pressed })
|
||||||
onDragCancel = { vDraggingThumb = false },
|
|
||||||
)
|
vDraggingThumb = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Klib ABI Dump
|
||||||
|
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
|
||||||
|
// Rendering settings:
|
||||||
|
// - Signature version: 2
|
||||||
|
// - Show manifest properties: true
|
||||||
|
// - Show declarations: true
|
||||||
|
|
||||||
|
// Library unique name: <com.bitsycore.compose.sdl:material-ripple>
|
||||||
|
abstract interface androidx.compose.material.ripple/RippleTheme { // androidx.compose.material.ripple/RippleTheme|null[0]
|
||||||
|
abstract fun defaultColor(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.ui.graphics/Color // androidx.compose.material.ripple/RippleTheme.defaultColor|defaultColor(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
abstract fun rippleAlpha(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.material.ripple/RippleAlpha // androidx.compose.material.ripple/RippleTheme.rippleAlpha|rippleAlpha(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
|
||||||
|
final object Companion { // androidx.compose.material.ripple/RippleTheme.Companion|null[0]
|
||||||
|
final fun defaultRippleAlpha(androidx.compose.ui.graphics/Color, kotlin/Boolean): androidx.compose.material.ripple/RippleAlpha // androidx.compose.material.ripple/RippleTheme.Companion.defaultRippleAlpha|defaultRippleAlpha(androidx.compose.ui.graphics.Color;kotlin.Boolean){}[0]
|
||||||
|
final fun defaultRippleColor(androidx.compose.ui.graphics/Color, kotlin/Boolean): androidx.compose.ui.graphics/Color // androidx.compose.material.ripple/RippleTheme.Companion.defaultRippleColor|defaultRippleColor(androidx.compose.ui.graphics.Color;kotlin.Boolean){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class androidx.compose.material.ripple/RippleAlpha { // androidx.compose.material.ripple/RippleAlpha|null[0]
|
||||||
|
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // androidx.compose.material.ripple/RippleAlpha.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
||||||
|
|
||||||
|
final val draggedAlpha // androidx.compose.material.ripple/RippleAlpha.draggedAlpha|{}draggedAlpha[0]
|
||||||
|
final fun <get-draggedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.draggedAlpha.<get-draggedAlpha>|<get-draggedAlpha>(){}[0]
|
||||||
|
final val focusedAlpha // androidx.compose.material.ripple/RippleAlpha.focusedAlpha|{}focusedAlpha[0]
|
||||||
|
final fun <get-focusedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.focusedAlpha.<get-focusedAlpha>|<get-focusedAlpha>(){}[0]
|
||||||
|
final val hoveredAlpha // androidx.compose.material.ripple/RippleAlpha.hoveredAlpha|{}hoveredAlpha[0]
|
||||||
|
final fun <get-hoveredAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.hoveredAlpha.<get-hoveredAlpha>|<get-hoveredAlpha>(){}[0]
|
||||||
|
final val pressedAlpha // androidx.compose.material.ripple/RippleAlpha.pressedAlpha|{}pressedAlpha[0]
|
||||||
|
final fun <get-pressedAlpha>(): kotlin/Float // androidx.compose.material.ripple/RippleAlpha.pressedAlpha.<get-pressedAlpha>|<get-pressedAlpha>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.material.ripple/RippleAlpha.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // androidx.compose.material.ripple/RippleAlpha.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // androidx.compose.material.ripple/RippleAlpha.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final val androidx.compose.material.ripple/LocalRippleTheme // androidx.compose.material.ripple/LocalRippleTheme|{}LocalRippleTheme[0]
|
||||||
|
final fun <get-LocalRippleTheme>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.material.ripple/RippleTheme> // androidx.compose.material.ripple/LocalRippleTheme.<get-LocalRippleTheme>|<get-LocalRippleTheme>(){}[0]
|
||||||
|
final val androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop // androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop|#static{}androidx_compose_material_ripple_RippleAlpha$stableprop[0]
|
||||||
|
|
||||||
|
final fun androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop_getter(): kotlin/Int // androidx.compose.material.ripple/androidx_compose_material_ripple_RippleAlpha$stableprop_getter|androidx_compose_material_ripple_RippleAlpha$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.material.ripple/createRippleModifierNode(androidx.compose.foundation.interaction/InteractionSource, kotlin/Boolean, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/ColorProducer, kotlin/Function0<androidx.compose.material.ripple/RippleAlpha>): androidx.compose.ui.node/DelegatableNode // androidx.compose.material.ripple/createRippleModifierNode|createRippleModifierNode(androidx.compose.foundation.interaction.InteractionSource;kotlin.Boolean;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.ColorProducer;kotlin.Function0<androidx.compose.material.ripple.RippleAlpha>){}[0]
|
||||||
|
final fun androidx.compose.material.ripple/rememberRipple(kotlin/Boolean, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/Indication // androidx.compose.material.ripple/rememberRipple|rememberRipple(kotlin.Boolean;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -32,9 +32,13 @@ import com.compose.sdl.window.createPopupHostState
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import kotlinx.cinterop.toKString
|
||||||
import kotlinx.cinterop.reinterpret
|
import kotlinx.cinterop.reinterpret
|
||||||
import sdl3.SDL_Delay
|
import sdl3.SDL_Delay
|
||||||
|
import sdl3.SDL_GetPerformanceCounter
|
||||||
|
import sdl3.SDL_GetPerformanceFrequency
|
||||||
import sdl3.SDL_GetTicks
|
import sdl3.SDL_GetTicks
|
||||||
|
import sdl3.SDL_GetTicksNS
|
||||||
import sdl3.SDL_Quit
|
import sdl3.SDL_Quit
|
||||||
import sdl3.SDL_SetWindowTitle
|
import sdl3.SDL_SetWindowTitle
|
||||||
import sdl3.SDL_WaitEventTimeout
|
import sdl3.SDL_WaitEventTimeout
|
||||||
@@ -141,12 +145,18 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
|
|
||||||
appComposition.setContent { appScope.content() }
|
appComposition.setContent { appScope.content() }
|
||||||
// Compose the initial Window() declarations before entering the loop.
|
// Compose the initial Window() declarations before entering the loop.
|
||||||
appClock.sendFrame()
|
appClock.sendFrame(frameClockNanos())
|
||||||
yield()
|
yield()
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Main loop
|
// Main loop
|
||||||
|
var vGcLastTicks = SDL_GetTicks()
|
||||||
|
var vRenderedSinceGc = false
|
||||||
while (!runtime.exitRequested) {
|
while (!runtime.exitRequested) {
|
||||||
|
FrameProfiler.mark()
|
||||||
|
// One virtual 16.6ms tick per loop iteration (no-op unless useVirtualFrameTime) —
|
||||||
|
// all windows' clocks share the same timestamp this iteration.
|
||||||
|
advanceVirtualFrame()
|
||||||
Snapshot.sendApplyNotifications()
|
Snapshot.sendApplyNotifications()
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
@@ -174,11 +184,12 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mainDispatcher.drainPending()
|
mainDispatcher.drainPending()
|
||||||
|
FrameProfiler.phase("events")
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// App composition pump — Window()s may appear / disappear here.
|
// App composition pump — Window()s may appear / disappear here.
|
||||||
Snapshot.sendApplyNotifications()
|
Snapshot.sendApplyNotifications()
|
||||||
appClock.sendFrame()
|
appClock.sendFrame(frameClockNanos())
|
||||||
yield()
|
yield()
|
||||||
runtime.reapDestroyed()
|
runtime.reapDestroyed()
|
||||||
|
|
||||||
@@ -193,6 +204,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
// Exit when the last window is gone (after at least one existed).
|
// Exit when the last window is gone (after at least one existed).
|
||||||
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
|
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
|
||||||
if (runtime.exitRequested) break
|
if (runtime.exitRequested) break
|
||||||
|
FrameProfiler.phase("app")
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Per-window pump + render.
|
// Per-window pump + render.
|
||||||
@@ -201,18 +213,29 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
val vAppPending = appRecomposer.hasPendingWork
|
val vAppPending = appRecomposer.hasPendingWork
|
||||||
for (vW in runtime.windows.toList()) {
|
for (vW in runtime.windows.toList()) {
|
||||||
vW.installGlobals()
|
vW.installGlobals()
|
||||||
vW.host.sendAnimationFrame(SDL_GetTicks().toLong() * 1_000_000L)
|
vW.host.sendAnimationFrame(animationClockNanos())
|
||||||
mainDispatcher.drainPending()
|
mainDispatcher.drainPending()
|
||||||
Snapshot.sendApplyNotifications()
|
Snapshot.sendApplyNotifications()
|
||||||
vW.frameClock.sendFrame()
|
vW.frameClock.sendFrame(frameClockNanos())
|
||||||
yield()
|
yield()
|
||||||
|
FrameProfiler.phase("pump")
|
||||||
if (vW.shouldRender()) {
|
if (vW.shouldRender()) {
|
||||||
vW.renderFrame()
|
vW.renderFrame()
|
||||||
vAnyRendered = true
|
vAnyRendered = true
|
||||||
if (!vW.backend.vsyncEnabled) vAllVsync = false
|
if (!vW.backend.vsyncEnabled) vAllVsync = false
|
||||||
}
|
}
|
||||||
|
FrameProfiler.phase("render")
|
||||||
}
|
}
|
||||||
runtime.reapDestroyed()
|
runtime.reapDestroyed()
|
||||||
|
FrameProfiler.frameDone(vAnyRendered)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Drain deferred native-resource disposals on the MAIN thread —
|
||||||
|
// textures/surfaces whose owner closed them or whose Cleaner fired
|
||||||
|
// on a GC worker enqueue here (SDL calls aren't thread-safe). This
|
||||||
|
// is the ownership path that makes the GC nudge below a mere
|
||||||
|
// backstop (ROADMAP.md item 1).
|
||||||
|
com.compose.sdl.graphics.NativeReleaseQueue.drain()
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Pace / idle-skip.
|
// Pace / idle-skip.
|
||||||
@@ -224,6 +247,23 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
SDL_WaitEventTimeout(null, 10)
|
SDL_WaitEventTimeout(null, 10)
|
||||||
for (vW in runtime.windows) vW.resetFpsWindow()
|
for (vW in runtime.windows) vW.resetFpsWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Native-memory nudge. Renderer resources (Skia surfaces / images /
|
||||||
|
// fonts, SDL textures) are freed by Cleaners that only run when the
|
||||||
|
// Kotlin/Native GC collects — and a Compose app's Kotlin heap is
|
||||||
|
// small enough that the allocation-driven scheduler can starve them
|
||||||
|
// for minutes while the NATIVE heap balloons (issue #2: memory
|
||||||
|
// "never released" while navigating). Collect periodically, only
|
||||||
|
// after rendering activity, between frames; costs ~ms at these
|
||||||
|
// heap sizes and keeps RSS tracking real usage.
|
||||||
|
if (vAnyRendered) vRenderedSinceGc = true
|
||||||
|
val vNowTicks = SDL_GetTicks()
|
||||||
|
if (vRenderedSinceGc && vNowTicks - vGcLastTicks >= 10_000uL) {
|
||||||
|
vGcLastTicks = vNowTicks
|
||||||
|
vRenderedSinceGc = false
|
||||||
|
collectNativeGarbage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
@@ -231,6 +271,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
snapshotHandle.dispose()
|
snapshotHandle.dispose()
|
||||||
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
|
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
|
||||||
runtime.reapDestroyed()
|
runtime.reapDestroyed()
|
||||||
|
com.compose.sdl.graphics.NativeReleaseQueue.drain()
|
||||||
appComposition.dispose()
|
appComposition.dispose()
|
||||||
appRecomposer.cancel()
|
appRecomposer.cancel()
|
||||||
appRecomposeJob.cancelAndJoin()
|
appRecomposeJob.cancelAndJoin()
|
||||||
@@ -243,6 +284,132 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
SDL_Quit()
|
SDL_Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CDN_PROFILE=1 — per-phase timings, printed every ~2s of rendered frames.
|
||||||
|
A named-phase SINGLETON so both the main loop (events / app / pump / render)
|
||||||
|
AND renderFrame's sub-steps (render.layout / render.draw / render.present)
|
||||||
|
report into one line. `mark()` resets the stopwatch; `phase(name)` charges
|
||||||
|
the elapsed since the last mark/phase to that name. Measure first, optimize
|
||||||
|
second — see ROADMAP.md. */
|
||||||
|
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
|
internal object FrameProfiler {
|
||||||
|
// null until first checked; then true/false for the run's lifetime.
|
||||||
|
private var fEnabled: Boolean? = null
|
||||||
|
// Output file — resolved once from CDN_PROFILE. Writing to a file (not
|
||||||
|
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
|
||||||
|
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
|
||||||
|
// the cwd; CDN_PROFILE=<path> → that path.
|
||||||
|
private var fPath: String = "cdn_profile.log"
|
||||||
|
val enabled: Boolean
|
||||||
|
get() = fEnabled ?: run {
|
||||||
|
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
||||||
|
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv
|
||||||
|
(vEnv != null).also { fEnabled = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
|
||||||
|
// Insertion-ordered so the printed line follows the call order.
|
||||||
|
private val fSum = LinkedHashMap<String, Double>()
|
||||||
|
private val fMax = LinkedHashMap<String, Double>()
|
||||||
|
private var fFrames = 0
|
||||||
|
private var fLastPrintMs = SDL_GetTicks()
|
||||||
|
private var fMark = 0uL
|
||||||
|
|
||||||
|
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() }
|
||||||
|
|
||||||
|
fun phase(inName: String) {
|
||||||
|
if (!enabled) return
|
||||||
|
val vNow = SDL_GetPerformanceCounter()
|
||||||
|
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
|
||||||
|
fSum[inName] = (fSum[inName] ?: 0.0) + vMs
|
||||||
|
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs
|
||||||
|
fMark = vNow
|
||||||
|
}
|
||||||
|
|
||||||
|
fun frameDone(inRendered: Boolean) {
|
||||||
|
if (!enabled) return
|
||||||
|
if (inRendered) fFrames++
|
||||||
|
val vNowMs = SDL_GetTicks()
|
||||||
|
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
|
||||||
|
val vParts = fSum.keys.map { vName ->
|
||||||
|
val vAvg = (fSum[vName] ?: 0.0) / fFrames
|
||||||
|
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
||||||
|
}
|
||||||
|
// Per-frame draw-work averages (see DrawStats): what's inside `draw`.
|
||||||
|
val vStats = com.compose.sdl.graphics.DrawStats
|
||||||
|
val vDraw = "geo=${vStats.geometrySubmits / fFrames} verts=${vStats.vertices / fFrames} " +
|
||||||
|
"masks=${vStats.maskRealizations / fFrames} text=${vStats.textDraws / fFrames} img=${vStats.imageBlits / fFrames}"
|
||||||
|
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + " | per-frame " + vDraw + "\n"
|
||||||
|
val vFile = platform.posix.fopen(fPath, "a")
|
||||||
|
if (vFile != null) {
|
||||||
|
platform.posix.fputs(vLine, vFile)
|
||||||
|
platform.posix.fclose(vFile)
|
||||||
|
}
|
||||||
|
vStats.reset()
|
||||||
|
fSum.clear(); fMax.clear()
|
||||||
|
fFrames = 0
|
||||||
|
fLastPrintMs = vNowMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
||||||
|
their native memory (see the main loop's native-memory nudge). */
|
||||||
|
private val kForceRender: Boolean = platform.posix.getenv("CDN_FORCERENDER") != null
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Probe / screenshot support (render-to-quiescence)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
|
||||||
|
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
|
||||||
|
rememberInfiniteTransition & co. freeze at their initial value — the same mechanism
|
||||||
|
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
|
||||||
|
reach quiescence and capture deterministically. */
|
||||||
|
var disableInfiniteAnimations: Boolean = false
|
||||||
|
|
||||||
|
/* The cancelling policy: a coroutine that ends in CancellationException counts as
|
||||||
|
cancelled (not failed), so only the animation coroutine stops — nothing propagates. */
|
||||||
|
private object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
|
||||||
|
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
|
||||||
|
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
|
||||||
|
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
|
||||||
|
real-time ticks — the native mirror of the JVM parity leg's render(nanos) stepping.
|
||||||
|
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
|
||||||
|
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
|
||||||
|
screenshots become deterministic. Input timestamps and FPS stay on real time. */
|
||||||
|
var useVirtualFrameTime: Boolean = false
|
||||||
|
|
||||||
|
private var virtualFrameNanos = 0L
|
||||||
|
|
||||||
|
private fun advanceVirtualFrame() {
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
|
||||||
|
private fun frameClockNanos(): Long =
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
|
||||||
|
|
||||||
|
// Timestamp for the owner's node-animation clock — real path keeps the pre-existing
|
||||||
|
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
|
||||||
|
private fun animationClockNanos(): Long =
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
|
||||||
|
|
||||||
|
// The window currently inside renderFrame — the loop is single-threaded, so a plain
|
||||||
|
// var is enough for onFrame probes to address "the window I'm being called for".
|
||||||
|
private var renderingWindow: WindowInstance? = null
|
||||||
|
|
||||||
|
/* From inside an onFrame callback: true while the just-rendered window still has pending
|
||||||
|
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
|
||||||
|
Screenshot probes capture once this stays false for a few consecutive frames instead of
|
||||||
|
at a fixed frame count. Outside onFrame it answers false. */
|
||||||
|
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
|
||||||
|
|
||||||
|
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||||
|
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
||||||
|
|
||||||
/* Single-window compatibility wrapper — the pre-multi-window entry point.
|
/* Single-window compatibility wrapper — the pre-multi-window entry point.
|
||||||
Closing the window exits the app, exactly as before. */
|
Closing the window exits the app, exactly as before. */
|
||||||
fun nativeComposeWindow(
|
fun nativeComposeWindow(
|
||||||
@@ -378,8 +545,9 @@ internal class WindowInstance(
|
|||||||
private var hasMousePos = false
|
private var hasMousePos = false
|
||||||
|
|
||||||
private var frameIndex = 0
|
private var frameIndex = 0
|
||||||
private var fpsFrames = 0
|
private var fpsEma = 0.0
|
||||||
private var fpsLastMs = SDL_GetTicks()
|
private var fpsLastFrameMs = SDL_GetTicks()
|
||||||
|
private var fpsLastTitleMs = 0uL
|
||||||
|
|
||||||
// Escape → back (upstream desktop's BackNavigationEventInput): unconsumed
|
// Escape → back (upstream desktop's BackNavigationEventInput): unconsumed
|
||||||
// Escape completes a back navigation on THIS window's dispatcher.
|
// Escape completes a back navigation on THIS window's dispatcher.
|
||||||
@@ -433,6 +601,10 @@ internal class WindowInstance(
|
|||||||
|
|
||||||
host = ComposeRootHost(inDensity = backend.pixelDensity)
|
host = ComposeRootHost(inDensity = backend.pixelDensity)
|
||||||
host.attach()
|
host.attach()
|
||||||
|
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame
|
||||||
|
// even when nothing else (recompose / relayout) is pending — retained layers
|
||||||
|
// need this so a draw-only state change still repaints.
|
||||||
|
host.setInvalidationCallback { needsFrame = true }
|
||||||
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
||||||
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
||||||
|
|
||||||
@@ -441,7 +613,11 @@ internal class WindowInstance(
|
|||||||
// setContent below).
|
// setContent below).
|
||||||
installGlobals()
|
installGlobals()
|
||||||
|
|
||||||
val vRecomposer = Recomposer(inScope.coroutineContext + frameClock)
|
// Effect context for this window's composition — the screenshot flag injects the
|
||||||
|
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
|
||||||
|
var vEffectContext = inScope.coroutineContext + frameClock
|
||||||
|
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
|
||||||
|
val vRecomposer = Recomposer(vEffectContext)
|
||||||
recomposer = vRecomposer
|
recomposer = vRecomposer
|
||||||
recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() }
|
recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() }
|
||||||
val vComposition = Composition(host.applier, vRecomposer)
|
val vComposition = Composition(host.applier, vRecomposer)
|
||||||
@@ -640,7 +816,18 @@ internal class WindowInstance(
|
|||||||
// Frame pump
|
// Frame pump
|
||||||
|
|
||||||
fun shouldRender(): Boolean =
|
fun shouldRender(): Boolean =
|
||||||
needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null
|
needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null || kForceRender
|
||||||
|
|
||||||
|
/* True while this window still has pending work: a state/layout/draw invalidation
|
||||||
|
(needsFrame), recomposer work, or a composition/node animation awaiting the next
|
||||||
|
frame. Sampled by windowHasInvalidations() from onFrame probes — the quiescence
|
||||||
|
signal for render-to-settle screenshot capture. */
|
||||||
|
fun hasInvalidations(): Boolean =
|
||||||
|
needsFrame || recomposer?.hasPendingWork == true ||
|
||||||
|
frameClock.hasAwaiters || host.hasAnimationAwaiters()
|
||||||
|
|
||||||
|
// TEMP measurement: CDN_FORCERENDER=1 forces every frame to render so
|
||||||
|
// sustained steady-state timings can be measured on otherwise-idle screens.
|
||||||
|
|
||||||
fun renderFrame() {
|
fun renderFrame() {
|
||||||
val vRender = renderBackend ?: return
|
val vRender = renderBackend ?: return
|
||||||
@@ -651,6 +838,7 @@ internal class WindowInstance(
|
|||||||
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
||||||
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
||||||
host.measureAndLayout()
|
host.measureAndLayout()
|
||||||
|
FrameProfiler.phase(" layout")
|
||||||
|
|
||||||
// Hover refresh after layout (upstream skiko: SyntheticEventSender).
|
// Hover refresh after layout (upstream skiko: SyntheticEventSender).
|
||||||
if (hasMousePos) {
|
if (hasMousePos) {
|
||||||
@@ -658,30 +846,48 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vRender.beginFrame(1f)
|
vRender.beginFrame(1f)
|
||||||
vRender.drawRoot(host)
|
vRender.drawRoot { canvas -> host.drawRoot(canvas) }
|
||||||
if (onFrame != null && !onFrame.invoke(vRender, frameIndex)) {
|
if (onFrame != null) {
|
||||||
|
renderingWindow = this
|
||||||
|
val vContinue = onFrame.invoke(vRender, frameIndex)
|
||||||
|
renderingWindow = null
|
||||||
// Probe consumers end the app when their scenario completes.
|
// Probe consumers end the app when their scenario completes.
|
||||||
facade.close()
|
if (!vContinue) facade.close()
|
||||||
}
|
}
|
||||||
|
FrameProfiler.phase(" draw")
|
||||||
vRender.endFrame()
|
vRender.endFrame()
|
||||||
|
FrameProfiler.phase(" present")
|
||||||
frameIndex++
|
frameIndex++
|
||||||
|
|
||||||
// FPS — refreshed ~once a second, per window.
|
// FPS — instantaneous inter-frame rate, EMA-smoothed, title refreshed
|
||||||
fpsFrames++
|
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
|
||||||
|
// than waiting for a full second of unbroken rendering to accumulate
|
||||||
|
// (the old fixed-window counter never got its first update: bursty
|
||||||
|
// interaction kept idling before 1s elapsed, and the idle-skip reset the
|
||||||
|
// window each time — so FPS only appeared during a long enough page
|
||||||
|
// transition). A dt outside 1..100ms is the first frame or a resume from
|
||||||
|
// idle (the gap isn't a real frame interval), so it's not sampled — the
|
||||||
|
// title just holds the last active rate while idle.
|
||||||
val vNowMs = SDL_GetTicks()
|
val vNowMs = SDL_GetTicks()
|
||||||
val vElapsed = (vNowMs - fpsLastMs).toInt()
|
val vDt = (vNowMs - fpsLastFrameMs).toInt()
|
||||||
if (vElapsed >= 1000) {
|
fpsLastFrameMs = vNowMs
|
||||||
val vFps = fpsFrames * 1000 / vElapsed
|
if (vDt in 1..100) {
|
||||||
facade.updateFps(vFps)
|
val vInst = 1000.0 / vDt
|
||||||
SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS")
|
fpsEma = if (fpsEma <= 0.0) vInst else fpsEma * 0.9 + vInst * 0.1
|
||||||
fpsFrames = 0
|
if ((vNowMs - fpsLastTitleMs).toInt() >= 250) {
|
||||||
fpsLastMs = vNowMs
|
val vFps = (fpsEma + 0.5).toInt()
|
||||||
|
facade.updateFps(vFps)
|
||||||
|
SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS")
|
||||||
|
fpsLastTitleMs = vNowMs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle-skip calls this: drop the frame timer so the resume-from-idle frame's
|
||||||
|
// dt (the whole idle gap) isn't sampled as a real interval. The EMA is left
|
||||||
|
// intact so the title keeps showing the last active rate while idle.
|
||||||
fun resetFpsWindow() {
|
fun resetFpsWindow() {
|
||||||
fpsFrames = 0
|
fpsLastFrameMs = SDL_GetTicks()
|
||||||
fpsLastMs = SDL_GetTicks()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ internal class SDL3FrameClock : MonotonicFrameClock {
|
|||||||
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
||||||
broadcast.withFrameNanos(onFrame)
|
broadcast.withFrameNanos(onFrame)
|
||||||
|
|
||||||
fun sendFrame() {
|
fun sendFrame(inNanos: Long = SDL_GetTicksNS().toLong()) {
|
||||||
broadcast.sendFrame(SDL_GetTicksNS().toLong())
|
broadcast.sendFrame(inNanos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coroutines (recomposer / composition animations) currently suspended in withFrameNanos
|
||||||
|
// awaiting the next sendFrame — the "an animation is still running" half of the window's
|
||||||
|
// quiescence signal (see WindowInstance.hasInvalidations).
|
||||||
|
val hasAwaiters: Boolean get() = broadcast.hasAwaiters
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
api("androidx.navigationevent:navigationevent-compose:1.1.2")
|
api("androidx.navigationevent:navigationevent-compose:1.1.2")
|
||||||
api(project(":ui-util"))
|
api(project(":ui-util"))
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
api(project(":ui-util"))
|
api(project(":ui-util"))
|
||||||
}
|
}
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
// Klib ABI Dump
|
||||||
|
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
|
||||||
|
// Rendering settings:
|
||||||
|
// - Signature version: 2
|
||||||
|
// - Show manifest properties: true
|
||||||
|
// - Show declarations: true
|
||||||
|
|
||||||
|
// Library unique name: <com.bitsycore.compose.sdl:ui-tooling-preview>
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/AndroidUiMode : kotlin/Annotation { // androidx.compose.ui.tooling.preview/AndroidUiMode|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview/AndroidUiMode.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/Preview : kotlin/Annotation { // androidx.compose.ui.tooling.preview/Preview|null[0]
|
||||||
|
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Float = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Long = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Int = ...) // androidx.compose.ui.tooling.preview/Preview.<init>|<init>(kotlin.String;kotlin.String;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.String;kotlin.Float;kotlin.Boolean;kotlin.Boolean;kotlin.Long;kotlin.Int;kotlin.String;kotlin.Int){}[0]
|
||||||
|
|
||||||
|
final val apiLevel // androidx.compose.ui.tooling.preview/Preview.apiLevel|{}apiLevel[0]
|
||||||
|
final fun <get-apiLevel>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.apiLevel.<get-apiLevel>|<get-apiLevel>(){}[0]
|
||||||
|
final val backgroundColor // androidx.compose.ui.tooling.preview/Preview.backgroundColor|{}backgroundColor[0]
|
||||||
|
final fun <get-backgroundColor>(): kotlin/Long // androidx.compose.ui.tooling.preview/Preview.backgroundColor.<get-backgroundColor>|<get-backgroundColor>(){}[0]
|
||||||
|
final val device // androidx.compose.ui.tooling.preview/Preview.device|{}device[0]
|
||||||
|
final fun <get-device>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.device.<get-device>|<get-device>(){}[0]
|
||||||
|
final val fontScale // androidx.compose.ui.tooling.preview/Preview.fontScale|{}fontScale[0]
|
||||||
|
final fun <get-fontScale>(): kotlin/Float // androidx.compose.ui.tooling.preview/Preview.fontScale.<get-fontScale>|<get-fontScale>(){}[0]
|
||||||
|
final val group // androidx.compose.ui.tooling.preview/Preview.group|{}group[0]
|
||||||
|
final fun <get-group>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.group.<get-group>|<get-group>(){}[0]
|
||||||
|
final val heightDp // androidx.compose.ui.tooling.preview/Preview.heightDp|{}heightDp[0]
|
||||||
|
final fun <get-heightDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.heightDp.<get-heightDp>|<get-heightDp>(){}[0]
|
||||||
|
final val locale // androidx.compose.ui.tooling.preview/Preview.locale|{}locale[0]
|
||||||
|
final fun <get-locale>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.locale.<get-locale>|<get-locale>(){}[0]
|
||||||
|
final val name // androidx.compose.ui.tooling.preview/Preview.name|{}name[0]
|
||||||
|
final fun <get-name>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.name.<get-name>|<get-name>(){}[0]
|
||||||
|
final val showBackground // androidx.compose.ui.tooling.preview/Preview.showBackground|{}showBackground[0]
|
||||||
|
final fun <get-showBackground>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showBackground.<get-showBackground>|<get-showBackground>(){}[0]
|
||||||
|
final val showSystemUi // androidx.compose.ui.tooling.preview/Preview.showSystemUi|{}showSystemUi[0]
|
||||||
|
final fun <get-showSystemUi>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showSystemUi.<get-showSystemUi>|<get-showSystemUi>(){}[0]
|
||||||
|
final val uiMode // androidx.compose.ui.tooling.preview/Preview.uiMode|{}uiMode[0]
|
||||||
|
final fun <get-uiMode>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.uiMode.<get-uiMode>|<get-uiMode>(){}[0]
|
||||||
|
final val wallpaper // androidx.compose.ui.tooling.preview/Preview.wallpaper|{}wallpaper[0]
|
||||||
|
final fun <get-wallpaper>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.wallpaper.<get-wallpaper>|<get-wallpaper>(){}[0]
|
||||||
|
final val widthDp // androidx.compose.ui.tooling.preview/Preview.widthDp|{}widthDp[0]
|
||||||
|
final fun <get-widthDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.widthDp.<get-widthDp>|<get-widthDp>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewDynamicColors : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewDynamicColors|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewDynamicColors.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewFontScale : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewFontScale|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewFontScale.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewLightDark : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewLightDark|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewLightDark.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewParameter : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewParameter|null[0]
|
||||||
|
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>>, kotlin/Int = ...) // androidx.compose.ui.tooling.preview/PreviewParameter.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewParameterProvider<*>>;kotlin.Int){}[0]
|
||||||
|
|
||||||
|
final val limit // androidx.compose.ui.tooling.preview/PreviewParameter.limit|{}limit[0]
|
||||||
|
final fun <get-limit>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameter.limit.<get-limit>|<get-limit>(){}[0]
|
||||||
|
final val provider // androidx.compose.ui.tooling.preview/PreviewParameter.provider|{}provider[0]
|
||||||
|
final fun <get-provider>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>> // androidx.compose.ui.tooling.preview/PreviewParameter.provider.<get-provider>|<get-provider>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewScreenSizes : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewScreenSizes|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewScreenSizes.<init>|<init>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open annotation class androidx.compose.ui.tooling.preview/PreviewWrapper : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewWrapper|null[0]
|
||||||
|
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider>) // androidx.compose.ui.tooling.preview/PreviewWrapper.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewWrapperProvider>){}[0]
|
||||||
|
|
||||||
|
final val wrapper // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper|{}wrapper[0]
|
||||||
|
final fun <get-wrapper>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider> // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper.<get-wrapper>|<get-wrapper>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface <#A: kotlin/Any?> androidx.compose.ui.tooling.preview/PreviewParameterProvider { // androidx.compose.ui.tooling.preview/PreviewParameterProvider|null[0]
|
||||||
|
abstract val values // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values|{}values[0]
|
||||||
|
abstract fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
|
||||||
|
open val count // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count|{}count[0]
|
||||||
|
open fun <get-count>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count.<get-count>|<get-count>(){}[0]
|
||||||
|
|
||||||
|
open fun getDisplayName(kotlin/Int): kotlin/String? // androidx.compose.ui.tooling.preview/PreviewParameterProvider.getDisplayName|getDisplayName(kotlin.Int){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface androidx.compose.ui.tooling.preview/PreviewWrapperProvider { // androidx.compose.ui.tooling.preview/PreviewWrapperProvider|null[0]
|
||||||
|
abstract fun Wrap(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.ui.tooling.preview/PreviewWrapperProvider.Wrap|Wrap(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open class <#A: kotlin/Any?> androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider : androidx.compose.ui.tooling.preview/PreviewParameterProvider<#A> { // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider|null[0]
|
||||||
|
constructor <init>(kotlin.collections/Collection<#A>) // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.<init>|<init>(kotlin.collections.Collection<1:0>){}[0]
|
||||||
|
|
||||||
|
open val values // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values|{}values[0]
|
||||||
|
open fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
open class androidx.compose.ui.tooling.preview.datasource/LoremIpsum : androidx.compose.ui.tooling.preview/PreviewParameterProvider<kotlin/String> { // androidx.compose.ui.tooling.preview.datasource/LoremIpsum|null[0]
|
||||||
|
constructor <init>() // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(){}[0]
|
||||||
|
constructor <init>(kotlin/Int) // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(kotlin.Int){}[0]
|
||||||
|
|
||||||
|
open val values // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values|{}values[0]
|
||||||
|
open fun <get-values>(): kotlin.sequences/Sequence<kotlin/String> // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values.<get-values>|<get-values>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object androidx.compose.ui.tooling.preview/AndroidUiModes { // androidx.compose.ui.tooling.preview/AndroidUiModes|null[0]
|
||||||
|
final const val UI_MODE_NIGHT_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK|{}UI_MODE_NIGHT_MASK[0]
|
||||||
|
final fun <get-UI_MODE_NIGHT_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK.<get-UI_MODE_NIGHT_MASK>|<get-UI_MODE_NIGHT_MASK>(){}[0]
|
||||||
|
final const val UI_MODE_NIGHT_NO // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO|{}UI_MODE_NIGHT_NO[0]
|
||||||
|
final fun <get-UI_MODE_NIGHT_NO>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO.<get-UI_MODE_NIGHT_NO>|<get-UI_MODE_NIGHT_NO>(){}[0]
|
||||||
|
final const val UI_MODE_NIGHT_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED|{}UI_MODE_NIGHT_UNDEFINED[0]
|
||||||
|
final fun <get-UI_MODE_NIGHT_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED.<get-UI_MODE_NIGHT_UNDEFINED>|<get-UI_MODE_NIGHT_UNDEFINED>(){}[0]
|
||||||
|
final const val UI_MODE_NIGHT_YES // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES|{}UI_MODE_NIGHT_YES[0]
|
||||||
|
final fun <get-UI_MODE_NIGHT_YES>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES.<get-UI_MODE_NIGHT_YES>|<get-UI_MODE_NIGHT_YES>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_APPLIANCE // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE|{}UI_MODE_TYPE_APPLIANCE[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_APPLIANCE>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE.<get-UI_MODE_TYPE_APPLIANCE>|<get-UI_MODE_TYPE_APPLIANCE>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_CAR // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR|{}UI_MODE_TYPE_CAR[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_CAR>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR.<get-UI_MODE_TYPE_CAR>|<get-UI_MODE_TYPE_CAR>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_DESK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK|{}UI_MODE_TYPE_DESK[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_DESK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK.<get-UI_MODE_TYPE_DESK>|<get-UI_MODE_TYPE_DESK>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK|{}UI_MODE_TYPE_MASK[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK.<get-UI_MODE_TYPE_MASK>|<get-UI_MODE_TYPE_MASK>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_NORMAL // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL|{}UI_MODE_TYPE_NORMAL[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_NORMAL>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL.<get-UI_MODE_TYPE_NORMAL>|<get-UI_MODE_TYPE_NORMAL>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_TELEVISION // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION|{}UI_MODE_TYPE_TELEVISION[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_TELEVISION>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION.<get-UI_MODE_TYPE_TELEVISION>|<get-UI_MODE_TYPE_TELEVISION>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED|{}UI_MODE_TYPE_UNDEFINED[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED.<get-UI_MODE_TYPE_UNDEFINED>|<get-UI_MODE_TYPE_UNDEFINED>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_VR_HEADSET // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET|{}UI_MODE_TYPE_VR_HEADSET[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_VR_HEADSET>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET.<get-UI_MODE_TYPE_VR_HEADSET>|<get-UI_MODE_TYPE_VR_HEADSET>(){}[0]
|
||||||
|
final const val UI_MODE_TYPE_WATCH // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH|{}UI_MODE_TYPE_WATCH[0]
|
||||||
|
final fun <get-UI_MODE_TYPE_WATCH>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH.<get-UI_MODE_TYPE_WATCH>|<get-UI_MODE_TYPE_WATCH>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object androidx.compose.ui.tooling.preview/Devices { // androidx.compose.ui.tooling.preview/Devices|null[0]
|
||||||
|
final const val AUTOMOTIVE_1024p // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p|{}AUTOMOTIVE_1024p[0]
|
||||||
|
final fun <get-AUTOMOTIVE_1024p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p.<get-AUTOMOTIVE_1024p>|<get-AUTOMOTIVE_1024p>(){}[0]
|
||||||
|
final const val DEFAULT // androidx.compose.ui.tooling.preview/Devices.DEFAULT|{}DEFAULT[0]
|
||||||
|
final fun <get-DEFAULT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
|
||||||
|
final const val DESKTOP // androidx.compose.ui.tooling.preview/Devices.DESKTOP|{}DESKTOP[0]
|
||||||
|
final fun <get-DESKTOP>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DESKTOP.<get-DESKTOP>|<get-DESKTOP>(){}[0]
|
||||||
|
final const val FOLDABLE // androidx.compose.ui.tooling.preview/Devices.FOLDABLE|{}FOLDABLE[0]
|
||||||
|
final fun <get-FOLDABLE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.FOLDABLE.<get-FOLDABLE>|<get-FOLDABLE>(){}[0]
|
||||||
|
final const val NEXUS_10 // androidx.compose.ui.tooling.preview/Devices.NEXUS_10|{}NEXUS_10[0]
|
||||||
|
final fun <get-NEXUS_10>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_10.<get-NEXUS_10>|<get-NEXUS_10>(){}[0]
|
||||||
|
final const val NEXUS_5 // androidx.compose.ui.tooling.preview/Devices.NEXUS_5|{}NEXUS_5[0]
|
||||||
|
final fun <get-NEXUS_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5.<get-NEXUS_5>|<get-NEXUS_5>(){}[0]
|
||||||
|
final const val NEXUS_5X // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X|{}NEXUS_5X[0]
|
||||||
|
final fun <get-NEXUS_5X>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X.<get-NEXUS_5X>|<get-NEXUS_5X>(){}[0]
|
||||||
|
final const val NEXUS_6 // androidx.compose.ui.tooling.preview/Devices.NEXUS_6|{}NEXUS_6[0]
|
||||||
|
final fun <get-NEXUS_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6.<get-NEXUS_6>|<get-NEXUS_6>(){}[0]
|
||||||
|
final const val NEXUS_6P // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P|{}NEXUS_6P[0]
|
||||||
|
final fun <get-NEXUS_6P>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P.<get-NEXUS_6P>|<get-NEXUS_6P>(){}[0]
|
||||||
|
final const val NEXUS_7 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7|{}NEXUS_7[0]
|
||||||
|
final fun <get-NEXUS_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7.<get-NEXUS_7>|<get-NEXUS_7>(){}[0]
|
||||||
|
final const val NEXUS_7_2013 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013|{}NEXUS_7_2013[0]
|
||||||
|
final fun <get-NEXUS_7_2013>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013.<get-NEXUS_7_2013>|<get-NEXUS_7_2013>(){}[0]
|
||||||
|
final const val NEXUS_9 // androidx.compose.ui.tooling.preview/Devices.NEXUS_9|{}NEXUS_9[0]
|
||||||
|
final fun <get-NEXUS_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_9.<get-NEXUS_9>|<get-NEXUS_9>(){}[0]
|
||||||
|
final const val PHONE // androidx.compose.ui.tooling.preview/Devices.PHONE|{}PHONE[0]
|
||||||
|
final fun <get-PHONE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PHONE.<get-PHONE>|<get-PHONE>(){}[0]
|
||||||
|
final const val PIXEL // androidx.compose.ui.tooling.preview/Devices.PIXEL|{}PIXEL[0]
|
||||||
|
final fun <get-PIXEL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL.<get-PIXEL>|<get-PIXEL>(){}[0]
|
||||||
|
final const val PIXEL_2 // androidx.compose.ui.tooling.preview/Devices.PIXEL_2|{}PIXEL_2[0]
|
||||||
|
final fun <get-PIXEL_2>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2.<get-PIXEL_2>|<get-PIXEL_2>(){}[0]
|
||||||
|
final const val PIXEL_2_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL|{}PIXEL_2_XL[0]
|
||||||
|
final fun <get-PIXEL_2_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL.<get-PIXEL_2_XL>|<get-PIXEL_2_XL>(){}[0]
|
||||||
|
final const val PIXEL_3 // androidx.compose.ui.tooling.preview/Devices.PIXEL_3|{}PIXEL_3[0]
|
||||||
|
final fun <get-PIXEL_3>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3.<get-PIXEL_3>|<get-PIXEL_3>(){}[0]
|
||||||
|
final const val PIXEL_3A // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A|{}PIXEL_3A[0]
|
||||||
|
final fun <get-PIXEL_3A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A.<get-PIXEL_3A>|<get-PIXEL_3A>(){}[0]
|
||||||
|
final const val PIXEL_3A_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL|{}PIXEL_3A_XL[0]
|
||||||
|
final fun <get-PIXEL_3A_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL.<get-PIXEL_3A_XL>|<get-PIXEL_3A_XL>(){}[0]
|
||||||
|
final const val PIXEL_3_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL|{}PIXEL_3_XL[0]
|
||||||
|
final fun <get-PIXEL_3_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL.<get-PIXEL_3_XL>|<get-PIXEL_3_XL>(){}[0]
|
||||||
|
final const val PIXEL_4 // androidx.compose.ui.tooling.preview/Devices.PIXEL_4|{}PIXEL_4[0]
|
||||||
|
final fun <get-PIXEL_4>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4.<get-PIXEL_4>|<get-PIXEL_4>(){}[0]
|
||||||
|
final const val PIXEL_4A // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A|{}PIXEL_4A[0]
|
||||||
|
final fun <get-PIXEL_4A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A.<get-PIXEL_4A>|<get-PIXEL_4A>(){}[0]
|
||||||
|
final const val PIXEL_4_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL|{}PIXEL_4_XL[0]
|
||||||
|
final fun <get-PIXEL_4_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL.<get-PIXEL_4_XL>|<get-PIXEL_4_XL>(){}[0]
|
||||||
|
final const val PIXEL_5 // androidx.compose.ui.tooling.preview/Devices.PIXEL_5|{}PIXEL_5[0]
|
||||||
|
final fun <get-PIXEL_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_5.<get-PIXEL_5>|<get-PIXEL_5>(){}[0]
|
||||||
|
final const val PIXEL_6 // androidx.compose.ui.tooling.preview/Devices.PIXEL_6|{}PIXEL_6[0]
|
||||||
|
final fun <get-PIXEL_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6.<get-PIXEL_6>|<get-PIXEL_6>(){}[0]
|
||||||
|
final const val PIXEL_6A // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A|{}PIXEL_6A[0]
|
||||||
|
final fun <get-PIXEL_6A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A.<get-PIXEL_6A>|<get-PIXEL_6A>(){}[0]
|
||||||
|
final const val PIXEL_6_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO|{}PIXEL_6_PRO[0]
|
||||||
|
final fun <get-PIXEL_6_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO.<get-PIXEL_6_PRO>|<get-PIXEL_6_PRO>(){}[0]
|
||||||
|
final const val PIXEL_7 // androidx.compose.ui.tooling.preview/Devices.PIXEL_7|{}PIXEL_7[0]
|
||||||
|
final fun <get-PIXEL_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7.<get-PIXEL_7>|<get-PIXEL_7>(){}[0]
|
||||||
|
final const val PIXEL_7A // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A|{}PIXEL_7A[0]
|
||||||
|
final fun <get-PIXEL_7A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A.<get-PIXEL_7A>|<get-PIXEL_7A>(){}[0]
|
||||||
|
final const val PIXEL_7_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO|{}PIXEL_7_PRO[0]
|
||||||
|
final fun <get-PIXEL_7_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO.<get-PIXEL_7_PRO>|<get-PIXEL_7_PRO>(){}[0]
|
||||||
|
final const val PIXEL_8 // androidx.compose.ui.tooling.preview/Devices.PIXEL_8|{}PIXEL_8[0]
|
||||||
|
final fun <get-PIXEL_8>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8.<get-PIXEL_8>|<get-PIXEL_8>(){}[0]
|
||||||
|
final const val PIXEL_8A // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A|{}PIXEL_8A[0]
|
||||||
|
final fun <get-PIXEL_8A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A.<get-PIXEL_8A>|<get-PIXEL_8A>(){}[0]
|
||||||
|
final const val PIXEL_8_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO|{}PIXEL_8_PRO[0]
|
||||||
|
final fun <get-PIXEL_8_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO.<get-PIXEL_8_PRO>|<get-PIXEL_8_PRO>(){}[0]
|
||||||
|
final const val PIXEL_9 // androidx.compose.ui.tooling.preview/Devices.PIXEL_9|{}PIXEL_9[0]
|
||||||
|
final fun <get-PIXEL_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9.<get-PIXEL_9>|<get-PIXEL_9>(){}[0]
|
||||||
|
final const val PIXEL_9_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO|{}PIXEL_9_PRO[0]
|
||||||
|
final fun <get-PIXEL_9_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO.<get-PIXEL_9_PRO>|<get-PIXEL_9_PRO>(){}[0]
|
||||||
|
final const val PIXEL_9_PRO_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD|{}PIXEL_9_PRO_FOLD[0]
|
||||||
|
final fun <get-PIXEL_9_PRO_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD.<get-PIXEL_9_PRO_FOLD>|<get-PIXEL_9_PRO_FOLD>(){}[0]
|
||||||
|
final const val PIXEL_9_PRO_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL|{}PIXEL_9_PRO_XL[0]
|
||||||
|
final fun <get-PIXEL_9_PRO_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL.<get-PIXEL_9_PRO_XL>|<get-PIXEL_9_PRO_XL>(){}[0]
|
||||||
|
final const val PIXEL_C // androidx.compose.ui.tooling.preview/Devices.PIXEL_C|{}PIXEL_C[0]
|
||||||
|
final fun <get-PIXEL_C>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_C.<get-PIXEL_C>|<get-PIXEL_C>(){}[0]
|
||||||
|
final const val PIXEL_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD|{}PIXEL_FOLD[0]
|
||||||
|
final fun <get-PIXEL_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD.<get-PIXEL_FOLD>|<get-PIXEL_FOLD>(){}[0]
|
||||||
|
final const val PIXEL_TABLET // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET|{}PIXEL_TABLET[0]
|
||||||
|
final fun <get-PIXEL_TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET.<get-PIXEL_TABLET>|<get-PIXEL_TABLET>(){}[0]
|
||||||
|
final const val PIXEL_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL|{}PIXEL_XL[0]
|
||||||
|
final fun <get-PIXEL_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL.<get-PIXEL_XL>|<get-PIXEL_XL>(){}[0]
|
||||||
|
final const val TABLET // androidx.compose.ui.tooling.preview/Devices.TABLET|{}TABLET[0]
|
||||||
|
final fun <get-TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TABLET.<get-TABLET>|<get-TABLET>(){}[0]
|
||||||
|
final const val TV_1080p // androidx.compose.ui.tooling.preview/Devices.TV_1080p|{}TV_1080p[0]
|
||||||
|
final fun <get-TV_1080p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_1080p.<get-TV_1080p>|<get-TV_1080p>(){}[0]
|
||||||
|
final const val TV_720p // androidx.compose.ui.tooling.preview/Devices.TV_720p|{}TV_720p[0]
|
||||||
|
final fun <get-TV_720p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_720p.<get-TV_720p>|<get-TV_720p>(){}[0]
|
||||||
|
final const val WEAR_OS_LARGE_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND|{}WEAR_OS_LARGE_ROUND[0]
|
||||||
|
final fun <get-WEAR_OS_LARGE_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND.<get-WEAR_OS_LARGE_ROUND>|<get-WEAR_OS_LARGE_ROUND>(){}[0]
|
||||||
|
final const val WEAR_OS_RECT // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT|{}WEAR_OS_RECT[0]
|
||||||
|
final fun <get-WEAR_OS_RECT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT.<get-WEAR_OS_RECT>|<get-WEAR_OS_RECT>(){}[0]
|
||||||
|
final const val WEAR_OS_SMALL_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND|{}WEAR_OS_SMALL_ROUND[0]
|
||||||
|
final fun <get-WEAR_OS_SMALL_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND.<get-WEAR_OS_SMALL_ROUND>|<get-WEAR_OS_SMALL_ROUND>(){}[0]
|
||||||
|
final const val WEAR_OS_SQUARE // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE|{}WEAR_OS_SQUARE[0]
|
||||||
|
final fun <get-WEAR_OS_SQUARE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE.<get-WEAR_OS_SQUARE>|<get-WEAR_OS_SQUARE>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object androidx.compose.ui.tooling.preview/Wallpapers { // androidx.compose.ui.tooling.preview/Wallpapers|null[0]
|
||||||
|
final const val BLUE_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE|{}BLUE_DOMINATED_EXAMPLE[0]
|
||||||
|
final fun <get-BLUE_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE.<get-BLUE_DOMINATED_EXAMPLE>|<get-BLUE_DOMINATED_EXAMPLE>(){}[0]
|
||||||
|
final const val GREEN_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE|{}GREEN_DOMINATED_EXAMPLE[0]
|
||||||
|
final fun <get-GREEN_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE.<get-GREEN_DOMINATED_EXAMPLE>|<get-GREEN_DOMINATED_EXAMPLE>(){}[0]
|
||||||
|
final const val NONE // androidx.compose.ui.tooling.preview/Wallpapers.NONE|{}NONE[0]
|
||||||
|
final fun <get-NONE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.NONE.<get-NONE>|<get-NONE>(){}[0]
|
||||||
|
final const val RED_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE|{}RED_DOMINATED_EXAMPLE[0]
|
||||||
|
final fun <get-RED_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE.<get-RED_DOMINATED_EXAMPLE>|<get-RED_DOMINATED_EXAMPLE>(){}[0]
|
||||||
|
final const val YELLOW_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE|{}YELLOW_DOMINATED_EXAMPLE[0]
|
||||||
|
final fun <get-YELLOW_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE.<get-YELLOW_DOMINATED_EXAMPLE>|<get-YELLOW_DOMINATED_EXAMPLE>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop[0]
|
||||||
|
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop[0]
|
||||||
|
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop|#static{}androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop[0]
|
||||||
|
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop|#static{}androidx_compose_ui_tooling_preview_Devices$stableprop[0]
|
||||||
|
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop|#static{}androidx_compose_ui_tooling_preview_Wallpapers$stableprop[0]
|
||||||
|
|
||||||
|
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter|androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter|androidx_compose_ui_tooling_preview_Devices$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter|androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(){}[0]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// :ui-tooling-preview — androidx.compose.ui.tooling.preview.* (the common
|
||||||
|
// @Preview annotation + PreviewParameterProvider), vendored VERBATIM from
|
||||||
|
// upstream (compose/ui/ui-tooling-preview). The Maven artifact ships no
|
||||||
|
// mingwX64/linux klibs — apps declare the OFFICIAL coords in commonMain and
|
||||||
|
// the root bridge substitutes this module on native configurations. The
|
||||||
|
// annotations are IDE-only metadata: previews render through the app's JVM
|
||||||
|
// target against upstream Compose Desktop.
|
||||||
|
// Never hand-edit src/vendor/ — change the manifest + `python scripts/compose-fork/sync.py compose/ui/ui-tooling-preview`.
|
||||||
|
// Publication artifactId: ui-tooling-preview.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
|
}
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
nativeMain { kotlin.srcDir("src/vendor/native/kotlin") }
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
SET_REPO=https://github.com/JetBrains/compose-multiplatform-core@<COMPOSE_CORE_REF>
|
||||||
|
SET_FOLDER=compose/ui/ui-tooling-preview/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Device.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
|
||||||
|
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt
|
||||||
|
|
||||||
|
nonJvmMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.native.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# desktopMain (1)
|
||||||
|
# | desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt -> src/vendor/desktop/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt
|
||||||
|
# jvmAndAndroidMain (1)
|
||||||
|
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -13,7 +13,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
api(project(":ui-geometry"))
|
api(project(":ui-geometry"))
|
||||||
api(project(":ui-util"))
|
api(project(":ui-util"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
api("androidx.collection:collection:1.5.0")
|
api("androidx.collection:collection:1.5.0")
|
||||||
}
|
}
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
|||||||
+93
-189
@@ -1834,6 +1834,10 @@ abstract interface androidx.compose.ui/UiMediaScope { // androidx.compose.ui/UiM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract interface com.compose.sdl.graphics/EncodedImageDecoder { // com.compose.sdl.graphics/EncodedImageDecoder|null[0]
|
||||||
|
abstract fun decode(kotlin/ByteArray): androidx.compose.ui.graphics/ImageBitmap? // com.compose.sdl.graphics/EncodedImageDecoder.decode|decode(kotlin.ByteArray){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
abstract interface com.compose.sdl.graphics/NativePainterCanvas { // com.compose.sdl.graphics/NativePainterCanvas|null[0]
|
abstract interface com.compose.sdl.graphics/NativePainterCanvas { // com.compose.sdl.graphics/NativePainterCanvas|null[0]
|
||||||
abstract fun drawNativePainter(kotlin/String, com.compose.sdl.res/ResourceKind, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, androidx.compose.ui.layout/ContentScale, kotlin/Float) // com.compose.sdl.graphics/NativePainterCanvas.drawNativePainter|drawNativePainter(kotlin.String;com.compose.sdl.res.ResourceKind;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.layout.ContentScale;kotlin.Float){}[0]
|
abstract fun drawNativePainter(kotlin/String, com.compose.sdl.res/ResourceKind, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, androidx.compose.ui.layout/ContentScale, kotlin/Float) // com.compose.sdl.graphics/NativePainterCanvas.drawNativePainter|drawNativePainter(kotlin.String;com.compose.sdl.res.ResourceKind;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.layout.ContentScale;kotlin.Float){}[0]
|
||||||
}
|
}
|
||||||
@@ -2522,9 +2526,23 @@ final class androidx.compose.ui.autofill/AutofillTree { // androidx.compose.ui.a
|
|||||||
final fun plusAssign(androidx.compose.ui.autofill/AutofillNode) // androidx.compose.ui.autofill/AutofillTree.plusAssign|plusAssign(androidx.compose.ui.autofill.AutofillNode){}[0]
|
final fun plusAssign(androidx.compose.ui.autofill/AutofillNode) // androidx.compose.ui.autofill/AutofillTree.plusAssign|plusAssign(androidx.compose.ui.autofill.AutofillNode){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class androidx.compose.ui.draganddrop/DragAndDropEvent // androidx.compose.ui.draganddrop/DragAndDropEvent|null[0]
|
final class androidx.compose.ui.draganddrop/DragAndDropEvent { // androidx.compose.ui.draganddrop/DragAndDropEvent|null[0]
|
||||||
|
final val position // androidx.compose.ui.draganddrop/DragAndDropEvent.position|{}position[0]
|
||||||
|
final fun <get-position>(): androidx.compose.ui.geometry/Offset // androidx.compose.ui.draganddrop/DragAndDropEvent.position.<get-position>|<get-position>(){}[0]
|
||||||
|
final val transferData // androidx.compose.ui.draganddrop/DragAndDropEvent.transferData|{}transferData[0]
|
||||||
|
final fun <get-transferData>(): androidx.compose.ui.draganddrop/DragAndDropTransferData // androidx.compose.ui.draganddrop/DragAndDropEvent.transferData.<get-transferData>|<get-transferData>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
final class androidx.compose.ui.draganddrop/DragAndDropTransferData // androidx.compose.ui.draganddrop/DragAndDropTransferData|null[0]
|
final class androidx.compose.ui.draganddrop/DragAndDropTransferData { // androidx.compose.ui.draganddrop/DragAndDropTransferData|null[0]
|
||||||
|
final val filePaths // androidx.compose.ui.draganddrop/DragAndDropTransferData.filePaths|{}filePaths[0]
|
||||||
|
final fun <get-filePaths>(): kotlin.collections/List<kotlin/String> // androidx.compose.ui.draganddrop/DragAndDropTransferData.filePaths.<get-filePaths>|<get-filePaths>(){}[0]
|
||||||
|
final val hasFiles // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasFiles|{}hasFiles[0]
|
||||||
|
final fun <get-hasFiles>(): kotlin/Boolean // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasFiles.<get-hasFiles>|<get-hasFiles>(){}[0]
|
||||||
|
final val hasText // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasText|{}hasText[0]
|
||||||
|
final fun <get-hasText>(): kotlin/Boolean // androidx.compose.ui.draganddrop/DragAndDropTransferData.hasText.<get-hasText>|<get-hasText>(){}[0]
|
||||||
|
final val text // androidx.compose.ui.draganddrop/DragAndDropTransferData.text|{}text[0]
|
||||||
|
final fun <get-text>(): kotlin/String? // androidx.compose.ui.draganddrop/DragAndDropTransferData.text.<get-text>|<get-text>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
final class androidx.compose.ui.draw/CacheDrawScope : androidx.compose.ui.unit/Density { // androidx.compose.ui.draw/CacheDrawScope|null[0]
|
final class androidx.compose.ui.draw/CacheDrawScope : androidx.compose.ui.unit/Density { // androidx.compose.ui.draw/CacheDrawScope|null[0]
|
||||||
final val density // androidx.compose.ui.draw/CacheDrawScope.density|{}density[0]
|
final val density // androidx.compose.ui.draw/CacheDrawScope.density|{}density[0]
|
||||||
@@ -3654,9 +3672,11 @@ final class androidx.compose.ui.platform/ClipEntry { // androidx.compose.ui.plat
|
|||||||
final val clipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata|{}clipMetadata[0]
|
final val clipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata|{}clipMetadata[0]
|
||||||
final fun <get-clipMetadata>(): androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata.<get-clipMetadata>|<get-clipMetadata>(){}[0]
|
final fun <get-clipMetadata>(): androidx.compose.ui.platform/ClipMetadata // androidx.compose.ui.platform/ClipEntry.clipMetadata.<get-clipMetadata>|<get-clipMetadata>(){}[0]
|
||||||
|
|
||||||
|
final fun getImage(): kotlin/ByteArray? // androidx.compose.ui.platform/ClipEntry.getImage|getImage(){}[0]
|
||||||
final fun getPlainText(): kotlin/String? // androidx.compose.ui.platform/ClipEntry.getPlainText|getPlainText(){}[0]
|
final fun getPlainText(): kotlin/String? // androidx.compose.ui.platform/ClipEntry.getPlainText|getPlainText(){}[0]
|
||||||
|
|
||||||
final object Companion { // androidx.compose.ui.platform/ClipEntry.Companion|null[0]
|
final object Companion { // androidx.compose.ui.platform/ClipEntry.Companion|null[0]
|
||||||
|
final fun withImage(kotlin/ByteArray): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withImage|withImage(kotlin.ByteArray){}[0]
|
||||||
final fun withPlainText(kotlin/String): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withPlainText|withPlainText(kotlin.String){}[0]
|
final fun withPlainText(kotlin/String): androidx.compose.ui.platform/ClipEntry // androidx.compose.ui.platform/ClipEntry.Companion.withPlainText|withPlainText(kotlin.String){}[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5348,136 +5368,6 @@ final class com.compose.sdl.element/GraphicsLayerNode : androidx.compose.ui/Modi
|
|||||||
final fun <set-translationY>(kotlin/Float) // com.compose.sdl.element/GraphicsLayerNode.translationY.<set-translationY>|<set-translationY>(kotlin.Float){}[0]
|
final fun <set-translationY>(kotlin/Float) // com.compose.sdl.element/GraphicsLayerNode.translationY.<set-translationY>|<set-translationY>(kotlin.Float){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl.element/MiddleClickModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/MiddleClickNode> { // com.compose.sdl.element/MiddleClickModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickModifier.<init>|<init>(kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val onClick // com.compose.sdl.element/MiddleClickModifier.onClick|{}onClick[0]
|
|
||||||
final fun <get-onClick>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/MiddleClickModifier.onClick.<get-onClick>|<get-onClick>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/MiddleClickNode // com.compose.sdl.element/MiddleClickModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/MiddleClickModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/MiddleClickModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/MiddleClickNode) // com.compose.sdl.element/MiddleClickModifier.update|update(com.compose.sdl.element.MiddleClickNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/MiddleClickNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/MiddleClickNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickNode.<init>|<init>(kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var onClick // com.compose.sdl.element/MiddleClickNode.onClick|{}onClick[0]
|
|
||||||
final fun <get-onClick>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/MiddleClickNode.onClick.<get-onClick>|<get-onClick>(){}[0]
|
|
||||||
final fun <set-onClick>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/MiddleClickNode.onClick.<set-onClick>|<set-onClick>(kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnDragModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnDragNode> { // com.compose.sdl.element/OnDragModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val onDrag // com.compose.sdl.element/OnDragModifier.onDrag|{}onDrag[0]
|
|
||||||
final fun <get-onDrag>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onDrag.<get-onDrag>|<get-onDrag>(){}[0]
|
|
||||||
final val onEnd // com.compose.sdl.element/OnDragModifier.onEnd|{}onEnd[0]
|
|
||||||
final fun <get-onEnd>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onEnd.<get-onEnd>|<get-onEnd>(){}[0]
|
|
||||||
final val onStart // com.compose.sdl.element/OnDragModifier.onStart|{}onStart[0]
|
|
||||||
final fun <get-onStart>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragModifier.onStart.<get-onStart>|<get-onStart>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/OnDragNode // com.compose.sdl.element/OnDragModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnDragModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnDragModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/OnDragNode) // com.compose.sdl.element/OnDragModifier.update|update(com.compose.sdl.element.OnDragNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnDragNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnDragNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>, kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var onDrag // com.compose.sdl.element/OnDragNode.onDrag|{}onDrag[0]
|
|
||||||
final fun <get-onDrag>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragNode.onDrag.<get-onDrag>|<get-onDrag>(){}[0]
|
|
||||||
final fun <set-onDrag>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onDrag.<set-onDrag>|<set-onDrag>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
final var onEnd // com.compose.sdl.element/OnDragNode.onEnd|{}onEnd[0]
|
|
||||||
final fun <get-onEnd>(): kotlin/Function0<kotlin/Unit> // com.compose.sdl.element/OnDragNode.onEnd.<get-onEnd>|<get-onEnd>(){}[0]
|
|
||||||
final fun <set-onEnd>(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onEnd.<set-onEnd>|<set-onEnd>(kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
final var onStart // com.compose.sdl.element/OnDragNode.onStart|{}onStart[0]
|
|
||||||
final fun <get-onStart>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnDragNode.onStart.<get-onStart>|<get-onStart>(){}[0]
|
|
||||||
final fun <set-onStart>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnDragNode.onStart.<set-onStart>|<set-onStart>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnPressedModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnPressedNode> { // com.compose.sdl.element/OnPressedModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val handler // com.compose.sdl.element/OnPressedModifier.handler|{}handler[0]
|
|
||||||
final fun <get-handler>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnPressedModifier.handler.<get-handler>|<get-handler>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/OnPressedNode // com.compose.sdl.element/OnPressedModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnPressedModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnPressedModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/OnPressedNode) // com.compose.sdl.element/OnPressedModifier.update|update(com.compose.sdl.element.OnPressedNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnPressedNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnPressedNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var handler // com.compose.sdl.element/OnPressedNode.handler|{}handler[0]
|
|
||||||
final fun <get-handler>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/OnPressedNode.handler.<get-handler>|<get-handler>(){}[0]
|
|
||||||
final fun <set-handler>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/OnPressedNode.handler.<set-handler>|<set-handler>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnTextInputModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/OnTextInputNode> { // com.compose.sdl.element/OnTextInputModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputModifier.<init>|<init>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val handler // com.compose.sdl.element/OnTextInputModifier.handler|{}handler[0]
|
|
||||||
final fun <get-handler>(): kotlin/Function1<kotlin/String, kotlin/Unit> // com.compose.sdl.element/OnTextInputModifier.handler.<get-handler>|<get-handler>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/OnTextInputNode // com.compose.sdl.element/OnTextInputModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/OnTextInputModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/OnTextInputModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/OnTextInputNode) // com.compose.sdl.element/OnTextInputModifier.update|update(com.compose.sdl.element.OnTextInputNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/OnTextInputNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/OnTextInputNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputNode.<init>|<init>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var handler // com.compose.sdl.element/OnTextInputNode.handler|{}handler[0]
|
|
||||||
final fun <get-handler>(): kotlin/Function1<kotlin/String, kotlin/Unit> // com.compose.sdl.element/OnTextInputNode.handler.<get-handler>|<get-handler>(){}[0]
|
|
||||||
final fun <set-handler>(kotlin/Function1<kotlin/String, kotlin/Unit>) // com.compose.sdl.element/OnTextInputNode.handler.<set-handler>|<set-handler>(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/PressableModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/PressableNode> { // com.compose.sdl.element/PressableModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableModifier.<init>|<init>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val onChange // com.compose.sdl.element/PressableModifier.onChange|{}onChange[0]
|
|
||||||
final fun <get-onChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit> // com.compose.sdl.element/PressableModifier.onChange.<get-onChange>|<get-onChange>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/PressableNode // com.compose.sdl.element/PressableModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/PressableModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/PressableModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/PressableNode) // com.compose.sdl.element/PressableModifier.update|update(com.compose.sdl.element.PressableNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/PressableNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/PressableNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableNode.<init>|<init>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var onChange // com.compose.sdl.element/PressableNode.onChange|{}onChange[0]
|
|
||||||
final fun <get-onChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit> // com.compose.sdl.element/PressableNode.onChange.<get-onChange>|<get-onChange>(){}[0]
|
|
||||||
final fun <set-onChange>(kotlin/Function1<kotlin/Boolean, kotlin/Unit>) // com.compose.sdl.element/PressableNode.onChange.<set-onChange>|<set-onChange>(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/SecondaryClickModifier : androidx.compose.ui.node/ModifierNodeElement<com.compose.sdl.element/SecondaryClickNode> { // com.compose.sdl.element/SecondaryClickModifier|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickModifier.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final val onClick // com.compose.sdl.element/SecondaryClickModifier.onClick|{}onClick[0]
|
|
||||||
final fun <get-onClick>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/SecondaryClickModifier.onClick.<get-onClick>|<get-onClick>(){}[0]
|
|
||||||
|
|
||||||
final fun create(): com.compose.sdl.element/SecondaryClickNode // com.compose.sdl.element/SecondaryClickModifier.create|create(){}[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.element/SecondaryClickModifier.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.element/SecondaryClickModifier.hashCode|hashCode(){}[0]
|
|
||||||
final fun update(com.compose.sdl.element/SecondaryClickNode) // com.compose.sdl.element/SecondaryClickModifier.update|update(com.compose.sdl.element.SecondaryClickNode){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.element/SecondaryClickNode : androidx.compose.ui/Modifier.Node { // com.compose.sdl.element/SecondaryClickNode|null[0]
|
|
||||||
constructor <init>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickNode.<init>|<init>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
|
|
||||||
final var onClick // com.compose.sdl.element/SecondaryClickNode.onClick|{}onClick[0]
|
|
||||||
final fun <get-onClick>(): kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> // com.compose.sdl.element/SecondaryClickNode.onClick.<get-onClick>|<get-onClick>(){}[0]
|
|
||||||
final fun <set-onClick>(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>) // com.compose.sdl.element/SecondaryClickNode.onClick.<set-onClick>|<set-onClick>(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.graphics/ProjectCanvas : androidx.compose.ui.graphics/Canvas { // com.compose.sdl.graphics/ProjectCanvas|null[0]
|
final class com.compose.sdl.graphics/ProjectCanvas : androidx.compose.ui.graphics/Canvas { // com.compose.sdl.graphics/ProjectCanvas|null[0]
|
||||||
constructor <init>() // com.compose.sdl.graphics/ProjectCanvas.<init>|<init>(){}[0]
|
constructor <init>() // com.compose.sdl.graphics/ProjectCanvas.<init>|<init>(){}[0]
|
||||||
|
|
||||||
@@ -5649,8 +5539,6 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
|
|||||||
|
|
||||||
final val applier // com.compose.sdl.node/ComposeRootHost.applier|{}applier[0]
|
final val applier // com.compose.sdl.node/ComposeRootHost.applier|{}applier[0]
|
||||||
final fun <get-applier>(): androidx.compose.runtime/Applier<*> // com.compose.sdl.node/ComposeRootHost.applier.<get-applier>|<get-applier>(){}[0]
|
final fun <get-applier>(): androidx.compose.runtime/Applier<*> // com.compose.sdl.node/ComposeRootHost.applier.<get-applier>|<get-applier>(){}[0]
|
||||||
final val autofillTree // com.compose.sdl.node/ComposeRootHost.autofillTree|{}autofillTree[0]
|
|
||||||
final fun <get-autofillTree>(): androidx.compose.ui.autofill/AutofillTree // com.compose.sdl.node/ComposeRootHost.autofillTree.<get-autofillTree>|<get-autofillTree>(){}[0]
|
|
||||||
final val density // com.compose.sdl.node/ComposeRootHost.density|{}density[0]
|
final val density // com.compose.sdl.node/ComposeRootHost.density|{}density[0]
|
||||||
final fun <get-density>(): androidx.compose.ui.unit/Density // com.compose.sdl.node/ComposeRootHost.density.<get-density>|<get-density>(){}[0]
|
final fun <get-density>(): androidx.compose.ui.unit/Density // com.compose.sdl.node/ComposeRootHost.density.<get-density>|<get-density>(){}[0]
|
||||||
final val focusManager // com.compose.sdl.node/ComposeRootHost.focusManager|{}focusManager[0]
|
final val focusManager // com.compose.sdl.node/ComposeRootHost.focusManager|{}focusManager[0]
|
||||||
@@ -5665,8 +5553,6 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
|
|||||||
final fun <get-layoutDirection>(): androidx.compose.ui.unit/LayoutDirection // com.compose.sdl.node/ComposeRootHost.layoutDirection.<get-layoutDirection>|<get-layoutDirection>(){}[0]
|
final fun <get-layoutDirection>(): androidx.compose.ui.unit/LayoutDirection // com.compose.sdl.node/ComposeRootHost.layoutDirection.<get-layoutDirection>|<get-layoutDirection>(){}[0]
|
||||||
final val softwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController|{}softwareKeyboardController[0]
|
final val softwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController|{}softwareKeyboardController[0]
|
||||||
final fun <get-softwareKeyboardController>(): androidx.compose.ui.platform/SoftwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController.<get-softwareKeyboardController>|<get-softwareKeyboardController>(){}[0]
|
final fun <get-softwareKeyboardController>(): androidx.compose.ui.platform/SoftwareKeyboardController // com.compose.sdl.node/ComposeRootHost.softwareKeyboardController.<get-softwareKeyboardController>|<get-softwareKeyboardController>(){}[0]
|
||||||
final val textInputService // com.compose.sdl.node/ComposeRootHost.textInputService|{}textInputService[0]
|
|
||||||
final fun <get-textInputService>(): androidx.compose.ui.text.input/TextInputService // com.compose.sdl.node/ComposeRootHost.textInputService.<get-textInputService>|<get-textInputService>(){}[0]
|
|
||||||
final val textToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar|{}textToolbar[0]
|
final val textToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar|{}textToolbar[0]
|
||||||
final fun <get-textToolbar>(): androidx.compose.ui.platform/TextToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar.<get-textToolbar>|<get-textToolbar>(){}[0]
|
final fun <get-textToolbar>(): androidx.compose.ui.platform/TextToolbar // com.compose.sdl.node/ComposeRootHost.textToolbar.<get-textToolbar>|<get-textToolbar>(){}[0]
|
||||||
final val viewConfiguration // com.compose.sdl.node/ComposeRootHost.viewConfiguration|{}viewConfiguration[0]
|
final val viewConfiguration // com.compose.sdl.node/ComposeRootHost.viewConfiguration|{}viewConfiguration[0]
|
||||||
@@ -5676,9 +5562,12 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
|
|||||||
|
|
||||||
final fun attach() // com.compose.sdl.node/ComposeRootHost.attach|attach(){}[0]
|
final fun attach() // com.compose.sdl.node/ComposeRootHost.attach|attach(){}[0]
|
||||||
final fun dispatchKeyEvent(androidx.compose.ui.input.key/KeyEvent): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchKeyEvent|dispatchKeyEvent(androidx.compose.ui.input.key.KeyEvent){}[0]
|
final fun dispatchKeyEvent(androidx.compose.ui.input.key/KeyEvent): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchKeyEvent|dispatchKeyEvent(androidx.compose.ui.input.key.KeyEvent){}[0]
|
||||||
final fun dispatchTextInput(kotlin/String): kotlin/Boolean // com.compose.sdl.node/ComposeRootHost.dispatchTextInput|dispatchTextInput(kotlin.String){}[0]
|
|
||||||
final fun measureAndLayout() // com.compose.sdl.node/ComposeRootHost.measureAndLayout|measureAndLayout(){}[0]
|
final fun measureAndLayout() // com.compose.sdl.node/ComposeRootHost.measureAndLayout|measureAndLayout(){}[0]
|
||||||
final fun onPointer(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int) // com.compose.sdl.node/ComposeRootHost.onPointer|onPointer(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int){}[0]
|
final fun onDropBegin() // com.compose.sdl.node/ComposeRootHost.onDropBegin|onDropBegin(){}[0]
|
||||||
|
final fun onDropComplete() // com.compose.sdl.node/ComposeRootHost.onDropComplete|onDropComplete(){}[0]
|
||||||
|
final fun onDropFile(kotlin/String) // com.compose.sdl.node/ComposeRootHost.onDropFile|onDropFile(kotlin.String){}[0]
|
||||||
|
final fun onDropPosition(kotlin/Float, kotlin/Float) // com.compose.sdl.node/ComposeRootHost.onDropPosition|onDropPosition(kotlin.Float;kotlin.Float){}[0]
|
||||||
|
final fun onDropText(kotlin/String) // com.compose.sdl.node/ComposeRootHost.onDropText|onDropText(kotlin.String){}[0]
|
||||||
final fun onPointerRaw(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onPointerRaw|onPointerRaw(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Long){}[0]
|
final fun onPointerRaw(kotlin/Float, kotlin/Float, kotlin/Int, kotlin/Int, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onPointerRaw|onPointerRaw(kotlin.Float;kotlin.Float;kotlin.Int;kotlin.Int;kotlin.Long){}[0]
|
||||||
final fun onWheel(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onWheel|onWheel(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Long){}[0]
|
final fun onWheel(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Long) // com.compose.sdl.node/ComposeRootHost.onWheel|onWheel(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Long){}[0]
|
||||||
final fun sendAnimationFrame(kotlin/Long) // com.compose.sdl.node/ComposeRootHost.sendAnimationFrame|sendAnimationFrame(kotlin.Long){}[0]
|
final fun sendAnimationFrame(kotlin/Long) // com.compose.sdl.node/ComposeRootHost.sendAnimationFrame|sendAnimationFrame(kotlin.Long){}[0]
|
||||||
@@ -8387,6 +8276,45 @@ sealed class androidx.compose.ui.text.font/SystemFontFamily : androidx.compose.u
|
|||||||
sealed class androidx.compose.ui.text/TtsAnnotation : androidx.compose.ui.text/AnnotatedString.Annotation // androidx.compose.ui.text/TtsAnnotation|null[0]
|
sealed class androidx.compose.ui.text/TtsAnnotation : androidx.compose.ui.text/AnnotatedString.Annotation // androidx.compose.ui.text/TtsAnnotation|null[0]
|
||||||
|
|
||||||
sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
|
sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
|
||||||
|
final enum class DropPhase : kotlin/Enum<com.compose.sdl/AppEvent.DropPhase> { // com.compose.sdl/AppEvent.DropPhase|null[0]
|
||||||
|
enum entry BEGIN // com.compose.sdl/AppEvent.DropPhase.BEGIN|null[0]
|
||||||
|
enum entry COMPLETE // com.compose.sdl/AppEvent.DropPhase.COMPLETE|null[0]
|
||||||
|
enum entry FILE // com.compose.sdl/AppEvent.DropPhase.FILE|null[0]
|
||||||
|
enum entry POSITION // com.compose.sdl/AppEvent.DropPhase.POSITION|null[0]
|
||||||
|
enum entry TEXT // com.compose.sdl/AppEvent.DropPhase.TEXT|null[0]
|
||||||
|
|
||||||
|
final val entries // com.compose.sdl/AppEvent.DropPhase.entries|#static{}entries[0]
|
||||||
|
final fun <get-entries>(): kotlin.enums/EnumEntries<com.compose.sdl/AppEvent.DropPhase> // com.compose.sdl/AppEvent.DropPhase.entries.<get-entries>|<get-entries>#static(){}[0]
|
||||||
|
|
||||||
|
final fun valueOf(kotlin/String): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.DropPhase.valueOf|valueOf#static(kotlin.String){}[0]
|
||||||
|
final fun values(): kotlin/Array<com.compose.sdl/AppEvent.DropPhase> // com.compose.sdl/AppEvent.DropPhase.values|values#static(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Drop : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Drop|null[0]
|
||||||
|
constructor <init>(kotlin/UInt, com.compose.sdl/AppEvent.DropPhase, kotlin/Float = ..., kotlin/Float = ..., kotlin/String? = ...) // com.compose.sdl/AppEvent.Drop.<init>|<init>(kotlin.UInt;com.compose.sdl.AppEvent.DropPhase;kotlin.Float;kotlin.Float;kotlin.String?){}[0]
|
||||||
|
|
||||||
|
final val data // com.compose.sdl/AppEvent.Drop.data|{}data[0]
|
||||||
|
final fun <get-data>(): kotlin/String? // com.compose.sdl/AppEvent.Drop.data.<get-data>|<get-data>(){}[0]
|
||||||
|
final val phase // com.compose.sdl/AppEvent.Drop.phase|{}phase[0]
|
||||||
|
final fun <get-phase>(): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.Drop.phase.<get-phase>|<get-phase>(){}[0]
|
||||||
|
final val windowId // com.compose.sdl/AppEvent.Drop.windowId|{}windowId[0]
|
||||||
|
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.Drop.windowId.<get-windowId>|<get-windowId>(){}[0]
|
||||||
|
final val x // com.compose.sdl/AppEvent.Drop.x|{}x[0]
|
||||||
|
final fun <get-x>(): kotlin/Float // com.compose.sdl/AppEvent.Drop.x.<get-x>|<get-x>(){}[0]
|
||||||
|
final val y // com.compose.sdl/AppEvent.Drop.y|{}y[0]
|
||||||
|
final fun <get-y>(): kotlin/Float // com.compose.sdl/AppEvent.Drop.y.<get-y>|<get-y>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): kotlin/UInt // com.compose.sdl/AppEvent.Drop.component1|component1(){}[0]
|
||||||
|
final fun component2(): com.compose.sdl/AppEvent.DropPhase // com.compose.sdl/AppEvent.Drop.component2|component2(){}[0]
|
||||||
|
final fun component3(): kotlin/Float // com.compose.sdl/AppEvent.Drop.component3|component3(){}[0]
|
||||||
|
final fun component4(): kotlin/Float // com.compose.sdl/AppEvent.Drop.component4|component4(){}[0]
|
||||||
|
final fun component5(): kotlin/String? // com.compose.sdl/AppEvent.Drop.component5|component5(){}[0]
|
||||||
|
final fun copy(kotlin/UInt = ..., com.compose.sdl/AppEvent.DropPhase = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/String? = ...): com.compose.sdl/AppEvent.Drop // com.compose.sdl/AppEvent.Drop.copy|copy(kotlin.UInt;com.compose.sdl.AppEvent.DropPhase;kotlin.Float;kotlin.Float;kotlin.String?){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.Drop.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.Drop.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.Drop.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
final class Key : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Key|null[0]
|
final class Key : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Key|null[0]
|
||||||
constructor <init>(androidx.compose.ui.input.key/KeyEvent, kotlin/UInt = ...) // com.compose.sdl/AppEvent.Key.<init>|<init>(androidx.compose.ui.input.key.KeyEvent;kotlin.UInt){}[0]
|
constructor <init>(androidx.compose.ui.input.key/KeyEvent, kotlin/UInt = ...) // com.compose.sdl/AppEvent.Key.<init>|<init>(androidx.compose.ui.input.key.KeyEvent;kotlin.UInt){}[0]
|
||||||
|
|
||||||
@@ -8473,6 +8401,25 @@ sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
|
|||||||
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.TextInput.toString|toString(){}[0]
|
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.TextInput.toString|toString(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class WindowActivation : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowActivation|null[0]
|
||||||
|
constructor <init>(kotlin/UInt = ..., kotlin/Boolean? = ..., kotlin/Boolean? = ...) // com.compose.sdl/AppEvent.WindowActivation.<init>|<init>(kotlin.UInt;kotlin.Boolean?;kotlin.Boolean?){}[0]
|
||||||
|
|
||||||
|
final val focused // com.compose.sdl/AppEvent.WindowActivation.focused|{}focused[0]
|
||||||
|
final fun <get-focused>(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.focused.<get-focused>|<get-focused>(){}[0]
|
||||||
|
final val visible // com.compose.sdl/AppEvent.WindowActivation.visible|{}visible[0]
|
||||||
|
final fun <get-visible>(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.visible.<get-visible>|<get-visible>(){}[0]
|
||||||
|
final val windowId // com.compose.sdl/AppEvent.WindowActivation.windowId|{}windowId[0]
|
||||||
|
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.WindowActivation.windowId.<get-windowId>|<get-windowId>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): kotlin/UInt // com.compose.sdl/AppEvent.WindowActivation.component1|component1(){}[0]
|
||||||
|
final fun component2(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.component2|component2(){}[0]
|
||||||
|
final fun component3(): kotlin/Boolean? // com.compose.sdl/AppEvent.WindowActivation.component3|component3(){}[0]
|
||||||
|
final fun copy(kotlin/UInt = ..., kotlin/Boolean? = ..., kotlin/Boolean? = ...): com.compose.sdl/AppEvent.WindowActivation // com.compose.sdl/AppEvent.WindowActivation.copy|copy(kotlin.UInt;kotlin.Boolean?;kotlin.Boolean?){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.WindowActivation.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.WindowActivation.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.WindowActivation.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
final class WindowClose : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowClose|null[0]
|
final class WindowClose : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.WindowClose|null[0]
|
||||||
constructor <init>(kotlin/UInt = ...) // com.compose.sdl/AppEvent.WindowClose.<init>|<init>(kotlin.UInt){}[0]
|
constructor <init>(kotlin/UInt = ...) // com.compose.sdl/AppEvent.WindowClose.<init>|<init>(kotlin.UInt){}[0]
|
||||||
|
|
||||||
@@ -9299,16 +9246,6 @@ final val androidx.compose.ui.platform/androidx_compose_ui_platform_InspectorVal
|
|||||||
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop|#static{}androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop[0]
|
final val androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop|#static{}androidx_compose_ui_platform_PlatformWindowInsetsProviderNode$stableprop[0]
|
||||||
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0]
|
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement$stableprop|#static{}androidx_compose_ui_platform_ValueElement$stableprop[0]
|
||||||
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0]
|
final val androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop|#static{}androidx_compose_ui_platform_ValueElementSequence$stableprop[0]
|
||||||
final val androidx.compose.ui.platform/currentClipboard // androidx.compose.ui.platform/currentClipboard|{}currentClipboard[0]
|
|
||||||
final fun <get-currentClipboard>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/currentClipboard.<get-currentClipboard>|<get-currentClipboard>(){}[0]
|
|
||||||
final val androidx.compose.ui.platform/defaultClipboard // androidx.compose.ui.platform/defaultClipboard|{}defaultClipboard[0]
|
|
||||||
final fun <get-defaultClipboard>(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/defaultClipboard.<get-defaultClipboard>|<get-defaultClipboard>(){}[0]
|
|
||||||
final val androidx.compose.ui.platform/defaultClipboardManager // androidx.compose.ui.platform/defaultClipboardManager|{}defaultClipboardManager[0]
|
|
||||||
final fun <get-defaultClipboardManager>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/defaultClipboardManager.<get-defaultClipboardManager>|<get-defaultClipboardManager>(){}[0]
|
|
||||||
final val androidx.compose.ui.platform/platformClipboard // androidx.compose.ui.platform/platformClipboard|{}platformClipboard[0]
|
|
||||||
final fun <get-platformClipboard>(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/platformClipboard.<get-platformClipboard>|<get-platformClipboard>(){}[0]
|
|
||||||
final val androidx.compose.ui.platform/platformClipboardManager // androidx.compose.ui.platform/platformClipboardManager|{}platformClipboardManager[0]
|
|
||||||
final fun <get-platformClipboardManager>(): androidx.compose.ui.platform/ClipboardManager // androidx.compose.ui.platform/platformClipboardManager.<get-platformClipboardManager>|<get-platformClipboardManager>(){}[0]
|
|
||||||
final val androidx.compose.ui.platform/safeContent // androidx.compose.ui.platform/safeContent|@androidx.compose.ui.platform.PlatformWindowInsets{}safeContent[0]
|
final val androidx.compose.ui.platform/safeContent // androidx.compose.ui.platform/safeContent|@androidx.compose.ui.platform.PlatformWindowInsets{}safeContent[0]
|
||||||
final fun (androidx.compose.ui.platform/PlatformWindowInsets).<get-safeContent>(): androidx.compose.ui.platform/PlatformInsets // androidx.compose.ui.platform/safeContent.<get-safeContent>|<get-safeContent>@androidx.compose.ui.platform.PlatformWindowInsets(){}[0]
|
final fun (androidx.compose.ui.platform/PlatformWindowInsets).<get-safeContent>(): androidx.compose.ui.platform/PlatformInsets // androidx.compose.ui.platform/safeContent.<get-safeContent>|<get-safeContent>@androidx.compose.ui.platform.PlatformWindowInsets(){}[0]
|
||||||
final val androidx.compose.ui.platform/safeDrawing // androidx.compose.ui.platform/safeDrawing|@androidx.compose.ui.platform.PlatformWindowInsets{}safeDrawing[0]
|
final val androidx.compose.ui.platform/safeDrawing // androidx.compose.ui.platform/safeDrawing|@androidx.compose.ui.platform.PlatformWindowInsets{}safeDrawing[0]
|
||||||
@@ -9424,22 +9361,6 @@ final val com.compose.sdl.element/com_compose_sdl_element_ClipModifier$stablepro
|
|||||||
final val com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop|#static{}com_compose_sdl_element_ClipNode$stableprop[0]
|
final val com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop|#static{}com_compose_sdl_element_ClipNode$stableprop[0]
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop|#static{}com_compose_sdl_element_GraphicsLayerModifier$stableprop[0]
|
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop|#static{}com_compose_sdl_element_GraphicsLayerModifier$stableprop[0]
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop|#static{}com_compose_sdl_element_GraphicsLayerNode$stableprop[0]
|
final val com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop|#static{}com_compose_sdl_element_GraphicsLayerNode$stableprop[0]
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop|#static{}com_compose_sdl_element_MiddleClickModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop|#static{}com_compose_sdl_element_MiddleClickNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop|#static{}com_compose_sdl_element_OnDragModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop|#static{}com_compose_sdl_element_OnDragNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop|#static{}com_compose_sdl_element_OnPressedModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop|#static{}com_compose_sdl_element_OnPressedNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop|#static{}com_compose_sdl_element_OnTextInputModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop|#static{}com_compose_sdl_element_OnTextInputNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop|#static{}com_compose_sdl_element_PressableModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop|#static{}com_compose_sdl_element_PressableNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop|#static{}com_compose_sdl_element_SecondaryClickModifier$stableprop[0]
|
|
||||||
final val com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop|#static{}com_compose_sdl_element_SecondaryClickNode$stableprop[0]
|
|
||||||
final val com.compose.sdl.graphics/a8 // com.compose.sdl.graphics/a8|@androidx.compose.ui.graphics.Color{}a8[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).<get-a8>(): kotlin/Int // com.compose.sdl.graphics/a8.<get-a8>|<get-a8>@androidx.compose.ui.graphics.Color(){}[0]
|
|
||||||
final val com.compose.sdl.graphics/b8 // com.compose.sdl.graphics/b8|@androidx.compose.ui.graphics.Color{}b8[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).<get-b8>(): kotlin/Int // com.compose.sdl.graphics/b8.<get-b8>|<get-b8>@androidx.compose.ui.graphics.Color(){}[0]
|
|
||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop|#static{}com_compose_sdl_graphics_PathCommand_Close$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop|#static{}com_compose_sdl_graphics_PathCommand_Close$stableprop[0]
|
||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_CubicTo$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_CubicTo$stableprop[0]
|
||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_LineTo$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop|#static{}com_compose_sdl_graphics_PathCommand_LineTo$stableprop[0]
|
||||||
@@ -9450,8 +9371,6 @@ final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectImageBitmap$s
|
|||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop|#static{}com_compose_sdl_graphics_ProjectPaint$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop|#static{}com_compose_sdl_graphics_ProjectPaint$stableprop[0]
|
||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop|#static{}com_compose_sdl_graphics_ProjectPath$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop|#static{}com_compose_sdl_graphics_ProjectPath$stableprop[0]
|
||||||
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop|#static{}com_compose_sdl_graphics_ProjectPathIterator$stableprop[0]
|
final val com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop|#static{}com_compose_sdl_graphics_ProjectPathIterator$stableprop[0]
|
||||||
final val com.compose.sdl.graphics/g8 // com.compose.sdl.graphics/g8|@androidx.compose.ui.graphics.Color{}g8[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).<get-g8>(): kotlin/Int // com.compose.sdl.graphics/g8.<get-g8>|<get-g8>@androidx.compose.ui.graphics.Color(){}[0]
|
|
||||||
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.RadialGradient{}gradientCenter[0]
|
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.RadialGradient{}gradientCenter[0]
|
||||||
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientCenter>(): androidx.compose.ui.geometry/Offset // com.compose.sdl.graphics/gradientCenter.<get-gradientCenter>|<get-gradientCenter>@androidx.compose.ui.graphics.RadialGradient(){}[0]
|
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientCenter>(): androidx.compose.ui.geometry/Offset // com.compose.sdl.graphics/gradientCenter.<get-gradientCenter>|<get-gradientCenter>@androidx.compose.ui.graphics.RadialGradient(){}[0]
|
||||||
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.SweepGradient{}gradientCenter[0]
|
final val com.compose.sdl.graphics/gradientCenter // com.compose.sdl.graphics/gradientCenter|@androidx.compose.ui.graphics.SweepGradient{}gradientCenter[0]
|
||||||
@@ -9478,8 +9397,6 @@ final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/
|
|||||||
final fun (androidx.compose.ui.graphics/LinearGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.LinearGradient(){}[0]
|
final fun (androidx.compose.ui.graphics/LinearGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.LinearGradient(){}[0]
|
||||||
final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/gradientTileMode|@androidx.compose.ui.graphics.RadialGradient{}gradientTileMode[0]
|
final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/gradientTileMode|@androidx.compose.ui.graphics.RadialGradient{}gradientTileMode[0]
|
||||||
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.RadialGradient(){}[0]
|
final fun (androidx.compose.ui.graphics/RadialGradient).<get-gradientTileMode>(): androidx.compose.ui.graphics/TileMode // com.compose.sdl.graphics/gradientTileMode.<get-gradientTileMode>|<get-gradientTileMode>@androidx.compose.ui.graphics.RadialGradient(){}[0]
|
||||||
final val com.compose.sdl.graphics/r8 // com.compose.sdl.graphics/r8|@androidx.compose.ui.graphics.Color{}r8[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).<get-r8>(): kotlin/Int // com.compose.sdl.graphics/r8.<get-r8>|<get-r8>@androidx.compose.ui.graphics.Color(){}[0]
|
|
||||||
final val com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop|#static{}com_compose_sdl_icons_IconFont$stableprop[0]
|
final val com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop|#static{}com_compose_sdl_icons_IconFont$stableprop[0]
|
||||||
final val com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop|#static{}com_compose_sdl_input_LegacyPointerEvent$stableprop[0]
|
final val com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop|#static{}com_compose_sdl_input_LegacyPointerEvent$stableprop[0]
|
||||||
final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0]
|
final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0]
|
||||||
@@ -9512,12 +9429,14 @@ final val com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stablepro
|
|||||||
final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0]
|
final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0]
|
||||||
final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0]
|
final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent$stableprop // com.compose.sdl/com_compose_sdl_AppEvent$stableprop|#static{}com_compose_sdl_AppEvent$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent$stableprop // com.compose.sdl/com_compose_sdl_AppEvent$stableprop|#static{}com_compose_sdl_AppEvent$stableprop[0]
|
||||||
|
final val com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop|#static{}com_compose_sdl_AppEvent_Drop$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop|#static{}com_compose_sdl_AppEvent_Key$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop|#static{}com_compose_sdl_AppEvent_Key$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop|#static{}com_compose_sdl_AppEvent_MouseWheel$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop|#static{}com_compose_sdl_AppEvent_MouseWheel$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop|#static{}com_compose_sdl_AppEvent_Pointer$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop|#static{}com_compose_sdl_AppEvent_Pointer$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop|#static{}com_compose_sdl_AppEvent_Quit$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop|#static{}com_compose_sdl_AppEvent_Quit$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop|#static{}com_compose_sdl_AppEvent_RedrawNeeded$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop|#static{}com_compose_sdl_AppEvent_RedrawNeeded$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop|#static{}com_compose_sdl_AppEvent_TextInput$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop|#static{}com_compose_sdl_AppEvent_TextInput$stableprop[0]
|
||||||
|
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop|#static{}com_compose_sdl_AppEvent_WindowActivation$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop|#static{}com_compose_sdl_AppEvent_WindowClose$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop|#static{}com_compose_sdl_AppEvent_WindowClose$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop|#static{}com_compose_sdl_AppEvent_WindowResized$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop|#static{}com_compose_sdl_AppEvent_WindowResized$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0]
|
||||||
@@ -9655,6 +9574,8 @@ final var androidx.compose.ui/isDialogAnimationEnabled // androidx.compose.ui/is
|
|||||||
final var androidx.compose.ui/useLegacyRenderNodeLayers // androidx.compose.ui/useLegacyRenderNodeLayers|@androidx.compose.ui.ComposeUiFlags{}useLegacyRenderNodeLayers[0]
|
final var androidx.compose.ui/useLegacyRenderNodeLayers // androidx.compose.ui/useLegacyRenderNodeLayers|@androidx.compose.ui.ComposeUiFlags{}useLegacyRenderNodeLayers[0]
|
||||||
final fun (androidx.compose.ui/ComposeUiFlags).<get-useLegacyRenderNodeLayers>(): kotlin/Boolean // androidx.compose.ui/useLegacyRenderNodeLayers.<get-useLegacyRenderNodeLayers>|<get-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(){}[0]
|
final fun (androidx.compose.ui/ComposeUiFlags).<get-useLegacyRenderNodeLayers>(): kotlin/Boolean // androidx.compose.ui/useLegacyRenderNodeLayers.<get-useLegacyRenderNodeLayers>|<get-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(){}[0]
|
||||||
final fun (androidx.compose.ui/ComposeUiFlags).<set-useLegacyRenderNodeLayers>(kotlin/Boolean) // androidx.compose.ui/useLegacyRenderNodeLayers.<set-useLegacyRenderNodeLayers>|<set-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(kotlin.Boolean){}[0]
|
final fun (androidx.compose.ui/ComposeUiFlags).<set-useLegacyRenderNodeLayers>(kotlin/Boolean) // androidx.compose.ui/useLegacyRenderNodeLayers.<set-useLegacyRenderNodeLayers>|<set-useLegacyRenderNodeLayers>@androidx.compose.ui.ComposeUiFlags(kotlin.Boolean){}[0]
|
||||||
|
final var com.compose.sdl.graphics/encodedImageDecoder // com.compose.sdl.graphics/encodedImageDecoder|{}encodedImageDecoder[0]
|
||||||
|
final fun <get-encodedImageDecoder>(): com.compose.sdl.graphics/EncodedImageDecoder? // com.compose.sdl.graphics/encodedImageDecoder.<get-encodedImageDecoder>|<get-encodedImageDecoder>(){}[0]
|
||||||
final var com.compose.sdl.res/currentImageLoader // com.compose.sdl.res/currentImageLoader|{}currentImageLoader[0]
|
final var com.compose.sdl.res/currentImageLoader // com.compose.sdl.res/currentImageLoader|{}currentImageLoader[0]
|
||||||
final fun <get-currentImageLoader>(): com.compose.sdl.res/ImageLoader // com.compose.sdl.res/currentImageLoader.<get-currentImageLoader>|<get-currentImageLoader>(){}[0]
|
final fun <get-currentImageLoader>(): com.compose.sdl.res/ImageLoader // com.compose.sdl.res/currentImageLoader.<get-currentImageLoader>|<get-currentImageLoader>(){}[0]
|
||||||
final fun <set-currentImageLoader>(com.compose.sdl.res/ImageLoader) // com.compose.sdl.res/currentImageLoader.<set-currentImageLoader>|<set-currentImageLoader>(com.compose.sdl.res.ImageLoader){}[0]
|
final fun <set-currentImageLoader>(com.compose.sdl.res/ImageLoader) // com.compose.sdl.res/currentImageLoader.<set-currentImageLoader>|<set-currentImageLoader>(com.compose.sdl.res.ImageLoader){}[0]
|
||||||
@@ -9703,9 +9624,6 @@ final fun (androidx.compose.ui.graphics/Canvas).androidx.compose.ui.graphics/sca
|
|||||||
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/compositeOver(androidx.compose.ui.graphics/Color): androidx.compose.ui.graphics/Color // androidx.compose.ui.graphics/compositeOver|compositeOver@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color){}[0]
|
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/compositeOver(androidx.compose.ui.graphics/Color): androidx.compose.ui.graphics/Color // androidx.compose.ui.graphics/compositeOver|compositeOver@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color){}[0]
|
||||||
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/luminance(): kotlin/Float // androidx.compose.ui.graphics/luminance|luminance@androidx.compose.ui.graphics.Color(){}[0]
|
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/luminance(): kotlin/Float // androidx.compose.ui.graphics/luminance|luminance@androidx.compose.ui.graphics.Color(){}[0]
|
||||||
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/toArgb(): kotlin/Int // androidx.compose.ui.graphics/toArgb|toArgb@androidx.compose.ui.graphics.Color(){}[0]
|
final fun (androidx.compose.ui.graphics/Color).androidx.compose.ui.graphics/toArgb(): kotlin/Int // androidx.compose.ui.graphics/toArgb|toArgb@androidx.compose.ui.graphics.Color(){}[0]
|
||||||
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/blend(androidx.compose.ui.graphics/Color, kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/blend|blend@androidx.compose.ui.graphics.Color(androidx.compose.ui.graphics.Color;kotlin.Float){}[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/darken(kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/darken|darken@androidx.compose.ui.graphics.Color(kotlin.Float){}[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/Color).com.compose.sdl.graphics/lighten(kotlin/Float): androidx.compose.ui.graphics/Color // com.compose.sdl.graphics/lighten|lighten@androidx.compose.ui.graphics.Color(kotlin.Float){}[0]
|
|
||||||
final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/toPixelMap(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/IntArray = ..., kotlin/Int = ..., kotlin/Int = ...): androidx.compose.ui.graphics/PixelMap // androidx.compose.ui.graphics/toPixelMap|toPixelMap@androidx.compose.ui.graphics.ImageBitmap(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.IntArray;kotlin.Int;kotlin.Int){}[0]
|
final fun (androidx.compose.ui.graphics/ImageBitmap).androidx.compose.ui.graphics/toPixelMap(kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/IntArray = ..., kotlin/Int = ..., kotlin/Int = ...): androidx.compose.ui.graphics/PixelMap // androidx.compose.ui.graphics/toPixelMap|toPixelMap@androidx.compose.ui.graphics.ImageBitmap(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.IntArray;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun (androidx.compose.ui.graphics/Matrix).androidx.compose.ui.graphics/isIdentity(): kotlin/Boolean // androidx.compose.ui.graphics/isIdentity|isIdentity@androidx.compose.ui.graphics.Matrix(){}[0]
|
final fun (androidx.compose.ui.graphics/Matrix).androidx.compose.ui.graphics/isIdentity(): kotlin/Boolean // androidx.compose.ui.graphics/isIdentity|isIdentity@androidx.compose.ui.graphics.Matrix(){}[0]
|
||||||
final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addOutline(androidx.compose.ui.graphics/Outline) // androidx.compose.ui.graphics/addOutline|addOutline@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Outline){}[0]
|
final fun (androidx.compose.ui.graphics/Path).androidx.compose.ui.graphics/addOutline(androidx.compose.ui.graphics/Outline) // androidx.compose.ui.graphics/addOutline|addOutline@androidx.compose.ui.graphics.Path(androidx.compose.ui.graphics.Outline){}[0]
|
||||||
@@ -9908,12 +9826,6 @@ final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(
|
|||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/preferredFrameRate|preferredFrameRate@androidx.compose.ui.Modifier(kotlin.Float){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/preferredFrameRate(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/preferredFrameRate|preferredFrameRate@androidx.compose.ui.Modifier(kotlin.Float){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/sensitiveContent(kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.ui/sensitiveContent|sensitiveContent@androidx.compose.ui.Modifier(kotlin.Boolean){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/sensitiveContent(kotlin/Boolean = ...): androidx.compose.ui/Modifier // androidx.compose.ui/sensitiveContent|sensitiveContent@androidx.compose.ui.Modifier(kotlin.Boolean){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/zIndex(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/zIndex|zIndex@androidx.compose.ui.Modifier(kotlin.Float){}[0]
|
final fun (androidx.compose.ui/Modifier).androidx.compose.ui/zIndex(kotlin/Float): androidx.compose.ui/Modifier // androidx.compose.ui/zIndex|zIndex@androidx.compose.ui.Modifier(kotlin.Float){}[0]
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onDrag(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> = ..., kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit> = ..., kotlin/Function0<kotlin/Unit> = ...): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onDrag|onDrag@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>;kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onMiddleClick(kotlin/Function0<kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onMiddleClick|onMiddleClick@androidx.compose.ui.Modifier(kotlin.Function0<kotlin.Unit>){}[0]
|
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onPressed(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onPressed|onPressed@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onSecondaryClick(kotlin/Function2<kotlin/Int, kotlin/Int, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onSecondaryClick|onSecondaryClick@androidx.compose.ui.Modifier(kotlin.Function2<kotlin.Int,kotlin.Int,kotlin.Unit>){}[0]
|
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/onTextInput(kotlin/Function1<kotlin/String, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/onTextInput|onTextInput@androidx.compose.ui.Modifier(kotlin.Function1<kotlin.String,kotlin.Unit>){}[0]
|
|
||||||
final fun (androidx.compose.ui/Modifier).com.compose.sdl.modifier/pressable(kotlin/Function1<kotlin/Boolean, kotlin/Unit>): androidx.compose.ui/Modifier // com.compose.sdl.modifier/pressable|pressable@androidx.compose.ui.Modifier(kotlin.Function1<kotlin.Boolean,kotlin.Unit>){}[0]
|
|
||||||
final fun (kotlin.collections/List<androidx.compose.ui.graphics.vector/PathNode>).androidx.compose.ui.graphics.vector/toPath(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics.vector/toPath|toPath@kotlin.collections.List<androidx.compose.ui.graphics.vector.PathNode>(androidx.compose.ui.graphics.Path){}[0]
|
final fun (kotlin.collections/List<androidx.compose.ui.graphics.vector/PathNode>).androidx.compose.ui.graphics.vector/toPath(androidx.compose.ui.graphics/Path = ...): androidx.compose.ui.graphics/Path // androidx.compose.ui.graphics.vector/toPath|toPath@kotlin.collections.List<androidx.compose.ui.graphics.vector.PathNode>(androidx.compose.ui.graphics.Path){}[0]
|
||||||
final fun (kotlin/ByteArray).androidx.compose.ui.graphics/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0]
|
final fun (kotlin/ByteArray).androidx.compose.ui.graphics/decodeToImageBitmap(): androidx.compose.ui.graphics/ImageBitmap // androidx.compose.ui.graphics/decodeToImageBitmap|decodeToImageBitmap@kotlin.ByteArray(){}[0]
|
||||||
final fun (kotlin/CharSequence).androidx.compose.ui.text/substring(androidx.compose.ui.text/TextRange): kotlin/String // androidx.compose.ui.text/substring|substring@kotlin.CharSequence(androidx.compose.ui.text.TextRange){}[0]
|
final fun (kotlin/CharSequence).androidx.compose.ui.text/substring(androidx.compose.ui.text/TextRange): kotlin/String // androidx.compose.ui.text/substring|substring@kotlin.CharSequence(androidx.compose.ui.text.TextRange){}[0]
|
||||||
@@ -10168,6 +10080,7 @@ final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElement
|
|||||||
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0]
|
final fun androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(): kotlin/Int // androidx.compose.ui.platform/androidx_compose_ui_platform_ValueElementSequence$stableprop_getter|androidx_compose_ui_platform_ValueElementSequence$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner|findComposeDefaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // androidx.compose.ui.platform/findComposeDefaultViewModelStoreOwner|findComposeDefaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.navigationevent/NavigationEventDispatcherOwner? // androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner|findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.navigationevent/NavigationEventDispatcherOwner? // androidx.compose.ui.platform/findDefaultNavigationEventDispatcherOwner|findDefaultNavigationEventDispatcherOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.ui.platform/platformClipboard(): androidx.compose.ui.platform/Clipboard // androidx.compose.ui.platform/platformClipboard|platformClipboard(){}[0]
|
||||||
final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter|androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(){}[0]
|
final fun androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(): kotlin/Int // androidx.compose.ui.scene/androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter|androidx_compose_ui_scene_ComposeScenePointer$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter|androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(){}[0]
|
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter|androidx_compose_ui_semantics_AccessibilityAction$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter|androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(){}[0]
|
final fun androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(): kotlin/Int // androidx.compose.ui.semantics/androidx_compose_ui_semantics_CollectionInfo$stableprop_getter|androidx_compose_ui_semantics_CollectionInfo$stableprop_getter(){}[0]
|
||||||
@@ -10303,18 +10216,6 @@ final fun com.compose.sdl.element/com_compose_sdl_element_ClipModifier$stablepro
|
|||||||
final fun com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter|com_compose_sdl_element_ClipNode$stableprop_getter(){}[0]
|
final fun com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_ClipNode$stableprop_getter|com_compose_sdl_element_ClipNode$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter|com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(){}[0]
|
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter|com_compose_sdl_element_GraphicsLayerModifier$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter|com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(){}[0]
|
final fun com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_GraphicsLayerNode$stableprop_getter|com_compose_sdl_element_GraphicsLayerNode$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_MiddleClickModifier$stableprop_getter|com_compose_sdl_element_MiddleClickModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_MiddleClickNode$stableprop_getter|com_compose_sdl_element_MiddleClickNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnDragModifier$stableprop_getter|com_compose_sdl_element_OnDragModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnDragNode$stableprop_getter|com_compose_sdl_element_OnDragNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnPressedModifier$stableprop_getter|com_compose_sdl_element_OnPressedModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnPressedNode$stableprop_getter|com_compose_sdl_element_OnPressedNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnTextInputModifier$stableprop_getter|com_compose_sdl_element_OnTextInputModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_OnTextInputNode$stableprop_getter|com_compose_sdl_element_OnTextInputNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_PressableModifier$stableprop_getter|com_compose_sdl_element_PressableModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_PressableNode$stableprop_getter|com_compose_sdl_element_PressableNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickModifier$stableprop_getter|com_compose_sdl_element_SecondaryClickModifier$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop_getter(): kotlin/Int // com.compose.sdl.element/com_compose_sdl_element_SecondaryClickNode$stableprop_getter|com_compose_sdl_element_SecondaryClickNode$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter|com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_Close$stableprop_getter|com_compose_sdl_graphics_PathCommand_Close$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_CubicTo$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter|com_compose_sdl_graphics_PathCommand_LineTo$stableprop_getter(){}[0]
|
||||||
@@ -10325,6 +10226,7 @@ final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectImageBitmap$s
|
|||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter|com_compose_sdl_graphics_ProjectPaint$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPaint$stableprop_getter|com_compose_sdl_graphics_ProjectPaint$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter|com_compose_sdl_graphics_ProjectPath$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPath$stableprop_getter|com_compose_sdl_graphics_ProjectPath$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter|com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(){}[0]
|
final fun com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(): kotlin/Int // com.compose.sdl.graphics/com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter|com_compose_sdl_graphics_ProjectPathIterator$stableprop_getter(){}[0]
|
||||||
|
final fun com.compose.sdl.graphics/decodeEncodedImageBitmap(kotlin/ByteArray): androidx.compose.ui.graphics/ImageBitmap? // com.compose.sdl.graphics/decodeEncodedImageBitmap|decodeEncodedImageBitmap(kotlin.ByteArray){}[0]
|
||||||
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter|com_compose_sdl_icons_IconFont$stableprop_getter(){}[0]
|
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconFont$stableprop_getter|com_compose_sdl_icons_IconFont$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(): kotlin/Int // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter|com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(){}[0]
|
final fun com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(): kotlin/Int // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter|com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0]
|
final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0]
|
||||||
@@ -10358,12 +10260,14 @@ final fun com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stablepro
|
|||||||
final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0]
|
final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0]
|
||||||
final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0]
|
final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter|com_compose_sdl_AppEvent$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent$stableprop_getter|com_compose_sdl_AppEvent$stableprop_getter(){}[0]
|
||||||
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Drop$stableprop_getter|com_compose_sdl_AppEvent_Drop$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter|com_compose_sdl_AppEvent_Key$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Key$stableprop_getter|com_compose_sdl_AppEvent_Key$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter|com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_MouseWheel$stableprop_getter|com_compose_sdl_AppEvent_MouseWheel$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter|com_compose_sdl_AppEvent_Pointer$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Pointer$stableprop_getter|com_compose_sdl_AppEvent_Pointer$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter|com_compose_sdl_AppEvent_Quit$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_Quit$stableprop_getter|com_compose_sdl_AppEvent_Quit$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter|com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter|com_compose_sdl_AppEvent_RedrawNeeded$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter|com_compose_sdl_AppEvent_TextInput$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_TextInput$stableprop_getter|com_compose_sdl_AppEvent_TextInput$stableprop_getter(){}[0]
|
||||||
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowActivation$stableprop_getter|com_compose_sdl_AppEvent_WindowActivation$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter|com_compose_sdl_AppEvent_WindowClose$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowClose$stableprop_getter|com_compose_sdl_AppEvent_WindowClose$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter|com_compose_sdl_AppEvent_WindowResized$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_getter|com_compose_sdl_AppEvent_WindowResized$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ plugins {
|
|||||||
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
|
val useSdl3Everywhere = (findProperty("renderer") as? String) == "sdl3"
|
||||||
|
|
||||||
// All targets pull headers from the in-repo static build tree at <repo>/libs,
|
// All targets pull headers from the in-repo static build tree at <repo>/libs,
|
||||||
// populated by scripts/build-sdl/build-all.sh (or the per-library scripts).
|
// populated by scripts/build-sdl/build-all.py (optionally one library at a time).
|
||||||
// Same paths on macOS / Linux / Windows — the .def files are pathless and
|
// Same paths on macOS / Linux / Windows — the .def files are pathless and
|
||||||
// rely on these -I injections. Cross-target cinterop indexing under
|
// rely on these -I injections. Cross-target cinterop indexing under
|
||||||
// kotlin.mpp.enableCInteropCommonization is fine: clang silently ignores -I
|
// kotlin.mpp.enableCInteropCommonization is fine: clang silently ignores -I
|
||||||
@@ -156,9 +156,9 @@ kotlin {
|
|||||||
api(project(":ui-geometry"))
|
api(project(":ui-geometry"))
|
||||||
api(project(":ui-unit"))
|
api(project(":ui-unit"))
|
||||||
api(project(":ui-backhandler"))
|
api(project(":ui-backhandler"))
|
||||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
api("org.jetbrains.compose.runtime:runtime-saveable:1.11.1")
|
api("org.jetbrains.compose.runtime:runtime-saveable:${libs.versions.composeRuntime.get()}")
|
||||||
api("androidx.compose.runtime:runtime-retain:1.11.1")
|
api("androidx.compose.runtime:runtime-retain:${libs.versions.composeRuntime.get()}")
|
||||||
api("androidx.navigationevent:navigationevent-compose:1.1.2")
|
api("androidx.navigationevent:navigationevent-compose:1.1.2")
|
||||||
api("androidx.savedstate:savedstate:1.5.0")
|
api("androidx.savedstate:savedstate:1.5.0")
|
||||||
api("androidx.savedstate:savedstate-compose:1.5.0")
|
api("androidx.savedstate:savedstate-compose:1.5.0")
|
||||||
|
|||||||
@@ -514,14 +514,14 @@ skikoMain/kotlin/ -> src/vendor/skikoRenderer/kotlin/
|
|||||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt
|
# | skikoMain/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/SkiaVertexMode.skiko.kt
|
||||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt
|
# | skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt
|
||||||
# | skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt
|
# | skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt -> src/vendor/skikoRenderer/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/ImageBitmap.skiko.kt
|
# B6.1 (2026-07-16): un-refused — skiko leg adopts upstream graphics actuals.
|
||||||
|
# SkiaBackedCanvas is then MANUAL-VENDORED (moved to src/skikoRendererMain, re-refused
|
||||||
|
# below) to also implement the port Native* draw contracts. See RENDERER_TASKS.md B6.1.
|
||||||
|
# B6.2: un-refused — skiko leg adopts upstream GraphicsLayer/GraphicsContext (real
|
||||||
|
# skiko RenderNode) now the canvas is SkiaBackedCanvas. See RENDERER_TASKS.md B6.2.
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
|
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
|
# B6.1: MANUAL-VENDORED (flattened toBitmap/putBytesInto expect-actuals — see the file).
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaGraphicsContext.skiko.kt
|
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
|
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaImageAsset.skiko.kt
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/SkiaShader.skiko.kt
|
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt
|
|
||||||
!skikoMain/kotlin/androidx/compose/ui/graphics/shadow/Blur.skiko.kt
|
|
||||||
|
|
||||||
SET_FOLDER=compose/ui/ui-text/src
|
SET_FOLDER=compose/ui/ui-text/src
|
||||||
|
|
||||||
@@ -757,11 +757,13 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
|
|||||||
# | androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
|
# | androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
|
||||||
# | androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
|
# | androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/AndroidPopup.android.kt
|
||||||
# | androidMain/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt
|
# | androidMain/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt -> src/vendor/android/kotlin/androidx/compose/ui/window/SecureFlagPolicy.android.kt
|
||||||
# desktopMain (173)
|
# desktopMain (175)
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/Actuals.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/Actuals.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/Actuals.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/Actuals.desktop.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt
|
# | desktopMain/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeDesktopEntryPoint.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ComposeFeatureFlags.desktop.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ConfigureSwingGlobalsForCompose.desktop.kt
|
||||||
|
# | desktopMain/kotlin/androidx/compose/ui/DesktopComposeUiFlags.skiko.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/DesktopComposeUiFlags.skiko.kt
|
||||||
|
# | desktopMain/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/ProvideSystemTheme.desktop.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtDimension.desktop.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEventFilter.desktop.kt
|
||||||
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt
|
# | desktopMain/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt -> src/vendor/desktop/kotlin/androidx/compose/ui/awt/AwtEvents.desktop.kt
|
||||||
@@ -1092,7 +1094,7 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
|
|||||||
# | wasmJsMain/kotlin/androidx/compose/ui/Actuals.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/Actuals.wasm.kt
|
# | wasmJsMain/kotlin/androidx/compose/ui/Actuals.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/Actuals.wasm.kt
|
||||||
# | wasmJsMain/kotlin/androidx/compose/ui/internal/System.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/internal/System.wasm.kt
|
# | wasmJsMain/kotlin/androidx/compose/ui/internal/System.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/internal/System.wasm.kt
|
||||||
# | wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt
|
# | wasmJsMain/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt -> src/vendor/wasmJs/kotlin/androidx/compose/ui/platform/PlatformClipboard.wasm.kt
|
||||||
# webMain (39)
|
# webMain (40)
|
||||||
# | webMain/kotlin/androidx/compose/ui/dom/Events.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/dom/Events.web.kt
|
# | webMain/kotlin/androidx/compose/ui/dom/Events.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/dom/Events.web.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt
|
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDrop.web.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt
|
# | webMain/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/draganddrop/DragAndDropTransferData.web.kt
|
||||||
@@ -1129,6 +1131,7 @@ skikoMain/kotlin/androidx/compose/ui/text/style/TextMotion.skiko.kt -> src/vendo
|
|||||||
# | webMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt
|
# | webMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt
|
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindow.web.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt
|
# | webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt
|
||||||
|
# | webMain/kotlin/androidx/compose/ui/window/RootScrollObserver.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/RootScrollObserver.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt
|
# | webMain/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/SystemThemeObserver.web.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt
|
# | webMain/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebCompositionLocals.kt
|
||||||
# | webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebTextInputSession.kt
|
# | webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt -> src/vendor/web/kotlin/androidx/compose/ui/window/WebTextInputSession.kt
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
* Line-for-line kept in sync; the entry in compose/ui/compose-fork.txt is
|
* Line-for-line kept in sync; the entry in compose/ui/compose-fork.txt is
|
||||||
* commented out so this file survives `scripts/compose-fork/sync.sh`.
|
* commented out so this file survives `scripts/compose-fork/sync.sh`.
|
||||||
*/
|
*/
|
||||||
|
// VENDOR-BASE: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Synchronization.kt @ v1.12.0-beta03+dev4483
|
||||||
|
|
||||||
package androidx.compose.ui.platform
|
package androidx.compose.ui.platform
|
||||||
|
|
||||||
|
|||||||
+1
@@ -6,6 +6,7 @@
|
|||||||
* compose/ui/compose-fork.txt is commented out so this file survives
|
* compose/ui/compose-fork.txt is commented out so this file survives
|
||||||
* `scripts/compose-fork/sync.sh`.
|
* `scripts/compose-fork/sync.sh`.
|
||||||
*/
|
*/
|
||||||
|
// VENDOR-BASE: compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/platform/Synchronization.kt @ v1.12.0-beta03+dev4483
|
||||||
|
|
||||||
package androidx.compose.ui.text.platform
|
package androidx.compose.ui.text.platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
package com.compose.sdl.element
|
|
||||||
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
|
||||||
import androidx.compose.ui.node.ModifierNodeElement
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: Project modifier elements
|
|
||||||
// ==================
|
|
||||||
// Small set of project-only modifier elements — each pairs a
|
|
||||||
// `ModifierNodeElement<XxxNode>` with a `XxxNode : Modifier.Node`. Upstream
|
|
||||||
// LayoutNode reads them through the chain (ModifierNodeElement IS-A
|
|
||||||
// Modifier.Element). Everything gesture-related has been removed — pointer
|
|
||||||
// input, text input, right/middle click, drag: all go through the standard
|
|
||||||
// `Modifier.pointerInput` + `detectTapGestures` / `detectDragGestures` /
|
|
||||||
// `awaitPointerEventScope` APIs upstream now supplies. What's left is the
|
|
||||||
// two elements the renderer still owns directly:
|
|
||||||
// * ClipModifier — GraphicsLayer.kt lowers `clip = true` to it.
|
|
||||||
// * GraphicsLayerModifier — the transform / alpha / cache pipeline.
|
|
||||||
|
|
||||||
class ClipModifier(val shape: Shape) : ModifierNodeElement<ClipNode>() {
|
|
||||||
override fun create() = ClipNode(shape)
|
|
||||||
override fun update(node: ClipNode) { node.shape = shape }
|
|
||||||
override fun hashCode(): Int = shape.hashCode()
|
|
||||||
override fun equals(other: Any?): Boolean = other is ClipModifier && other.shape == shape
|
|
||||||
}
|
|
||||||
class ClipNode(var shape: Shape) : Modifier.Node()
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: GraphicsLayerModifier
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A "graphics layer" element: alpha + 2D transform (scale / rotation /
|
|
||||||
* translation), with an optional cacheKey that opts the subtree into
|
|
||||||
* render-to-texture caching across frames. See `Modifier.graphicsLayer`
|
|
||||||
* (in `androidx.compose.ui.graphics`) for the caching semantics.
|
|
||||||
*
|
|
||||||
* The renderer reads this element directly via the `LayoutNode.graphicsLayer`
|
|
||||||
* `foldIn` over the chain; the paired [GraphicsLayerNode] lifecycle stays
|
|
||||||
* dormant until the renderer rewrite drives it.
|
|
||||||
*/
|
|
||||||
class GraphicsLayerModifier(
|
|
||||||
val alpha: Float = 1f,
|
|
||||||
val scaleX: Float = 1f,
|
|
||||||
val scaleY: Float = 1f,
|
|
||||||
val rotationZ: Float = 0f,
|
|
||||||
val translationX: Float = 0f,
|
|
||||||
val translationY: Float = 0f,
|
|
||||||
val transformOrigin: TransformOrigin = TransformOrigin.Center,
|
|
||||||
val cacheKey: Any? = null,
|
|
||||||
) : ModifierNodeElement<GraphicsLayerNode>() {
|
|
||||||
|
|
||||||
val needsLayer: Boolean
|
|
||||||
get() = alpha < 1f || cacheKey != null
|
|
||||||
|
|
||||||
val needsTransform: Boolean
|
|
||||||
get() = scaleX != 1f || scaleY != 1f || rotationZ != 0f ||
|
|
||||||
translationX != 0f || translationY != 0f
|
|
||||||
|
|
||||||
val isIdentity: Boolean
|
|
||||||
get() = !needsLayer && !needsTransform
|
|
||||||
|
|
||||||
override fun create() =
|
|
||||||
GraphicsLayerNode(alpha, scaleX, scaleY, rotationZ, translationX, translationY, transformOrigin, cacheKey)
|
|
||||||
override fun update(node: GraphicsLayerNode) {
|
|
||||||
node.alpha = alpha; node.scaleX = scaleX; node.scaleY = scaleY
|
|
||||||
node.rotationZ = rotationZ; node.translationX = translationX; node.translationY = translationY
|
|
||||||
node.transformOrigin = transformOrigin; node.cacheKey = cacheKey
|
|
||||||
}
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var v = alpha.hashCode()
|
|
||||||
v = 31 * v + scaleX.hashCode(); v = 31 * v + scaleY.hashCode()
|
|
||||||
v = 31 * v + rotationZ.hashCode()
|
|
||||||
v = 31 * v + translationX.hashCode(); v = 31 * v + translationY.hashCode()
|
|
||||||
v = 31 * v + transformOrigin.hashCode(); v = 31 * v + (cacheKey?.hashCode() ?: 0)
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
override fun equals(other: Any?): Boolean =
|
|
||||||
other is GraphicsLayerModifier &&
|
|
||||||
other.alpha == alpha && other.scaleX == scaleX && other.scaleY == scaleY &&
|
|
||||||
other.rotationZ == rotationZ &&
|
|
||||||
other.translationX == translationX && other.translationY == translationY &&
|
|
||||||
other.transformOrigin == transformOrigin && other.cacheKey == cacheKey
|
|
||||||
}
|
|
||||||
|
|
||||||
class GraphicsLayerNode(
|
|
||||||
var alpha: Float,
|
|
||||||
var scaleX: Float,
|
|
||||||
var scaleY: Float,
|
|
||||||
var rotationZ: Float,
|
|
||||||
var translationX: Float,
|
|
||||||
var translationY: Float,
|
|
||||||
var transformOrigin: TransformOrigin,
|
|
||||||
var cacheKey: Any?,
|
|
||||||
) : Modifier.Node()
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.compose.sdl.graphics
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: GraphicsLayer factory (project bridge)
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
/* The vendored `expect class GraphicsLayer` exposes neither a constructor nor
|
|
||||||
the isReleased setter to common code — upstream owners create layers from
|
|
||||||
platform source sets. ComposeOwner lives in commonMain, so these two hops
|
|
||||||
let its GraphicsContext create and release the project record/replay
|
|
||||||
actual (GraphicsLayer.native.kt). */
|
|
||||||
|
|
||||||
internal expect fun createProjectGraphicsLayer(): GraphicsLayer
|
|
||||||
|
|
||||||
internal expect fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer)
|
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: NativeFinishableCanvas
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/*
|
||||||
|
A renderer canvas that must be flushed once drawing into it is done — SDL batches
|
||||||
|
geometry and only submits on flush; an offscreen SDL canvas must flush before its
|
||||||
|
backing texture is read. Implemented by Sdl3Canvas (real flush) and SkiaCanvas
|
||||||
|
(no-op — Skia save/restore is balanced per call). Lets renderer-agnostic code
|
||||||
|
(e.g. GraphicsLayer.toImageBitmap) commit an offscreen render without knowing the
|
||||||
|
concrete canvas type.
|
||||||
|
*/
|
||||||
|
interface NativeFinishableCanvas {
|
||||||
|
fun finish()
|
||||||
|
}
|
||||||
@@ -44,6 +44,14 @@ interface NativeTextCanvas {
|
|||||||
// (underline / line-through). Per-run overrides ride in via inSpans.
|
// (underline / line-through). Per-run overrides ride in via inSpans.
|
||||||
inBaseItalic: Boolean = false,
|
inBaseItalic: Boolean = false,
|
||||||
inTextDecoration: TextDecoration? = null,
|
inTextDecoration: TextDecoration? = null,
|
||||||
|
// The paragraph's per-line band height in px (TextStyle.lineHeight when the
|
||||||
|
// style specifies one — SdlParagraph passes it so glyph rows stack exactly
|
||||||
|
// where layout put the line boxes). <= 0 → derive from font metrics (legacy
|
||||||
|
// behaviour; icon callers and TextDrawNode don't pass it).
|
||||||
|
inLineHeightPx: Float = 0f,
|
||||||
|
// Compat-trim (raw styles): the FIRST line keeps the tight font cell. False
|
||||||
|
// for M3's LineHeightStyle(Trim.None) — every line is the full band.
|
||||||
|
inTrimFirstLine: Boolean = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ headerFilter = SDL3/**
|
|||||||
# archive location comes from :ui build.gradle.kts (-libraryPath) because
|
# archive location comes from :ui build.gradle.kts (-libraryPath) because
|
||||||
# .def files can't reference $rootDir. The linker options below list the
|
# .def files can't reference $rootDir. The linker options below list the
|
||||||
# system frameworks / libs SDL3's static build depends on (see the Libs
|
# system frameworks / libs SDL3's static build depends on (see the Libs
|
||||||
# line in libs/SDL3/lib/pkgconfig/sdl3.pc after build-sdl3.sh runs) —
|
# line in libs/SDL3/lib/pkgconfig/sdl3.pc after build-all.py runs) —
|
||||||
# cinterop propagates them through the klib to the final app link.
|
# cinterop propagates them through the klib to the final app link.
|
||||||
|
|
||||||
staticLibraries = libSDL3.a
|
staticLibraries = libSDL3.a
|
||||||
@@ -20,8 +20,13 @@ linkerOpts.osx = \
|
|||||||
-framework QuartzCore -weak_framework CoreHaptics \
|
-framework QuartzCore -weak_framework CoreHaptics \
|
||||||
-lpthread -lm
|
-lpthread -lm
|
||||||
|
|
||||||
linkerOpts.linux_x64 = -lpthread -ldl -lm -lrt
|
# The -L multi-arch dirs + fontconfig/GL/X11 serve the SKIA renderer (font
|
||||||
linkerOpts.linux_arm64 = -lpthread -ldl -lm -lrt
|
# matching, GL backend, windowing) — K/N's LLD sysroot doesn't include the
|
||||||
|
# host's multi-arch dir, and carrying them here means apps (including bridge
|
||||||
|
# consumers) need NO linker flags of their own. Unused under -Prenderer=sdl3;
|
||||||
|
# the extra -l is harmless, and LLD skips missing -L dirs.
|
||||||
|
linkerOpts.linux_x64 = -L/usr/lib/x86_64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
|
||||||
|
linkerOpts.linux_arm64 = -L/usr/lib/aarch64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
|
||||||
|
|
||||||
linkerOpts.mingw_x64 = \
|
linkerOpts.mingw_x64 = \
|
||||||
-lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 \
|
-lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 \
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ depends = sdl3
|
|||||||
# libSDL3_image.a + its vendored codec archives (PNG/JPG built-in via stb,
|
# libSDL3_image.a + its vendored codec archives (PNG/JPG built-in via stb,
|
||||||
# WEBP, SVG built-in) are all baked into the cinterop klib. See sdl3.def —
|
# WEBP, SVG built-in) are all baked into the cinterop klib. See sdl3.def —
|
||||||
# the static archive location is injected via :ui build.gradle.kts
|
# the static archive location is injected via :ui build.gradle.kts
|
||||||
# (-libraryPath). build-sdl3-image.sh normalises libzlibstatic.a → libz.a
|
# (-libraryPath). build-all.py's sdl3-image step normalises libzlibstatic.a
|
||||||
# on Windows so the same filename works everywhere.
|
# → libz.a on Windows so the same filename works everywhere.
|
||||||
staticLibraries = libSDL3_image.a libpng16.a libz.a libwebp.a libwebpdemux.a libwebpmux.a libsharpyuv.a
|
staticLibraries = libSDL3_image.a libpng16.a libz.a libwebp.a libwebpdemux.a libwebpmux.a libsharpyuv.a
|
||||||
|
|||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
package androidx.compose.ui.graphics.layer
|
|
||||||
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
|
||||||
import androidx.compose.ui.graphics.Canvas
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.Outline
|
|
||||||
import androidx.compose.ui.graphics.Path
|
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.graphics.RenderEffect
|
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
|
||||||
import androidx.compose.ui.unit.Density
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: GraphicsLayer — native no-op actual
|
|
||||||
// ==================
|
|
||||||
//
|
|
||||||
// Upstream's skiko actual (SkiaGraphicsLayer.skiko.kt, 513L) uses Skia's
|
|
||||||
// Picture/Canvas API for record + replay. We don't have skiko on the
|
|
||||||
// SDL3 path and the Skia path doesn't route through GraphicsLayer yet
|
|
||||||
// — the renderer's per-node draw does its own composition. So this
|
|
||||||
// actual is a plain field bag with no rendering behavior: setters store
|
|
||||||
// their values, getters return them, record/draw/toImageBitmap are
|
|
||||||
// no-ops. Nothing in our pipeline constructs a GraphicsLayer yet.
|
|
||||||
|
|
||||||
@Suppress("PropertyName", "unused")
|
|
||||||
actual class GraphicsLayer {
|
|
||||||
|
|
||||||
actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
|
|
||||||
actual var topLeft: IntOffset = IntOffset.Zero
|
|
||||||
actual var size: IntSize = IntSize.Zero
|
|
||||||
actual var pivotOffset: Offset = Offset.Unspecified
|
|
||||||
actual var alpha: Float = 1f
|
|
||||||
actual var scaleX: Float = 1f
|
|
||||||
actual var scaleY: Float = 1f
|
|
||||||
actual var translationX: Float = 0f
|
|
||||||
actual var translationY: Float = 0f
|
|
||||||
actual var shadowElevation: Float = 0f
|
|
||||||
actual var ambientShadowColor: Color = Color.Black
|
|
||||||
actual var spotShadowColor: Color = Color.Black
|
|
||||||
actual var blendMode: BlendMode = BlendMode.SrcOver
|
|
||||||
actual var colorFilter: ColorFilter? = null
|
|
||||||
|
|
||||||
private var outlineField: Outline = Outline.Rectangle(androidx.compose.ui.geometry.Rect.Zero)
|
|
||||||
actual val outline: Outline get() = outlineField
|
|
||||||
|
|
||||||
actual fun setOutsets(left: Int, top: Int, right: Int, bottom: Int) { /* no-op */ }
|
|
||||||
actual fun setPathOutline(path: Path) { outlineField = Outline.Generic(path) }
|
|
||||||
actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) {
|
|
||||||
outlineField = Outline.Rounded(
|
|
||||||
androidx.compose.ui.geometry.RoundRect(
|
|
||||||
left = topLeft.x, top = topLeft.y,
|
|
||||||
right = topLeft.x + size.width, bottom = topLeft.y + size.height,
|
|
||||||
cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actual fun setRectOutline(topLeft: Offset, size: Size) {
|
|
||||||
val effTopLeft = if (topLeft == Offset.Unspecified) Offset.Zero else topLeft
|
|
||||||
val effSize = if (size == Size.Unspecified) this.size.let { Size(it.width.toFloat(), it.height.toFloat()) } else size
|
|
||||||
outlineField = Outline.Rectangle(
|
|
||||||
androidx.compose.ui.geometry.Rect(
|
|
||||||
effTopLeft.x, effTopLeft.y,
|
|
||||||
effTopLeft.x + effSize.width, effTopLeft.y + effSize.height,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual var rotationX: Float = 0f
|
|
||||||
actual var rotationY: Float = 0f
|
|
||||||
actual var rotationZ: Float = 0f
|
|
||||||
actual var cameraDistance: Float = DefaultCameraDistance
|
|
||||||
actual var clip: Boolean = false
|
|
||||||
actual var renderEffect: RenderEffect? = null
|
|
||||||
actual var isReleased: Boolean = false
|
|
||||||
internal set
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Record / draw — lambda replay
|
|
||||||
//
|
|
||||||
// Upstream's skiko actual records into a Skia Picture. This renderer is
|
|
||||||
// immediate-mode, so record() stores the draw lambda and draw() REPLAYS it
|
|
||||||
// with the layer's transform / alpha / clip applied. Same visual result as
|
|
||||||
// a display list as long as callers re-record on invalidation (they do —
|
|
||||||
// that's the GraphicsLayer contract). This is what makes GraphicsContext
|
|
||||||
// consumers render: SharedTransitionLayout's overlay, rememberGraphicsLayer.
|
|
||||||
|
|
||||||
private var recordedBlock: (DrawScope.() -> Unit)? = null
|
|
||||||
private var recordedDensity: Density = Density(1f)
|
|
||||||
private var recordedLayoutDirection: LayoutDirection = LayoutDirection.Ltr
|
|
||||||
private val drawScope = androidx.compose.ui.graphics.drawscope.CanvasDrawScope()
|
|
||||||
|
|
||||||
actual fun record(
|
|
||||||
density: Density,
|
|
||||||
layoutDirection: LayoutDirection,
|
|
||||||
size: IntSize,
|
|
||||||
block: DrawScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
recordedDensity = density
|
|
||||||
recordedLayoutDirection = layoutDirection
|
|
||||||
this.size = size
|
|
||||||
recordedBlock = block
|
|
||||||
}
|
|
||||||
|
|
||||||
actual suspend fun toImageBitmap(): ImageBitmap =
|
|
||||||
throw NotImplementedError("GraphicsLayer.toImageBitmap not implemented on desktop")
|
|
||||||
|
|
||||||
internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) {
|
|
||||||
if (isReleased) return
|
|
||||||
val vBlock = recordedBlock ?: return
|
|
||||||
canvas.save()
|
|
||||||
canvas.translate(topLeft.x.toFloat(), topLeft.y.toFloat())
|
|
||||||
// Transform about the pivot (defaults to the layer centre).
|
|
||||||
if (scaleX != 1f || scaleY != 1f || rotationZ != 0f || translationX != 0f || translationY != 0f) {
|
|
||||||
val vPivot =
|
|
||||||
if (pivotOffset == Offset.Unspecified) Offset(size.width / 2f, size.height / 2f)
|
|
||||||
else pivotOffset
|
|
||||||
canvas.translate(translationX + vPivot.x, translationY + vPivot.y)
|
|
||||||
if (scaleX != 1f || scaleY != 1f) canvas.scale(scaleX, scaleY)
|
|
||||||
if (rotationZ != 0f) canvas.rotate(rotationZ)
|
|
||||||
canvas.translate(-vPivot.x, -vPivot.y)
|
|
||||||
}
|
|
||||||
if (clip) {
|
|
||||||
// Rect/rounded outlines clip to their bounds (SDL clips are rects;
|
|
||||||
// rounded-corner exactness would need the offscreen mask path);
|
|
||||||
// generic outlines go through clipPath (bbox fallback on SDL).
|
|
||||||
when (val vOutline = outline) {
|
|
||||||
is Outline.Rectangle -> canvas.clipRect(vOutline.rect)
|
|
||||||
is Outline.Rounded -> canvas.clipRect(
|
|
||||||
androidx.compose.ui.geometry.Rect(
|
|
||||||
vOutline.roundRect.left, vOutline.roundRect.top,
|
|
||||||
vOutline.roundRect.right, vOutline.roundRect.bottom,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
is Outline.Generic -> canvas.clipPath(vOutline.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val vNeedsAlphaLayer = alpha < 1f
|
|
||||||
if (vNeedsAlphaLayer) {
|
|
||||||
canvas.saveLayer(
|
|
||||||
androidx.compose.ui.geometry.Rect(0f, 0f, size.width.toFloat(), size.height.toFloat()),
|
|
||||||
androidx.compose.ui.graphics.Paint().apply { this.alpha = this@GraphicsLayer.alpha },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
drawScope.draw(
|
|
||||||
recordedDensity,
|
|
||||||
recordedLayoutDirection,
|
|
||||||
canvas,
|
|
||||||
Size(size.width.toFloat(), size.height.toFloat()),
|
|
||||||
vBlock,
|
|
||||||
)
|
|
||||||
if (vNeedsAlphaLayer) canvas.restore()
|
|
||||||
canvas.restore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+437
@@ -0,0 +1,437 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MANUALLY VENDORED (NON-IDEMPOTENT) from compose-multiplatform-core:
|
||||||
|
// compose/ui/ui/src/skikoMain/.../node/GraphicsLayerOwnerLayer.skiko.kt
|
||||||
|
// VENDOR-BASE: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/GraphicsLayerOwnerLayer.skiko.kt @ v1.12.0-beta03+dev4483
|
||||||
|
// Change vs upstream: dropped the trailing `SkiaGraphicsContext.setLightingInfo`
|
||||||
|
// extension + LIGHT_* constants (skiko-only shadow lighting; our shadows go via
|
||||||
|
// NativeShadowCanvas). The class body is unchanged — it is the real OwnedLayer
|
||||||
|
// (dirty-gated record/replay over a per-renderer GraphicsLayer), replacing the
|
||||||
|
// immediate-mode ProjectOwnedLayer. `!`-refused in compose-fork.txt; reconcile
|
||||||
|
// upstream changes by hand (run scripts/compose-fork/check-vendor-drift.py at each
|
||||||
|
// COMPOSE_CORE_REF bump). See RENDERER.md Appendix A.
|
||||||
|
package androidx.compose.ui.node
|
||||||
|
|
||||||
|
import androidx.compose.ui.FrameRateCategory
|
||||||
|
import androidx.compose.ui.geometry.MutableRect
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.isUnspecified
|
||||||
|
import androidx.compose.ui.graphics.Canvas
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.Fields
|
||||||
|
import androidx.compose.ui.graphics.GraphicsContext
|
||||||
|
import androidx.compose.ui.graphics.Matrix
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.graphics.isIdentity
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.setOutline
|
||||||
|
import com.compose.sdl.graphics.prepareLayerTransformationMatrix
|
||||||
|
import androidx.compose.ui.internal.checkPreconditionNotNull
|
||||||
|
import androidx.compose.ui.internal.requirePrecondition
|
||||||
|
import androidx.compose.ui.platform.invertTo
|
||||||
|
import androidx.compose.ui.platform.isInOutline
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class representing a layer that owns and manages a `GraphicsLayer` for composable rendering.
|
||||||
|
*
|
||||||
|
* This layer is responsible for managing graphical properties, transformations, and rendering
|
||||||
|
* tasks associated with a `GraphicsLayer`. It provides mechanisms for updating layer properties,
|
||||||
|
* triggering invalidations, resizing, and mapping offsets or bounds using transformation matrices.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param graphicsLayer The initial `GraphicsLayer` for this layer.
|
||||||
|
* @param context The graphics context responsible for managing this layer, or `null` if externally
|
||||||
|
* managed. When context is not null, it means the object is created internally, and we need to
|
||||||
|
* release it during disposal.
|
||||||
|
* @param layerManager The manager responsible for handling the ownership and lifecycle of this layer.
|
||||||
|
* @param drawBlock The lambda function invoked to perform custom drawing on the canvas.
|
||||||
|
* @param invalidateParentLayer Callback to invalidate the parent layer when necessary.
|
||||||
|
*/
|
||||||
|
internal class GraphicsLayerOwnerLayer(
|
||||||
|
graphicsLayer: GraphicsLayer,
|
||||||
|
private val context: GraphicsContext?,
|
||||||
|
private val layerManager: OwnedLayerManager,
|
||||||
|
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
|
||||||
|
invalidateParentLayer: () -> Unit,
|
||||||
|
) : OwnedLayer {
|
||||||
|
internal var graphicsLayer: GraphicsLayer = graphicsLayer
|
||||||
|
private set
|
||||||
|
private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
|
||||||
|
private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
|
||||||
|
|
||||||
|
private var size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
|
||||||
|
private var density = Density(1f)
|
||||||
|
private var layoutDirection = LayoutDirection.Ltr
|
||||||
|
private val scope = CanvasDrawScope()
|
||||||
|
private var mutatedFields: Int = 0
|
||||||
|
private var transformOrigin: TransformOrigin = TransformOrigin.Center
|
||||||
|
private var outline: Outline? = null
|
||||||
|
|
||||||
|
private val matrixCache = Matrix()
|
||||||
|
private var inverseMatrixCache: Matrix? = null
|
||||||
|
|
||||||
|
private var isDestroyed = false
|
||||||
|
private var isDirty = true
|
||||||
|
set(value) {
|
||||||
|
if (value != field) {
|
||||||
|
field = value
|
||||||
|
layerManager.notifyLayerIsDirty(this, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var isMatrixDirty = false
|
||||||
|
private var isInverseMatrixDirty = false
|
||||||
|
private var isIdentity = true
|
||||||
|
override var frameRate: Float = 0f
|
||||||
|
override var isFrameRateFromParent = false
|
||||||
|
|
||||||
|
override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
|
||||||
|
val maybeChangedFields = scope.mutatedFields or mutatedFields
|
||||||
|
this.layoutDirection = scope.layoutDirection
|
||||||
|
this.density = scope.graphicsDensity
|
||||||
|
if (maybeChangedFields and Fields.Outsets != 0) {
|
||||||
|
with(density) {
|
||||||
|
graphicsLayer.setOutsets(
|
||||||
|
left = scope.outsets.left.roundToPx(),
|
||||||
|
top = scope.outsets.top.roundToPx(),
|
||||||
|
right = scope.outsets.right.roundToPx(),
|
||||||
|
bottom = scope.outsets.bottom.roundToPx(),
|
||||||
|
)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.TransformOrigin != 0) {
|
||||||
|
this.transformOrigin = scope.transformOrigin
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.ScaleX != 0) {
|
||||||
|
graphicsLayer.scaleX = scope.scaleX
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.ScaleY != 0) {
|
||||||
|
graphicsLayer.scaleY = scope.scaleY
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.Alpha != 0) {
|
||||||
|
graphicsLayer.alpha = scope.alpha
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.TranslationX != 0) {
|
||||||
|
graphicsLayer.translationX = scope.translationX
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.TranslationY != 0) {
|
||||||
|
graphicsLayer.translationY = scope.translationY
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.ShadowElevation != 0) {
|
||||||
|
graphicsLayer.shadowElevation = scope.shadowElevation
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
|
||||||
|
graphicsLayer.ambientShadowColor = scope.ambientShadowColor
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.SpotShadowColor != 0) {
|
||||||
|
graphicsLayer.spotShadowColor = scope.spotShadowColor
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.RotationZ != 0) {
|
||||||
|
graphicsLayer.rotationZ = scope.rotationZ
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.RotationX != 0) {
|
||||||
|
graphicsLayer.rotationX = scope.rotationX
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.RotationY != 0) {
|
||||||
|
graphicsLayer.rotationY = scope.rotationY
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.CameraDistance != 0) {
|
||||||
|
graphicsLayer.cameraDistance = scope.cameraDistance
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.TransformOrigin != 0) {
|
||||||
|
if (transformOrigin == TransformOrigin.Center) {
|
||||||
|
graphicsLayer.pivotOffset = Offset.Unspecified
|
||||||
|
} else {
|
||||||
|
graphicsLayer.pivotOffset =
|
||||||
|
Offset(
|
||||||
|
transformOrigin.pivotFractionX * size.width,
|
||||||
|
transformOrigin.pivotFractionY * size.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.Clip != 0) {
|
||||||
|
graphicsLayer.clip = scope.clip
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.RenderEffect != 0) {
|
||||||
|
graphicsLayer.renderEffect = scope.renderEffect
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.ColorFilter != 0) {
|
||||||
|
graphicsLayer.colorFilter = scope.colorFilter
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.BlendMode != 0) {
|
||||||
|
graphicsLayer.blendMode = scope.blendMode
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.CompositingStrategy != 0) {
|
||||||
|
graphicsLayer.compositingStrategy =
|
||||||
|
when (scope.compositingStrategy) {
|
||||||
|
CompositingStrategy.Auto -> androidx.compose.ui.graphics.layer.CompositingStrategy.Auto
|
||||||
|
CompositingStrategy.Offscreen -> androidx.compose.ui.graphics.layer.CompositingStrategy.Offscreen
|
||||||
|
CompositingStrategy.ModulateAlpha -> androidx.compose.ui.graphics.layer.CompositingStrategy.ModulateAlpha
|
||||||
|
else -> throw IllegalStateException("Not supported composition strategy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
|
||||||
|
isMatrixDirty = true
|
||||||
|
isInverseMatrixDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var outlineChanged = false
|
||||||
|
|
||||||
|
if (outline != scope.outline) {
|
||||||
|
outlineChanged = true
|
||||||
|
outline = scope.outline
|
||||||
|
updateOutline()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutatedFields = scope.mutatedFields
|
||||||
|
if (maybeChangedFields != 0 || outlineChanged) {
|
||||||
|
triggerRepaint()
|
||||||
|
layerManager.voteFrameRate(frameRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers redrawing of Compose content during the next frame.
|
||||||
|
*/
|
||||||
|
private fun triggerRepaint() {
|
||||||
|
layerManager.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOutline() {
|
||||||
|
val outline = outline ?: return
|
||||||
|
graphicsLayer.setOutline(outline)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isInLayer(position: Offset): Boolean {
|
||||||
|
val x = position.x
|
||||||
|
val y = position.y
|
||||||
|
|
||||||
|
if (graphicsLayer.clip) {
|
||||||
|
return isInOutline(graphicsLayer.outline, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun move(position: IntOffset) {
|
||||||
|
layerManager.voteFrameRate(FrameRateCategory.High.value)
|
||||||
|
graphicsLayer.topLeft = position
|
||||||
|
triggerRepaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resize(size: IntSize) {
|
||||||
|
if (size != this.size) {
|
||||||
|
layerManager.voteFrameRate(FrameRateCategory.High.value)
|
||||||
|
this.size = size
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
|
||||||
|
updateDisplayList()
|
||||||
|
scope.drawContext.also {
|
||||||
|
it.canvas = canvas
|
||||||
|
it.graphicsLayer = parentLayer
|
||||||
|
}
|
||||||
|
scope.drawLayer(graphicsLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateDisplayList() {
|
||||||
|
layerManager.voteFrameRate(frameRate)
|
||||||
|
if (isDirty) {
|
||||||
|
if (transformOrigin != TransformOrigin.Center && graphicsLayer.size != size) {
|
||||||
|
graphicsLayer.pivotOffset =
|
||||||
|
Offset(
|
||||||
|
transformOrigin.pivotFractionX * size.width,
|
||||||
|
transformOrigin.pivotFractionY * size.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
graphicsLayer.record(density, layoutDirection, size, recordLambda)
|
||||||
|
isDirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val recordLambda: DrawScope.() -> Unit = {
|
||||||
|
drawIntoCanvas { canvas ->
|
||||||
|
this@GraphicsLayerOwnerLayer.drawBlock?.let { it(canvas, drawContext.graphicsLayer) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks content as dirty and triggers redrawing.
|
||||||
|
*/
|
||||||
|
override fun invalidate() {
|
||||||
|
if (isDestroyed) return
|
||||||
|
isDirty = true
|
||||||
|
layerManager.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
frameRate = 0f
|
||||||
|
isFrameRateFromParent = false
|
||||||
|
drawBlock = null
|
||||||
|
invalidateParentLayer = null
|
||||||
|
isDestroyed = true
|
||||||
|
isDirty = false
|
||||||
|
if (context != null) {
|
||||||
|
context.releaseGraphicsLayer(graphicsLayer)
|
||||||
|
|
||||||
|
// Recycle only in case of non-null context (meaning only for not external layers).
|
||||||
|
layerManager.recycle(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
|
||||||
|
val matrix =
|
||||||
|
if (inverse) {
|
||||||
|
getInverseMatrix() ?: return Offset.Infinite
|
||||||
|
} else {
|
||||||
|
getMatrix()
|
||||||
|
}
|
||||||
|
return if (isIdentity) {
|
||||||
|
point
|
||||||
|
} else {
|
||||||
|
matrix.map(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mapBounds(rect: MutableRect, inverse: Boolean) {
|
||||||
|
val matrix = if (inverse) getInverseMatrix() else getMatrix()
|
||||||
|
if (!isIdentity) {
|
||||||
|
if (matrix == null) {
|
||||||
|
rect.set(0f, 0f, 0f, 0f)
|
||||||
|
} else {
|
||||||
|
matrix.map(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reuseLayer(
|
||||||
|
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
|
||||||
|
invalidateParentLayer: () -> Unit
|
||||||
|
) {
|
||||||
|
val context =
|
||||||
|
checkPreconditionNotNull(context) {
|
||||||
|
"currently reuse is only supported when we manage the layer lifecycle"
|
||||||
|
}
|
||||||
|
requirePrecondition(graphicsLayer.isReleased) {
|
||||||
|
"layer should have been released before reuse"
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate a layer
|
||||||
|
graphicsLayer = context.createGraphicsLayer()
|
||||||
|
isDestroyed = false
|
||||||
|
|
||||||
|
// apply new params
|
||||||
|
this.drawBlock = drawBlock
|
||||||
|
this.invalidateParentLayer = invalidateParentLayer
|
||||||
|
|
||||||
|
// reset mutable variables to their initial values
|
||||||
|
isMatrixDirty = false
|
||||||
|
isInverseMatrixDirty = false
|
||||||
|
isIdentity = true
|
||||||
|
matrixCache.reset()
|
||||||
|
inverseMatrixCache?.reset()
|
||||||
|
transformOrigin = TransformOrigin.Center
|
||||||
|
size = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
|
||||||
|
outline = null
|
||||||
|
mutatedFields = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun transform(matrix: Matrix) {
|
||||||
|
matrix.timesAssign(getMatrix())
|
||||||
|
}
|
||||||
|
|
||||||
|
override val underlyingMatrix: Matrix
|
||||||
|
get() = getMatrix()
|
||||||
|
|
||||||
|
override fun inverseTransform(matrix: Matrix) {
|
||||||
|
val inverse = getInverseMatrix()
|
||||||
|
if (inverse != null) {
|
||||||
|
matrix.timesAssign(inverse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMatrix(): Matrix {
|
||||||
|
updateMatrix()
|
||||||
|
return matrixCache
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInverseMatrix(): Matrix? {
|
||||||
|
val matrix = getMatrix()
|
||||||
|
if (isIdentity) {
|
||||||
|
return matrix
|
||||||
|
}
|
||||||
|
|
||||||
|
val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it }
|
||||||
|
if (!isInverseMatrixDirty) {
|
||||||
|
return if (inverseMatrix[0, 0].isNaN()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
inverseMatrix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isInverseMatrixDirty = false
|
||||||
|
return if (matrix.invertTo(inverseMatrix)) {
|
||||||
|
inverseMatrix
|
||||||
|
} else {
|
||||||
|
inverseMatrix[0, 0] = Float.NaN
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMatrix() {
|
||||||
|
if (!isMatrixDirty) return
|
||||||
|
with(graphicsLayer) {
|
||||||
|
val pivotX: Float
|
||||||
|
val pivotY: Float
|
||||||
|
if (pivotOffset.isUnspecified) {
|
||||||
|
pivotX = this@GraphicsLayerOwnerLayer.size.width / 2f
|
||||||
|
pivotY = this@GraphicsLayerOwnerLayer.size.height / 2f
|
||||||
|
} else {
|
||||||
|
pivotX = pivotOffset.x
|
||||||
|
pivotY = pivotOffset.y
|
||||||
|
}
|
||||||
|
prepareLayerTransformationMatrix(
|
||||||
|
matrix = matrixCache,
|
||||||
|
pivotX = pivotX,
|
||||||
|
pivotY = pivotY,
|
||||||
|
translationX = translationX,
|
||||||
|
translationY = translationY,
|
||||||
|
rotationX = rotationX,
|
||||||
|
rotationY = rotationY,
|
||||||
|
rotationZ = rotationZ,
|
||||||
|
scaleX = scaleX,
|
||||||
|
scaleY = scaleY,
|
||||||
|
cameraDistance = cameraDistance
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isMatrixDirty = false
|
||||||
|
isIdentity = matrixCache.isIdentity()
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-6
@@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.Constraints
|
import androidx.compose.ui.unit.Constraints
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
import androidx.compose.ui.unit.isUnspecified
|
import androidx.compose.ui.unit.isUnspecified
|
||||||
import com.compose.sdl.text.NativeTextCanvas
|
import com.compose.sdl.text.NativeTextCanvas
|
||||||
import com.compose.sdl.text.currentTextMeasurer
|
import com.compose.sdl.text.currentTextMeasurer
|
||||||
@@ -73,7 +74,32 @@ internal class SdlParagraph(
|
|||||||
private val wrapped = currentTextMeasurer.wrap(text, fontPx, maxWidthPx, family, variations)
|
private val wrapped = currentTextMeasurer.wrap(text, fontPx, maxWidthPx, family, variations)
|
||||||
private val allLines: List<String> = wrapped.lines
|
private val allLines: List<String> = wrapped.lines
|
||||||
private val lineStarts: IntArray = wrapped.lineStarts
|
private val lineStarts: IntArray = wrapped.lineStarts
|
||||||
private val lh: Float = currentTextMeasurer.lineHeight(fontPx, family, variations).coerceAtLeast(1f)
|
|
||||||
|
// TextStyle.lineHeight in px — upstream Skia paragraphs honour it (every M3
|
||||||
|
// Typography style sets one, e.g. bodyMedium 14sp/20sp), so ignoring it made every
|
||||||
|
// text block's height differ from upstream by ~1px per WRAPPED line, an error that
|
||||||
|
// ACCUMULATED down the page (the dominant term in every parity diff). sp scales by
|
||||||
|
// density like fontPx; em multiplies the font size.
|
||||||
|
private val styleLineHeightPx: Float? = when {
|
||||||
|
style.lineHeight.isUnspecified -> null
|
||||||
|
style.lineHeight.type == TextUnitType.Sp -> style.lineHeight.value * density
|
||||||
|
style.lineHeight.type == TextUnitType.Em -> style.lineHeight.value * fontPx
|
||||||
|
else -> null
|
||||||
|
// Upstream ignores a lineHeight NOT STRICTLY GREATER than the font size (skia
|
||||||
|
// only applies height multipliers > 1em) — probe: 48sp/24lh -> the 65px cell,
|
||||||
|
// and 24sp/24lh (h = 1.0 exactly) -> the 33px cell, while 24sp/25lh -> 25.
|
||||||
|
}?.takeIf { it > fontPx }
|
||||||
|
// How the band applies depends on TextStyle.lineHeightStyle (probe-verified vs JVM):
|
||||||
|
// - unspecified (raw BasicText): COMPAT TRIM — the first line keeps the tight font
|
||||||
|
// cell, lineHeight is the advance between baselines. n lines = cell + (n-1)*lh.
|
||||||
|
// - Trim.None (every M3 Typography style: Center/None/Fixed): UNIFORM bands — every
|
||||||
|
// line is exactly lineHeight, single-line included (even below the cell).
|
||||||
|
private val uniformBands: Boolean =
|
||||||
|
style.lineHeightStyle?.trim == androidx.compose.ui.text.style.LineHeightStyle.Trim.None
|
||||||
|
private val fontCellH: Float =
|
||||||
|
currentTextMeasurer.lineHeight(fontPx, family, variations).coerceAtLeast(1f)
|
||||||
|
private val lh: Float = styleLineHeightPx ?: fontCellH
|
||||||
|
private val firstLineH: Float = if (uniformBands && styleLineHeightPx != null) lh else fontCellH
|
||||||
|
|
||||||
// True when a span changes glyph METRICS (fontSize / fontWeight) — gates
|
// True when a span changes glyph METRICS (fontSize / fontWeight) — gates
|
||||||
// the styled measurement paths so plain/colour-only text keeps the cheap
|
// the styled measurement paths so plain/colour-only text keeps the cheap
|
||||||
@@ -103,10 +129,14 @@ internal class SdlParagraph(
|
|||||||
// Per-line box heights: base line height unless a size span makes a line's
|
// Per-line box heights: base line height unless a size span makes a line's
|
||||||
// tallest run cell bigger. lineTops[i] = cumulative top; last entry = height.
|
// tallest run cell bigger. lineTops[i] = cumulative top; last entry = height.
|
||||||
private val lineHeights: FloatArray by lazy {
|
private val lineHeights: FloatArray by lazy {
|
||||||
if (!metricSpans) FloatArray(lineCount) { lh }
|
if (!metricSpans) FloatArray(lineCount) { if (it == 0) firstLineH else lh }
|
||||||
else FloatArray(lineCount) {
|
else FloatArray(lineCount) {
|
||||||
styledLineCellHeight(allLines[it], lineStarts[it], spanStyles, fontPx, density, currentTextMeasurer, family, variations)
|
// A size span can push a line's cell above the style's line height, but
|
||||||
.coerceAtLeast(1f)
|
// never below it (first line keeps its tight cell under compat trim).
|
||||||
|
max(
|
||||||
|
styledLineCellHeight(allLines[it], lineStarts[it], spanStyles, fontPx, density, currentTextMeasurer, family, variations),
|
||||||
|
if (it == 0 && !uniformBands) 0f else (styleLineHeightPx ?: 0f),
|
||||||
|
).coerceAtLeast(1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val lineTops: FloatArray by lazy {
|
private val lineTops: FloatArray by lazy {
|
||||||
@@ -118,7 +148,9 @@ internal class SdlParagraph(
|
|||||||
override val width: Float by lazy {
|
override val width: Float by lazy {
|
||||||
if (maxWidthPx == Int.MAX_VALUE) (lineWidths.maxOrNull() ?: 0f) else widthConstraint
|
if (maxWidthPx == Int.MAX_VALUE) (lineWidths.maxOrNull() ?: 0f) else widthConstraint
|
||||||
}
|
}
|
||||||
override val height: Float by lazy { if (!metricSpans) lineCount * lh else lineTops[lineCount] }
|
override val height: Float by lazy {
|
||||||
|
if (!metricSpans) firstLineH + (lineCount - 1) * lh else lineTops[lineCount]
|
||||||
|
}
|
||||||
// Widest single HARD-BREAK line — NOT the concatenated all-on-one-line width.
|
// Widest single HARD-BREAK line — NOT the concatenated all-on-one-line width.
|
||||||
// Compose's Text(softWrap = false) reads this to decide the paragraph width
|
// Compose's Text(softWrap = false) reads this to decide the paragraph width
|
||||||
// (LayoutUtils.finalMaxWidth returns Constraints.Infinity when softWrap=false,
|
// (LayoutUtils.finalMaxWidth returns Constraints.Infinity when softWrap=false,
|
||||||
@@ -193,8 +225,9 @@ internal class SdlParagraph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLineForVerticalPosition(vertical: Float): Int {
|
override fun getLineForVerticalPosition(vertical: Float): Int {
|
||||||
if (!metricSpans) return (vertical / lh).toInt().coerceIn(0, lineCount - 1)
|
|
||||||
// Cumulative tops are monotonic — walk to the line containing `vertical`.
|
// Cumulative tops are monotonic — walk to the line containing `vertical`.
|
||||||
|
// (No uniform-divide fast path anymore: the first line's box is the font
|
||||||
|
// cell while later lines use the style lineHeight.)
|
||||||
for (i in 0 until lineCount) if (vertical < lineTops[i + 1]) return i
|
for (i in 0 until lineCount) if (vertical < lineTops[i + 1]) return i
|
||||||
return lineCount - 1
|
return lineCount - 1
|
||||||
}
|
}
|
||||||
@@ -368,6 +401,8 @@ internal class SdlParagraph(
|
|||||||
inFontVariations = variations,
|
inFontVariations = variations,
|
||||||
inBaseItalic = style.fontStyle == androidx.compose.ui.text.font.FontStyle.Italic,
|
inBaseItalic = style.fontStyle == androidx.compose.ui.text.font.FontStyle.Italic,
|
||||||
inTextDecoration = inDecoration ?: style.textDecoration,
|
inTextDecoration = inDecoration ?: style.textDecoration,
|
||||||
|
inLineHeightPx = lh,
|
||||||
|
inTrimFirstLine = !uniformBands || styleLineHeightPx == null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ interface RenderBackend {
|
|||||||
so it maps 1:1 onto the pixel back buffer. */
|
so it maps 1:1 onto the pixel back buffer. */
|
||||||
fun beginFrame(inDpr: Float)
|
fun beginFrame(inDpr: Float)
|
||||||
|
|
||||||
/* Draw the upstream LayoutNode tree owned by [inHost] through the vendored
|
/* Build this frame's platform Canvas and hand it to [inDraw], which walks the
|
||||||
coordinator / DrawModifierNode pipeline (LayoutNode.draw → CanvasDrawScope
|
composition (host.drawRoot(canvas)) through the vendored coordinator /
|
||||||
→ the platform Canvas backend). Default no-op — the Skia backend will
|
DrawModifierNode pipeline. Taking a (Canvas)->Unit instead of the host keeps
|
||||||
override once SkiaCanvas lands (see NODE_ENGINE_PORT.md Phase 9). */
|
RenderBackend + its implementations independent of the node/host layer — the
|
||||||
fun drawRoot(inHost: com.compose.sdl.node.ComposeRootHost) {}
|
decoupling that lets the renderers live in :ui-graphics (no ui-graphics→ui
|
||||||
|
cycle). Default no-op. */
|
||||||
|
fun drawRoot(inDraw: (canvas: androidx.compose.ui.graphics.Canvas) -> Unit) {}
|
||||||
|
|
||||||
/* Flush + present whatever was just drawn. */
|
/* Flush + present whatever was just drawn. */
|
||||||
fun endFrame()
|
fun endFrame()
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Renderer entry points (expect)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* The renderer seam :window builds against. Each renderer source set
|
||||||
|
(skikoRendererMain / sdlRendererMain) supplies the actuals; exactly one of
|
||||||
|
the two is attached to any given target, so resolution stays unambiguous.
|
||||||
|
|
||||||
|
These were originally declared identically in both renderer source sets
|
||||||
|
with NO expect — fine per-target, but shared nativeMain METADATA (=: what
|
||||||
|
:window's KotlinMultiplatform publication compiles against) could not see
|
||||||
|
them on a host where the attached targets span BOTH renderers (Windows:
|
||||||
|
skikoRenderer for macos/linux + sdlRenderer for mingw). The expect makes
|
||||||
|
nativeMain metadata self-contained, which lets the WINDOWS publish job —
|
||||||
|
the only host that declares every target — produce the root modules. */
|
||||||
|
|
||||||
|
/* Create the render backend for the selected [GpuMode]; null when the
|
||||||
|
backend can't initialise (caller falls back / reports). */
|
||||||
|
expect fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend?
|
||||||
|
|
||||||
|
/* The renderer module's per-OS default GpuMode (used for GpuMode.Auto). */
|
||||||
|
expect fun rendererPreferredGpuMode(): GpuMode
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: DrawStats — per-frame renderer work counters (diagnostic)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cheap per-frame counters the SDL draw path bumps and the frame profiler
|
||||||
|
* reads, to answer "what inside `draw` costs the time?" — geometry submissions
|
||||||
|
* (SDL_RenderGeometry calls), total vertices tessellated, rounded-clip mask
|
||||||
|
* realizations (offscreen render-target passes — the expensive ones), text
|
||||||
|
* blits and image blits. Reset each frame by the profiler after reading.
|
||||||
|
*
|
||||||
|
* Single-threaded (all draw is on the main thread), so plain vars are fine.
|
||||||
|
* Zero overhead when the profiler isn't reading — just integer increments.
|
||||||
|
*/
|
||||||
|
object DrawStats {
|
||||||
|
var geometrySubmits: Int = 0
|
||||||
|
var vertices: Int = 0
|
||||||
|
var maskRealizations: Int = 0
|
||||||
|
var textDraws: Int = 0
|
||||||
|
var imageBlits: Int = 0
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
geometrySubmits = 0
|
||||||
|
vertices = 0
|
||||||
|
maskRealizations = 0
|
||||||
|
textDraws = 0
|
||||||
|
imageBlits = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun summary(): String =
|
||||||
|
"geo=$geometrySubmits verts=$vertices masks=$maskRealizations text=$textDraws img=$imageBlits"
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.GraphicsContext
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: GraphicsContext factory seam (B2 / P1.3)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* ComposeOwner.graphicsContext is created behind a per-renderer factory seam so each
|
||||||
|
leg supplies its own GraphicsContext: exactly one actual is attached per target
|
||||||
|
(the createRenderBackend trick). SDL → the project ProjectGraphicsContext (record/
|
||||||
|
replay via the SDL NativeRenderNode); Skia → upstream's SkiaGraphicsContext (owns a
|
||||||
|
skiko RenderNodeContext). See RENDERER.md §4 (B2). */
|
||||||
|
internal expect fun createGraphicsContext(): GraphicsContext
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
package com.compose.sdl.graphics
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: GraphicsLayer factory — native actuals
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
internal actual fun createProjectGraphicsLayer(): GraphicsLayer = GraphicsLayer()
|
|
||||||
|
|
||||||
internal actual fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer) {
|
|
||||||
inLayer.isReleased = true
|
|
||||||
}
|
|
||||||
@@ -7,18 +7,22 @@ import androidx.compose.ui.graphics.ImageBitmap
|
|||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/* Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
|
/* Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
|
||||||
drawable [ImageBitmap]. The active renderer registers its implementation —
|
drawable [ImageBitmap]. The active renderer registers its implementation at
|
||||||
the SDL backend goes through SDL3_image (see Sdl3EncodedImageDecoder); the
|
backend construction — the SDL backend goes through SDL3_image
|
||||||
Skia renderer never registers because its resources pipeline decodes through
|
(Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
|
||||||
Skia directly (vendored skikoRenderer actuals in :components-resources).
|
SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
|
||||||
|
hook under BOTH renderers (its skikoRendererMain reuses the SDL actuals).
|
||||||
|
|
||||||
Consumed by :components-resources' SDL actuals (`ByteArray.toImageBitmap`,
|
Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
|
||||||
`SvgElement.toSvgPainter`) so painterResource(Res.drawable.x) works without
|
painterResource(Res.drawable.x) works without that module reaching into
|
||||||
that module reaching into renderer internals. */
|
renderer internals. */
|
||||||
interface EncodedImageDecoder {
|
interface EncodedImageDecoder {
|
||||||
fun decode(inBytes: ByteArray): ImageBitmap?
|
fun decode(inBytes: ByteArray): ImageBitmap?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Volatile: written by the render backend on the main thread, read by the
|
||||||
|
resources pipeline on Dispatchers.Default workers. */
|
||||||
|
@kotlin.concurrent.Volatile
|
||||||
var encodedImageDecoder: EncodedImageDecoder? = null
|
var encodedImageDecoder: EncodedImageDecoder? = null
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Matrix
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
// Layer transform matrix builder for GraphicsLayerOwnerLayer (hit-testing /
|
||||||
|
// coordinate mapping). Body vendored from compose-multiplatform-core
|
||||||
|
// Matrices.skiko.kt `prepareTransformationMatrix` — pure Compose `Matrix` math,
|
||||||
|
// no Skia. It lives HERE in nativeMain (shared by both renderer legs) under a
|
||||||
|
// distinct name so it is visible to GraphicsLayerOwnerLayer (also nativeMain);
|
||||||
|
// the upstream `prepareTransformationMatrix` stays skikoRenderer-only in the
|
||||||
|
// vendored Matrices.skiko.kt and would not be visible up here. Distinct name
|
||||||
|
// avoids clashing with that copy on the Skia leg. (RENDERER.md P1.5 will
|
||||||
|
// reverse this rename once B2 vendors Matrices.skiko onto the Skia leg.)
|
||||||
|
// VENDOR-BASE: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/graphics/Matrices.skiko.kt @ v1.12.0-beta03+dev4483
|
||||||
|
|
||||||
|
// Builds the layer transform (translation / rotation / scale / camera about a
|
||||||
|
// pivot) into [matrix]. Matches the skiko copy so hit-testing agrees across legs.
|
||||||
|
internal fun prepareLayerTransformationMatrix(
|
||||||
|
matrix: Matrix,
|
||||||
|
pivotX: Float,
|
||||||
|
pivotY: Float,
|
||||||
|
translationX: Float,
|
||||||
|
translationY: Float,
|
||||||
|
rotationX: Float,
|
||||||
|
rotationY: Float,
|
||||||
|
rotationZ: Float,
|
||||||
|
scaleX: Float,
|
||||||
|
scaleY: Float,
|
||||||
|
cameraDistance: Float,
|
||||||
|
) {
|
||||||
|
matrix.reset()
|
||||||
|
matrix.translate(x = -pivotX, y = -pivotY)
|
||||||
|
matrix *= Matrix().apply {
|
||||||
|
rotateZ(rotationZ)
|
||||||
|
rotateY(rotationY)
|
||||||
|
rotateX(rotationX)
|
||||||
|
scale(scaleX, scaleY)
|
||||||
|
}
|
||||||
|
// Perspective transform should be applied only in case of rotations to avoid
|
||||||
|
// multiply application in hierarchies.
|
||||||
|
// See Android's frameworks/base/libs/hwui/RenderProperties.cpp for reference
|
||||||
|
if (!rotationX.isZero() || !rotationY.isZero()) {
|
||||||
|
matrix *= Matrix().apply {
|
||||||
|
// The camera location is passed in inches, set in pt
|
||||||
|
val depth = cameraDistance * 72f
|
||||||
|
this[2, 3] = -1f / depth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matrix *= Matrix().apply {
|
||||||
|
translate(x = pivotX + translationX, y = pivotY + translationY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third column and row are irrelevant for 2D space.
|
||||||
|
// Zeroing required to get correct inverse transformation matrix.
|
||||||
|
matrix[2, 0] = 0f
|
||||||
|
matrix[2, 1] = 0f
|
||||||
|
matrix[2, 3] = 0f
|
||||||
|
matrix[0, 2] = 0f
|
||||||
|
matrix[1, 2] = 0f
|
||||||
|
matrix[3, 2] = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val NON_ZERO_EPSILON = 0.001f
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun Float.isZero(): Boolean = abs(this) <= NON_ZERO_EPSILON
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import kotlinx.atomicfu.locks.SynchronizedObject
|
||||||
|
import kotlinx.atomicfu.locks.synchronized
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: NativeReleaseQueue — main-thread-deferred native resource disposal
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queue of native-resource release actions drained on the MAIN THREAD once
|
||||||
|
* per frame by the window loop (see ComposeWindow). It exists because the two
|
||||||
|
* ways a renderer resource dies are both off the main thread, while the calls
|
||||||
|
* that free them (SDL_DestroyTexture, and any renderer API) are main-thread-only:
|
||||||
|
*
|
||||||
|
* - a Kotlin/Native `Cleaner` runs on a GC worker thread, and
|
||||||
|
* - explicit `close()` can be reached from composition/effect code on other
|
||||||
|
* dispatchers.
|
||||||
|
*
|
||||||
|
* Both simply ENQUEUE here; the main loop runs the actions between frames. This
|
||||||
|
* turns the GC (issue #2: Cleaner-managed native memory starving because the
|
||||||
|
* quiet Kotlin heap rarely triggers a collection) from the disposal MECHANISM
|
||||||
|
* into a mere safeguard — ownership closes resources promptly, the Cleaner is
|
||||||
|
* the backstop for whatever leaks a `close()`.
|
||||||
|
*
|
||||||
|
* Actions must be idempotent-safe at the source (guard against a resource being
|
||||||
|
* both explicitly closed and later GC-cleaned — see SdlImageBitmap's holder).
|
||||||
|
*/
|
||||||
|
object NativeReleaseQueue {
|
||||||
|
|
||||||
|
private val fLock = SynchronizedObject()
|
||||||
|
private var fPending = ArrayList<() -> Unit>()
|
||||||
|
|
||||||
|
/** Enqueue a release action. Safe to call from any thread. */
|
||||||
|
fun enqueue(action: () -> Unit) {
|
||||||
|
synchronized(fLock) { fPending.add(action) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run and clear every queued action. MAIN THREAD ONLY — the actions call
|
||||||
|
* renderer APIs that aren't thread-safe. Returns the number drained.
|
||||||
|
*/
|
||||||
|
fun drain(): Int {
|
||||||
|
val vBatch = synchronized(fLock) {
|
||||||
|
if (fPending.isEmpty()) return 0
|
||||||
|
val vTaken = fPending
|
||||||
|
fPending = ArrayList()
|
||||||
|
vTaken
|
||||||
|
}
|
||||||
|
for (vAction in vBatch) {
|
||||||
|
runCatching { vAction() }
|
||||||
|
}
|
||||||
|
return vBatch.size
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-26
@@ -65,12 +65,36 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
fOwner.animationFrameClock.sendFrame(inNanos)
|
fOwner.animationFrameClock.sendFrame(inNanos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True while a node-level animation (scroll fling, node Animatable) awaits the next
|
||||||
|
// animation frame — the node half of the window's quiescence signal (render-to-quiescence
|
||||||
|
// screenshot capture asks the window whether ANY work is still pending).
|
||||||
|
fun hasAnimationAwaiters(): Boolean = fOwner.animationFrameClock.hasAwaiters
|
||||||
|
|
||||||
fun measureAndLayout() {
|
fun measureAndLayout() {
|
||||||
// Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so
|
// Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so
|
||||||
// requestFocus() from composition OR from a pointer event this frame takes effect —
|
// requestFocus() from composition OR from a pointer event this frame takes effect —
|
||||||
// e.g. focus-on-click, which schedules a focus invalidation during event dispatch.
|
// e.g. focus-on-click, which schedules a focus invalidation during event dispatch.
|
||||||
fOwner.onEndApplyChanges()
|
fOwner.onEndApplyChanges()
|
||||||
fOwner.measureAndLayout()
|
fOwner.measureAndLayout()
|
||||||
|
// Drop snapshot observations whose scope is no longer valid (detached nodes,
|
||||||
|
// destroyed layers, disposed draw scopes). Upstream RootNodeOwner does this after
|
||||||
|
// the measure pass; without it, every disposed subtree's measure/layout/DRAW
|
||||||
|
// observation scopes linger in the OwnerSnapshotObserver forever — each pins its
|
||||||
|
// observed object graph (a leak the P2.2 soak caught on ripple/indication draws).
|
||||||
|
fOwner.snapshotObserver.clearInvalidObservations()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the composed tree into [canvas] — re-records dirty layers, then walks
|
||||||
|
// the root so clean layers replay their cached content. Backends call this
|
||||||
|
// instead of rootNode.draw so retained-layer bookkeeping runs each frame.
|
||||||
|
fun drawRoot(canvas: androidx.compose.ui.graphics.Canvas) {
|
||||||
|
fOwner.renderRoot(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window wires this to set needsFrame — a layer whose content changed
|
||||||
|
// (OwnedLayer.invalidate) schedules a frame even with nothing else pending.
|
||||||
|
fun setInvalidationCallback(callback: () -> Unit) {
|
||||||
|
fOwner.onInvalidate = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// The owner's FocusOwner IS a FocusManager — the window provides it as LocalFocusManager.
|
// The owner's FocusOwner IS a FocusManager — the window provides it as LocalFocusManager.
|
||||||
@@ -138,29 +162,8 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
fun onDropComplete() = fOwner.dragAndDropManager.dropComplete()
|
fun onDropComplete() = fOwner.dragAndDropManager.dropComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// NOTE: feedPointerToProcessor / feedScrollToProcessor are plain nativeMain
|
||||||
// MARK: pointer-event bridge (nativeMain builds the internal PointerInputEvent)
|
// functions in PointerEventBridge.native.kt. This facade moved from commonMain to
|
||||||
// ==================
|
// nativeMain (it now constructs the native retained-layer OwnedLayer via
|
||||||
|
// ComposeOwner), so the former expect/actual split collapsed to a single native
|
||||||
/* The internal `expect PointerInputEvent` exposes no commonMain constructor, so the
|
// definition — see RENDERER.md §5.
|
||||||
build+dispatch is a nativeMain actual. Constructs a single-pointer event and feeds it
|
|
||||||
to the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release. */
|
|
||||||
internal expect fun feedPointerToProcessor(
|
|
||||||
inOwner: com.compose.sdl.node.impl.ComposeOwner,
|
|
||||||
inType: Int,
|
|
||||||
inButton: Int,
|
|
||||||
inUptime: Long,
|
|
||||||
inX: Float,
|
|
||||||
inY: Float,
|
|
||||||
)
|
|
||||||
|
|
||||||
/* Builds a Scroll-type PointerInputEvent (scrollDelta) and drives the processor, so the vendored
|
|
||||||
Modifier.scrollable's MouseWheelScrollingLogic handles the wheel. nativeMain-only (ctor is native). */
|
|
||||||
internal expect fun feedScrollToProcessor(
|
|
||||||
inOwner: com.compose.sdl.node.impl.ComposeOwner,
|
|
||||||
inX: Float,
|
|
||||||
inY: Float,
|
|
||||||
inDeltaX: Float,
|
|
||||||
inDeltaY: Float,
|
|
||||||
inUptime: Long,
|
|
||||||
)
|
|
||||||
+2
-2
@@ -25,7 +25,7 @@ private var fTertiaryDown = false
|
|||||||
/* Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse
|
/* Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse
|
||||||
pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release;
|
pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release;
|
||||||
inButton: 0=primary 1=secondary 2=tertiary. */
|
inButton: 0=primary 1=secondary 2=tertiary. */
|
||||||
internal actual fun feedPointerToProcessor(
|
internal fun feedPointerToProcessor(
|
||||||
inOwner: ComposeOwner,
|
inOwner: ComposeOwner,
|
||||||
inType: Int,
|
inType: Int,
|
||||||
inButton: Int,
|
inButton: Int,
|
||||||
@@ -82,7 +82,7 @@ internal actual fun feedPointerToProcessor(
|
|||||||
vendored Modifier.scrollable (MouseWheelScrollingLogic) consumes it. SDL wheel delta maps
|
vendored Modifier.scrollable (MouseWheelScrollingLogic) consumes it. SDL wheel delta maps
|
||||||
directly to scrollDelta (wheel-down = SDL deltaY<0 → content scrolls down); magnitude is scaled
|
directly to scrollDelta (wheel-down = SDL deltaY<0 → content scrolls down); magnitude is scaled
|
||||||
in Sdl3ScrollConfig (dp per notch). */
|
in Sdl3ScrollConfig (dp per notch). */
|
||||||
internal actual fun feedScrollToProcessor(
|
internal fun feedScrollToProcessor(
|
||||||
inOwner: ComposeOwner,
|
inOwner: ComposeOwner,
|
||||||
inX: Float,
|
inX: Float,
|
||||||
inY: Float,
|
inY: Float,
|
||||||
|
|||||||
+94
-13
@@ -9,6 +9,8 @@ import androidx.compose.ui.node.LayoutNodeDrawScope
|
|||||||
import androidx.compose.ui.node.MeasureAndLayoutDelegate
|
import androidx.compose.ui.node.MeasureAndLayoutDelegate
|
||||||
import androidx.compose.ui.node.Owner
|
import androidx.compose.ui.node.Owner
|
||||||
import androidx.compose.ui.node.OwnedLayer
|
import androidx.compose.ui.node.OwnedLayer
|
||||||
|
import androidx.compose.ui.node.OwnedLayerManager
|
||||||
|
import androidx.compose.ui.node.GraphicsLayerOwnerLayer
|
||||||
import androidx.compose.ui.node.OwnerScope
|
import androidx.compose.ui.node.OwnerScope
|
||||||
import androidx.compose.ui.node.OwnerSnapshotObserver
|
import androidx.compose.ui.node.OwnerSnapshotObserver
|
||||||
import androidx.compose.ui.node.RootForTest
|
import androidx.compose.ui.node.RootForTest
|
||||||
@@ -79,7 +81,7 @@ internal class ComposeOwner(
|
|||||||
override val root: LayoutNode,
|
override val root: LayoutNode,
|
||||||
override val density: Density = Density(1f, 1f),
|
override val density: Density = Density(1f, 1f),
|
||||||
override val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
|
override val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
|
||||||
) : Owner {
|
) : Owner, OwnedLayerManager {
|
||||||
|
|
||||||
// The vendored measure/layout state machine, rooted at [root].
|
// The vendored measure/layout state machine, rooted at [root].
|
||||||
private val fDelegate = MeasureAndLayoutDelegate(root)
|
private val fDelegate = MeasureAndLayoutDelegate(root)
|
||||||
@@ -151,6 +153,11 @@ internal class ComposeOwner(
|
|||||||
|
|
||||||
override fun onDetach(node: LayoutNode) {
|
override fun onDetach(node: LayoutNode) {
|
||||||
fDelegate.onNodeDetached(node)
|
fDelegate.onNodeDetached(node)
|
||||||
|
// Drop the detached node's read-observation scopes, exactly as upstream
|
||||||
|
// RootNodeOwner.onDetach does. Without this, every disposed node's measure/
|
||||||
|
// layout/draw observation scopes accumulate in the snapshot observer forever
|
||||||
|
// — the baseline composition-machinery leak the P2.2 soak caught.
|
||||||
|
snapshotObserver.clear(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
|
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
|
||||||
@@ -176,7 +183,86 @@ internal class ComposeOwner(
|
|||||||
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
|
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
|
||||||
invalidateParentLayer: () -> Unit,
|
invalidateParentLayer: () -> Unit,
|
||||||
explicitLayer: GraphicsLayer?,
|
explicitLayer: GraphicsLayer?,
|
||||||
): OwnedLayer = ProjectOwnedLayer(drawBlock)
|
): OwnedLayer = GraphicsLayerOwnerLayer(
|
||||||
|
graphicsLayer = explicitLayer ?: graphicsContext.createGraphicsLayer(),
|
||||||
|
context = if (explicitLayer != null) null else graphicsContext,
|
||||||
|
layerManager = this,
|
||||||
|
drawBlock = drawBlock,
|
||||||
|
invalidateParentLayer = invalidateParentLayer,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// OwnedLayerManager — retained-layer bookkeeping (mirrors upstream
|
||||||
|
// OwnedLayerManagerImpl). dirtyLayers are re-recorded once per frame in
|
||||||
|
// [renderRoot] before the tree is drawn; clean layers replay their cached
|
||||||
|
// display list. invalidate() asks the window to schedule a frame.
|
||||||
|
|
||||||
|
private val dirtyLayers = mutableListOf<OwnedLayer>()
|
||||||
|
private var postponedDirtyLayers: MutableList<OwnedLayer>? = null
|
||||||
|
private var isDrawingContent = false
|
||||||
|
|
||||||
|
// Set by the window (via ComposeRootHost) to flag needsFrame; a layer whose
|
||||||
|
// content changed (OwnedLayer.invalidate) schedules a frame even when nothing
|
||||||
|
// else (recompose / relayout) is pending.
|
||||||
|
internal var onInvalidate: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun invalidate() {
|
||||||
|
onInvalidate?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notifyLayerIsDirty(layer: OwnedLayer, isDirty: Boolean) {
|
||||||
|
// Mirrors upstream OwnedLayerManagerImpl exactly. The CRITICAL detail: when a
|
||||||
|
// layer clears its dirty flag DURING drawing (updateDisplayList sets isDirty
|
||||||
|
// = false in the renderRoot loop), we must NOT remove it from dirtyLayers here
|
||||||
|
// — that would shrink the list mid-iteration (IndexOutOfBounds). renderRoot's
|
||||||
|
// dirtyLayers.clear() after the loop handles it instead.
|
||||||
|
if (!isDirty) {
|
||||||
|
if (!isDrawingContent) {
|
||||||
|
dirtyLayers.remove(layer)
|
||||||
|
postponedDirtyLayers?.remove(layer)
|
||||||
|
}
|
||||||
|
} else if (!isDrawingContent) {
|
||||||
|
dirtyLayers += layer
|
||||||
|
} else {
|
||||||
|
val postponed = postponedDirtyLayers
|
||||||
|
?: mutableListOf<OwnedLayer>().also { postponedDirtyLayers = it }
|
||||||
|
postponed += layer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun recycle(layer: OwnedLayer): Boolean {
|
||||||
|
dirtyLayers.remove(layer)
|
||||||
|
postponedDirtyLayers?.remove(layer)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both Owner and OwnedLayerManager declare voteFrameRate with a default, so an
|
||||||
|
// explicit override is required. We render at a fixed vsync cadence — no voting.
|
||||||
|
override fun voteFrameRate(frameRate: Float) {}
|
||||||
|
|
||||||
|
// Draw the tree: re-record dirty layers' display lists, then walk the root so
|
||||||
|
// clean layers replay. Called once per frame by ComposeRootHost.drawRoot.
|
||||||
|
internal fun renderRoot(canvas: Canvas) {
|
||||||
|
isDrawingContent = true
|
||||||
|
// Re-record dirty layers BEFORE drawing (unlike Android, our draw forms the
|
||||||
|
// actual render-command sequence, so display lists must be current first).
|
||||||
|
// notifyLayerIsDirty(false) is a no-op while drawing, so the list can't shrink
|
||||||
|
// here; clear() below drops them all at once.
|
||||||
|
if (dirtyLayers.isNotEmpty()) {
|
||||||
|
for (i in 0 until dirtyLayers.size) {
|
||||||
|
dirtyLayers[i].updateDisplayList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dirtyLayers.clear()
|
||||||
|
root.draw(canvas, null)
|
||||||
|
// Layers that invalidated themselves during draw were parked in postponed;
|
||||||
|
// roll them into next frame.
|
||||||
|
postponedDirtyLayers?.let {
|
||||||
|
dirtyLayers.addAll(it)
|
||||||
|
it.clear()
|
||||||
|
}
|
||||||
|
isDrawingContent = false
|
||||||
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// No-op subsystems (focus / semantics / input / clipboard / text / …).
|
// No-op subsystems (focus / semantics / input / clipboard / text / …).
|
||||||
@@ -216,17 +302,12 @@ internal class ComposeOwner(
|
|||||||
containsControls: Boolean,
|
containsControls: Boolean,
|
||||||
): Long = originalTimeoutMillis
|
): Long = originalTimeoutMillis
|
||||||
}
|
}
|
||||||
// GraphicsLayer here is the project record/replay actual (see
|
// Created behind a per-renderer factory seam (B2 / P1.3): SDL uses the project
|
||||||
// GraphicsLayer.native.kt) — SharedTransitionLayout overlays and
|
// record/replay GraphicsContext; the Skia leg swaps in upstream's SkiaGraphicsContext
|
||||||
// rememberGraphicsLayer() create layers through this context. The
|
// at P1.6. SharedTransitionLayout overlays and rememberGraphicsLayer() create layers
|
||||||
// expect class hides its constructor/release from commonMain, hence
|
// through this context. See com/compose/sdl/graphics/GraphicsContextFactory.kt.
|
||||||
// the project factory hops.
|
override val graphicsContext: GraphicsContext =
|
||||||
override val graphicsContext: GraphicsContext = object : GraphicsContext {
|
com.compose.sdl.graphics.createGraphicsContext()
|
||||||
override fun createGraphicsLayer(): GraphicsLayer =
|
|
||||||
com.compose.sdl.graphics.createProjectGraphicsLayer()
|
|
||||||
override fun releaseGraphicsLayer(layer: GraphicsLayer) =
|
|
||||||
com.compose.sdl.graphics.releaseProjectGraphicsLayer(layer)
|
|
||||||
}
|
|
||||||
override val textToolbar: TextToolbar = object : TextToolbar {
|
override val textToolbar: TextToolbar = object : TextToolbar {
|
||||||
override val status: androidx.compose.ui.platform.TextToolbarStatus
|
override val status: androidx.compose.ui.platform.TextToolbarStatus
|
||||||
get() = androidx.compose.ui.platform.TextToolbarStatus.Hidden
|
get() = androidx.compose.ui.platform.TextToolbarStatus.Hidden
|
||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
package androidx.compose.ui.graphics.layer
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.geometry.isUnspecified
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Canvas
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.RenderEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import com.compose.sdl.graphics.NativeRenderNode
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: GraphicsLayer — façade over NativeRenderNode
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// DERIVED (copy + edit) from compose-multiplatform-core
|
||||||
|
// ui-graphics/src/skikoMain/.../graphics/layer/SkiaGraphicsLayer.skiko.kt.
|
||||||
|
// VENDOR-BASE: compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/layer/SkiaGraphicsLayer.skiko.kt @ v1.12.0-beta03+dev4483
|
||||||
|
// The ONLY structural change vs upstream: the backing display-list node is our
|
||||||
|
// renderer-agnostic `NativeRenderNode` instead of `org.jetbrains.skiko.node.RenderNode`,
|
||||||
|
// so both renderers share this façade (Skia's node wraps the real skiko RenderNode;
|
||||||
|
// SDL's node is SdlRenderNode). Every visual property is mirrored onto the node and
|
||||||
|
// applied at replay, exactly like upstream. Trimmed vs upstream: outsets (blur
|
||||||
|
// expansion) and ChildLayerDependenciesTracker (prompt child release — our children
|
||||||
|
// release via NativeReleaseQueue/GC instead). See RENDERER.md §4 (B2).
|
||||||
|
|
||||||
|
@Suppress("PropertyName")
|
||||||
|
actual class GraphicsLayer internal constructor(
|
||||||
|
private val renderNode: NativeRenderNode,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var outlineDirty = true
|
||||||
|
private var roundRectOutlineTopLeft: Offset = Offset.Zero
|
||||||
|
private var roundRectOutlineSize: Size = Size.Unspecified
|
||||||
|
private var roundRectCornerRadius: Float = 0f
|
||||||
|
private var internalOutline: Outline? = null
|
||||||
|
private var outlinePath: Path? = null
|
||||||
|
|
||||||
|
actual var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.compositingStrategy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var topLeft: IntOffset = IntOffset.Zero
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.topLeft = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var size: IntSize = IntSize.Zero
|
||||||
|
private set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.size = value
|
||||||
|
if (roundRectOutlineSize.isUnspecified) {
|
||||||
|
outlineDirty = true
|
||||||
|
configureOutlineAndClip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var pivotOffset: Offset = Offset.Unspecified
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.pivot = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var alpha: Float = 1f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.alpha = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var scaleX: Float = 1f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.scaleX = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var scaleY: Float = 1f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.scaleY = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var translationX: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.translationX = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var translationY: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.translationY = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var shadowElevation: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.shadowElevation = value
|
||||||
|
outlineDirty = true
|
||||||
|
configureOutlineAndClip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var ambientShadowColor: Color = Color.Black
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.ambientShadowColor = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var spotShadowColor: Color = Color.Black
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.spotShadowColor = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var blendMode: BlendMode = BlendMode.SrcOver
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.blendMode = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var colorFilter: ColorFilter? = null
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.colorFilter = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val outline: Outline
|
||||||
|
get() {
|
||||||
|
val tmpOutline = internalOutline
|
||||||
|
val tmpPath = outlinePath
|
||||||
|
return if (tmpOutline != null) {
|
||||||
|
tmpOutline
|
||||||
|
} else if (tmpPath != null) {
|
||||||
|
Outline.Generic(tmpPath).also { internalOutline = it }
|
||||||
|
} else {
|
||||||
|
resolveOutlinePosition { outlineTopLeft, outlineSize ->
|
||||||
|
val left = outlineTopLeft.x
|
||||||
|
val top = outlineTopLeft.y
|
||||||
|
val right = left + outlineSize.width
|
||||||
|
val bottom = top + outlineSize.height
|
||||||
|
val cornerRadius = this.roundRectCornerRadius
|
||||||
|
if (cornerRadius > 0f) {
|
||||||
|
Outline.Rounded(RoundRect(left, top, right, bottom, CornerRadius(cornerRadius)))
|
||||||
|
} else {
|
||||||
|
Outline.Rectangle(Rect(left, top, right, bottom))
|
||||||
|
}
|
||||||
|
}.also { internalOutline = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetOutlineParams() {
|
||||||
|
internalOutline = null
|
||||||
|
outlinePath = null
|
||||||
|
roundRectOutlineSize = Size.Unspecified
|
||||||
|
roundRectOutlineTopLeft = Offset.Zero
|
||||||
|
roundRectCornerRadius = 0f
|
||||||
|
outlineDirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun setPathOutline(path: Path) {
|
||||||
|
resetOutlineParams()
|
||||||
|
this.outlinePath = path
|
||||||
|
configureOutlineAndClip()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun setRoundRectOutline(topLeft: Offset, size: Size, cornerRadius: Float) {
|
||||||
|
if (this.roundRectOutlineTopLeft != topLeft ||
|
||||||
|
this.roundRectOutlineSize != size ||
|
||||||
|
this.roundRectCornerRadius != cornerRadius ||
|
||||||
|
this.outlinePath != null
|
||||||
|
) {
|
||||||
|
resetOutlineParams()
|
||||||
|
this.roundRectOutlineTopLeft = topLeft
|
||||||
|
this.roundRectOutlineSize = size
|
||||||
|
this.roundRectCornerRadius = cornerRadius
|
||||||
|
configureOutlineAndClip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun setRectOutline(topLeft: Offset, size: Size) {
|
||||||
|
setRoundRectOutline(topLeft, size, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var rotationX: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.rotationX = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var rotationY: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.rotationY = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var rotationZ: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.rotationZ = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var cameraDistance: Float = DefaultCameraDistance
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.cameraDistance = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var clip: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field != value) {
|
||||||
|
field = value
|
||||||
|
renderNode.clip = value
|
||||||
|
outlineDirty = true
|
||||||
|
configureOutlineAndClip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var renderEffect: RenderEffect? = null
|
||||||
|
set(value) {
|
||||||
|
if (field != value) { field = value; renderNode.renderEffect = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
actual var isReleased: Boolean = false
|
||||||
|
internal set
|
||||||
|
|
||||||
|
actual fun setOutsets(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
// Outsets (blur/shadow expansion) not modelled on this port — see header.
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun record(
|
||||||
|
density: Density,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
size: IntSize,
|
||||||
|
block: DrawScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
this.size = size
|
||||||
|
renderNode.record(density, layoutDirection, size, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun draw(canvas: Canvas, parentLayer: GraphicsLayer?) {
|
||||||
|
if (isReleased) return
|
||||||
|
configureOutlineAndClip()
|
||||||
|
renderNode.drawInto(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(androidx.compose.ui.InternalComposeUiApi::class)
|
||||||
|
private fun configureOutlineAndClip() {
|
||||||
|
if (!outlineDirty) return
|
||||||
|
val outlineIsNeeded = clip || shadowElevation > 0f
|
||||||
|
if (!outlineIsNeeded) {
|
||||||
|
renderNode.clip = false
|
||||||
|
renderNode.setClipPath(null)
|
||||||
|
} else {
|
||||||
|
renderNode.clip = clip
|
||||||
|
when (val tmpOutline = outline) {
|
||||||
|
is Outline.Rectangle -> renderNode.setClipRect(
|
||||||
|
tmpOutline.rect.left, tmpOutline.rect.top,
|
||||||
|
tmpOutline.rect.right, tmpOutline.rect.bottom,
|
||||||
|
)
|
||||||
|
is Outline.Rounded -> renderNode.setClipRRect(
|
||||||
|
tmpOutline.roundRect.left, tmpOutline.roundRect.top,
|
||||||
|
tmpOutline.roundRect.right, tmpOutline.roundRect.bottom,
|
||||||
|
floatArrayOf(
|
||||||
|
tmpOutline.roundRect.topLeftCornerRadius.x,
|
||||||
|
tmpOutline.roundRect.topLeftCornerRadius.y,
|
||||||
|
tmpOutline.roundRect.topRightCornerRadius.x,
|
||||||
|
tmpOutline.roundRect.topRightCornerRadius.y,
|
||||||
|
tmpOutline.roundRect.bottomRightCornerRadius.x,
|
||||||
|
tmpOutline.roundRect.bottomRightCornerRadius.y,
|
||||||
|
tmpOutline.roundRect.bottomLeftCornerRadius.x,
|
||||||
|
tmpOutline.roundRect.bottomLeftCornerRadius.y,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
is Outline.Generic -> renderNode.setClipPath(tmpOutline.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outlineDirty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> resolveOutlinePosition(block: (Offset, Size) -> T): T {
|
||||||
|
val layerSize = this.size.toSize()
|
||||||
|
val rRectTopLeft = roundRectOutlineTopLeft
|
||||||
|
val rRectSize = roundRectOutlineSize
|
||||||
|
val outlineSize = if (rRectSize.isUnspecified) layerSize else rRectSize
|
||||||
|
return block(rRectTopLeft, outlineSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun release() {
|
||||||
|
if (!isReleased) {
|
||||||
|
isReleased = true
|
||||||
|
renderNode.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors upstream SkiaGraphicsLayer.toImageBitmap: render the layer into a fresh
|
||||||
|
// bitmap of its own size. The offscreen canvas is flushed via NativeFinishableCanvas
|
||||||
|
// (SDL batches geometry; the backing render-target texture must be committed before
|
||||||
|
// it's read) — Skia's finish() is a no-op.
|
||||||
|
actual suspend fun toImageBitmap(): ImageBitmap {
|
||||||
|
val bitmap = ImageBitmap(size.width, size.height)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
draw(canvas, null)
|
||||||
|
(canvas as? com.compose.sdl.graphics.NativeFinishableCanvas)?.finish()
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import kotlin.experimental.ExperimentalNativeApi
|
|||||||
// MARK: SDL3 renderer per-OS default
|
// MARK: SDL3 renderer per-OS default
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
fun rendererPreferredGpuMode(): GpuMode {
|
actual fun rendererPreferredGpuMode(): GpuMode {
|
||||||
@OptIn(ExperimentalNativeApi::class)
|
@OptIn(ExperimentalNativeApi::class)
|
||||||
return when (Platform.osFamily) {
|
return when (Platform.osFamily) {
|
||||||
OsFamily.MACOSX -> GpuMode.Sdl3.Metal
|
OsFamily.MACOSX -> GpuMode.Sdl3.Metal
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import com.compose.sdl.renderer.sdl.Sdl3RenderBackend
|
|||||||
renderer modules expose createRenderBackend / rendererPreferredGpuMode
|
renderer modules expose createRenderBackend / rendererPreferredGpuMode
|
||||||
with identical signatures in this package, and the build includes exactly
|
with identical signatures in this package, and the build includes exactly
|
||||||
one of them per target. Rejects Skia.* since this module has no Skiko. */
|
one of them per target. Rejects Skia.* since this module has no Skiko. */
|
||||||
fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
actual fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
||||||
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||||
if (vResolved is GpuMode.Skia) {
|
if (vResolved is GpuMode.Skia) {
|
||||||
error("$vResolved isn't available in this build — Skiko isn't linked. " +
|
error("$vResolved isn't available in this build — Skiko isn't linked. " +
|
||||||
|
|||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.geometry.isUnspecified
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Canvas
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.Matrix
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Paint
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.RenderEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.layer.DefaultCameraDistance
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: DeferredRenderNode — replay-the-block NativeRenderNode
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/*
|
||||||
|
The renderer-agnostic [NativeRenderNode] both renderers use today. It does NOT
|
||||||
|
cache rasterised output: [record] stores the draw block; [drawInto] REPLAYS it
|
||||||
|
against the target canvas under the node's transform / clip / shadow / alpha. This
|
||||||
|
is behaviour-identical to the pre-refactor paths it replaces — the old
|
||||||
|
GraphicsLayer.native lambda-replay AND ProjectOwnedLayer's transform/shadow/clip —
|
||||||
|
now unified in one place behind the RenderNode seam.
|
||||||
|
|
||||||
|
It is the correct-but-unoptimised node. The perf win (stop re-tessellating on
|
||||||
|
replay) comes from swapping the per-renderer createNativeRenderNode actual to a
|
||||||
|
caching node — skiko RenderNode (a real display list) on Skia, an offscreen /
|
||||||
|
cached-geometry display list on SDL — WITHOUT touching GraphicsLayer. See
|
||||||
|
RENDERER.md §4 (Phase 2 = that swap).
|
||||||
|
|
||||||
|
Transform is applied via [prepareLayerTransformationMatrix] + canvas.concat, the
|
||||||
|
SAME matrix GraphicsLayerOwnerLayer uses for hit-testing (mapOffset), so draw and
|
||||||
|
hit-test provably agree. Shadow + rounded/shape clip go through the project canvas
|
||||||
|
interfaces (NativeShadowCanvas / NativeShapeClipCanvas), exactly as the old
|
||||||
|
ProjectOwnedLayer did.
|
||||||
|
*/
|
||||||
|
internal class DeferredRenderNode : NativeRenderNode {
|
||||||
|
|
||||||
|
override var topLeft: IntOffset = IntOffset.Zero
|
||||||
|
override var size: IntSize = IntSize.Zero
|
||||||
|
override var pivot: Offset = Offset.Unspecified
|
||||||
|
override var alpha: Float = 1f
|
||||||
|
override var scaleX: Float = 1f
|
||||||
|
override var scaleY: Float = 1f
|
||||||
|
override var translationX: Float = 0f
|
||||||
|
override var translationY: Float = 0f
|
||||||
|
override var rotationX: Float = 0f
|
||||||
|
override var rotationY: Float = 0f
|
||||||
|
override var rotationZ: Float = 0f
|
||||||
|
override var cameraDistance: Float = DefaultCameraDistance
|
||||||
|
override var shadowElevation: Float = 0f
|
||||||
|
override var ambientShadowColor: Color = Color.Black
|
||||||
|
override var spotShadowColor: Color = Color.Black
|
||||||
|
override var blendMode: BlendMode = BlendMode.SrcOver
|
||||||
|
override var colorFilter: ColorFilter? = null
|
||||||
|
override var renderEffect: RenderEffect? = null
|
||||||
|
override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
|
||||||
|
override var clip: Boolean = false
|
||||||
|
|
||||||
|
// Outline pushed by GraphicsLayer.configureOutlineAndClip whenever clip OR a
|
||||||
|
// shadow is present — so it drives BOTH the content clip (when clip=true) and
|
||||||
|
// the drop-shadow silhouette (when shadowElevation>0, even with clip=false).
|
||||||
|
private var clipOutline: Outline? = null
|
||||||
|
|
||||||
|
override fun setClipRect(left: Float, top: Float, right: Float, bottom: Float) {
|
||||||
|
clipOutline = Outline.Rectangle(Rect(left, top, right, bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setClipRRect(left: Float, top: Float, right: Float, bottom: Float, radii: FloatArray) {
|
||||||
|
clipOutline = Outline.Rounded(
|
||||||
|
RoundRect(
|
||||||
|
left = left, top = top, right = right, bottom = bottom,
|
||||||
|
topLeftCornerRadius = CornerRadius(radii[0], radii[1]),
|
||||||
|
topRightCornerRadius = CornerRadius(radii[2], radii[3]),
|
||||||
|
bottomRightCornerRadius = CornerRadius(radii[4], radii[5]),
|
||||||
|
bottomLeftCornerRadius = CornerRadius(radii[6], radii[7]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setClipPath(path: Path?) {
|
||||||
|
clipOutline = if (path == null) null else Outline.Generic(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Record — store the block (deferred; replayed in drawInto).
|
||||||
|
|
||||||
|
private var recordedBlock: (DrawScope.() -> Unit)? = null
|
||||||
|
private var recordedDensity: Density = Density(1f)
|
||||||
|
private var recordedLayoutDirection: LayoutDirection = LayoutDirection.Ltr
|
||||||
|
private val drawScope = CanvasDrawScope()
|
||||||
|
|
||||||
|
override fun record(
|
||||||
|
density: Density,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
size: IntSize,
|
||||||
|
block: DrawScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
recordedDensity = density
|
||||||
|
recordedLayoutDirection = layoutDirection
|
||||||
|
this.size = size
|
||||||
|
recordedBlock = block
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun drawInto(canvas: Canvas) {
|
||||||
|
val block = recordedBlock ?: return
|
||||||
|
val w = size.width.toFloat()
|
||||||
|
val h = size.height.toFloat()
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(topLeft.x.toFloat(), topLeft.y.toFloat())
|
||||||
|
|
||||||
|
// Transform about the pivot (default centre) — via the SAME matrix used for
|
||||||
|
// hit-testing, so draw and hit-test agree.
|
||||||
|
val hasTransform = scaleX != 1f || scaleY != 1f || rotationZ != 0f ||
|
||||||
|
rotationX != 0f || rotationY != 0f || translationX != 0f || translationY != 0f
|
||||||
|
if (hasTransform && w > 0f && h > 0f) {
|
||||||
|
val pivotX = if (pivot.isUnspecified) w / 2f else pivot.x
|
||||||
|
val pivotY = if (pivot.isUnspecified) h / 2f else pivot.y
|
||||||
|
val matrix = Matrix()
|
||||||
|
prepareLayerTransformationMatrix(
|
||||||
|
matrix = matrix,
|
||||||
|
pivotX = pivotX, pivotY = pivotY,
|
||||||
|
translationX = translationX, translationY = translationY,
|
||||||
|
rotationX = rotationX, rotationY = rotationY, rotationZ = rotationZ,
|
||||||
|
scaleX = scaleX, scaleY = scaleY, cameraDistance = cameraDistance,
|
||||||
|
)
|
||||||
|
canvas.concat(matrix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop shadow — layer-local, BEFORE the clip (shadow lives outside bounds).
|
||||||
|
if (shadowElevation > 0f && w > 0f && h > 0f) {
|
||||||
|
val outline = clipOutline
|
||||||
|
if (outline != null) {
|
||||||
|
(canvas as? NativeShadowCanvas)
|
||||||
|
?.drawDropShadow(outline, shadowElevation, ambientShadowColor, spotShadowColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content clip.
|
||||||
|
if (clip && w > 0f && h > 0f) applyClip(canvas)
|
||||||
|
|
||||||
|
// Offscreen for alpha (behaviour-preserving: the old paths saveLayer'd on
|
||||||
|
// alpha<1). ModulateAlpha suppresses the offscreen (alpha folds per-op) — but
|
||||||
|
// our DrawScope has no per-op multiplier yet, so it still composites here.
|
||||||
|
val needsAlphaLayer = alpha < 1f && w > 0f && h > 0f
|
||||||
|
if (needsAlphaLayer) {
|
||||||
|
canvas.saveLayer(
|
||||||
|
Rect(0f, 0f, w, h),
|
||||||
|
Paint().apply { alpha = this@DeferredRenderNode.alpha },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawScope.draw(recordedDensity, recordedLayoutDirection, canvas, Size(w, h), block)
|
||||||
|
|
||||||
|
if (needsAlphaLayer) canvas.restore()
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyClip(canvas: Canvas) {
|
||||||
|
when (val outline = clipOutline) {
|
||||||
|
is Outline.Rectangle -> canvas.clipRect(
|
||||||
|
outline.rect.left, outline.rect.top, outline.rect.right, outline.rect.bottom,
|
||||||
|
)
|
||||||
|
is Outline.Rounded -> {
|
||||||
|
// Skia clips rounded natively via clipPath; the SDL canvas has no
|
||||||
|
// path-clip primitive, so it implements NativeShapeClipCanvas and clips
|
||||||
|
// the rounded outline via an offscreen mask (preferred over clipPath,
|
||||||
|
// which falls back to the bounding rect there and left rounded state
|
||||||
|
// layers looking square).
|
||||||
|
val shapeClip = canvas as? NativeShapeClipCanvas
|
||||||
|
if (shapeClip != null) {
|
||||||
|
shapeClip.clipRoundRect(outline.roundRect)
|
||||||
|
} else {
|
||||||
|
val path = Path().apply { addRoundRect(outline.roundRect) }
|
||||||
|
canvas.clipPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Outline.Generic -> canvas.clipPath(outline.path)
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
recordedBlock = null
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.GraphicsContext
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: createGraphicsContext — SDL3 renderer actual
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* The SDL leg uses the project record/replay GraphicsContext (backed by the SDL
|
||||||
|
NativeRenderNode via createProjectGraphicsLayer). This is the permanent SDL home. */
|
||||||
|
internal actual fun createGraphicsContext(): GraphicsContext = ProjectGraphicsContext()
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: GraphicsLayer factory — native actuals
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
internal fun createProjectGraphicsLayer(): GraphicsLayer =
|
||||||
|
GraphicsLayer(createNativeRenderNode(NativeRenderNodeContext()))
|
||||||
|
|
||||||
|
internal fun releaseProjectGraphicsLayer(inLayer: GraphicsLayer) {
|
||||||
|
inLayer.release()
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Canvas
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.RenderEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: NativeRenderNode — the renderer-agnostic retained display-list node
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one genuinely renderer-specific piece of the retained-layer engine. This is
|
||||||
|
* the [GraphicsLayer] façade's backing node — the analogue of skiko's
|
||||||
|
* `org.jetbrains.skiko.node.RenderNode`. [GraphicsLayer] (our copy-edit of upstream
|
||||||
|
* `SkiaGraphicsLayer.skiko.kt`) mirrors every visual property onto this node, calls
|
||||||
|
* [record] to capture the layer's drawing once, and [drawInto] to replay it every
|
||||||
|
* frame with the transform / clip / shadow / compositing applied — WITHOUT
|
||||||
|
* re-recording.
|
||||||
|
*
|
||||||
|
* Property contract mirrors skiko `RenderNode` (translation/scale/rotation/pivot/
|
||||||
|
* alpha/shadow/clip/…), so the Skia actual wraps skiko `RenderNode` almost verbatim
|
||||||
|
* and the SDL actual (`SdlRenderNode`) implements the same contract.
|
||||||
|
*
|
||||||
|
* [record]/[drawInto] are higher-level than skiko's `beginRecording()`/`drawInto()`
|
||||||
|
* so BOTH a display-list impl (Skia: record into a skiko RenderNode; SDL later: a
|
||||||
|
* cached-geometry / offscreen display list) AND the current deferred-replay impl
|
||||||
|
* (SDL now: store the block, replay it under the transform) satisfy them.
|
||||||
|
*/
|
||||||
|
internal interface NativeRenderNode {
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Geometry — where the recorded content sits and its transform origin.
|
||||||
|
|
||||||
|
var topLeft: IntOffset
|
||||||
|
var size: IntSize
|
||||||
|
var pivot: Offset
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Transform — applied at replay, no re-record (matches skiko RenderNode).
|
||||||
|
|
||||||
|
var alpha: Float
|
||||||
|
var scaleX: Float
|
||||||
|
var scaleY: Float
|
||||||
|
var translationX: Float
|
||||||
|
var translationY: Float
|
||||||
|
var rotationX: Float
|
||||||
|
var rotationY: Float
|
||||||
|
var rotationZ: Float
|
||||||
|
var cameraDistance: Float
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Elevation shadow.
|
||||||
|
|
||||||
|
var shadowElevation: Float
|
||||||
|
var ambientShadowColor: Color
|
||||||
|
var spotShadowColor: Color
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Compositing — decides an offscreen (requiresLayer) and how content composites.
|
||||||
|
|
||||||
|
var blendMode: BlendMode
|
||||||
|
var colorFilter: ColorFilter?
|
||||||
|
var renderEffect: RenderEffect?
|
||||||
|
var compositingStrategy: CompositingStrategy
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Clip — outline pushed onto the node; rect / rounded-rect / generic path.
|
||||||
|
|
||||||
|
var clip: Boolean
|
||||||
|
fun setClipRect(left: Float, top: Float, right: Float, bottom: Float)
|
||||||
|
fun setClipRRect(left: Float, top: Float, right: Float, bottom: Float, radii: FloatArray)
|
||||||
|
fun setClipPath(path: Path?)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Record / replay.
|
||||||
|
|
||||||
|
/** Capture the layer content once; re-called only when the layer is dirty. */
|
||||||
|
fun record(density: Density, layoutDirection: LayoutDirection, size: IntSize, block: DrawScope.() -> Unit)
|
||||||
|
|
||||||
|
/** Replay the recorded content onto [canvas], applying this node's transform. */
|
||||||
|
fun drawInto(canvas: Canvas)
|
||||||
|
|
||||||
|
/** Release native resources (textures / picture / display list). */
|
||||||
|
fun close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-window context shared by the [NativeRenderNode]s of one composition — the
|
||||||
|
* analogue of skiko's `RenderNodeContext`. Minimal today; the SDL caching impl will
|
||||||
|
* grow it to carry the renderer handle it needs to allocate offscreen targets.
|
||||||
|
*/
|
||||||
|
internal class NativeRenderNodeContext(
|
||||||
|
val measureDrawBounds: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import com.compose.sdl.renderer.sdl.SdlDisplayListRenderNode
|
||||||
|
import com.compose.sdl.renderer.sdl.SdlRenderNode
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import kotlinx.cinterop.toKString
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: createNativeRenderNode — SDL3 renderer actual
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* CDN_LAYERCACHE selects the retained node. The DEFAULT is now the cached-geometry
|
||||||
|
node (Phase 4): it records each leaf's tessellated geometry + plain text runs ONCE
|
||||||
|
and replays them under the layer transform — crisp/bit-exact at any transform, no
|
||||||
|
render-target state, deterministic. A full 57-screen geo-vs-block-replay sweep is
|
||||||
|
clean (55 at 0.000%; GraphicsLayer/ModShortcuts <0.13% cosmetic rotated-edge AA)
|
||||||
|
and the former Carousel/Pickers diffs are gone, so it graduates from opt-in to
|
||||||
|
default. See RENDERER.md §4b/§13.
|
||||||
|
|
||||||
|
Escape hatches (env override):
|
||||||
|
off / defer / 0 → DeferredRenderNode (replay the block every frame; no caching —
|
||||||
|
the previous default, kept as a fallback)
|
||||||
|
1 / texture → SdlRenderNode (offscreen-texture cache — legacy; fast for
|
||||||
|
static leaves but timing-nondeterministic
|
||||||
|
on complex screens, so not the default)
|
||||||
|
* → SdlDisplayListRenderNode (geo — the default) */
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
private val layerCacheMode: String by lazy {
|
||||||
|
platform.posix.getenv("CDN_LAYERCACHE")?.toKString() ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun createNativeRenderNode(context: NativeRenderNodeContext): NativeRenderNode =
|
||||||
|
when (layerCacheMode) {
|
||||||
|
"off", "defer", "0" -> DeferredRenderNode()
|
||||||
|
"1", "texture" -> SdlRenderNode()
|
||||||
|
else -> SdlDisplayListRenderNode()
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.compose.sdl.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.GraphicsContext
|
||||||
|
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: ProjectGraphicsContext — SDL leg (record/replay GraphicsLayer)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/* The SDL leg's GraphicsContext: creates/releases the project record/replay
|
||||||
|
GraphicsLayer (backed by the SDL NativeRenderNode). GraphicsLayer's `expect class`
|
||||||
|
hides its constructor/release from common code, so layer creation/release hop
|
||||||
|
through the [createProjectGraphicsLayer] / [releaseProjectGraphicsLayer] factories
|
||||||
|
(co-located here). `shadowContext` keeps the GraphicsContext interface default. */
|
||||||
|
internal class ProjectGraphicsContext : GraphicsContext {
|
||||||
|
override fun createGraphicsLayer(): GraphicsLayer = createProjectGraphicsLayer()
|
||||||
|
override fun releaseGraphicsLayer(layer: GraphicsLayer) = releaseProjectGraphicsLayer(layer)
|
||||||
|
}
|
||||||
+443
-37
@@ -1,5 +1,6 @@
|
|||||||
package com.compose.sdl.renderer.sdl
|
package com.compose.sdl.renderer.sdl
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
import androidx.compose.ui.geometry.RoundRect
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.ui.graphics.BlendMode
|
|||||||
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||||
import androidx.compose.ui.graphics.drawscope.Fill
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import com.compose.sdl.graphics.a8
|
||||||
import com.compose.sdl.graphics.b8
|
import com.compose.sdl.graphics.b8
|
||||||
import com.compose.sdl.graphics.g8
|
import com.compose.sdl.graphics.g8
|
||||||
import com.compose.sdl.graphics.r8
|
import com.compose.sdl.graphics.r8
|
||||||
@@ -26,6 +28,7 @@ import com.compose.sdl.icons.IconFont
|
|||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
import sdl3.*
|
import sdl3.*
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.sqrt
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -69,17 +72,87 @@ internal class Sdl3Canvas(
|
|||||||
// Alpha8 mask) so the tint applied on blit colours it correctly.
|
// Alpha8 mask) so the tint applied on blit colours it correctly.
|
||||||
private val fOffscreenTexture: COpaquePointer? = null,
|
private val fOffscreenTexture: COpaquePointer? = null,
|
||||||
private val fForceWhite: Boolean = false,
|
private val fForceWhite: Boolean = false,
|
||||||
|
// Phase 4 capture mode: when set, this canvas RECORDS shape geometry into the list
|
||||||
|
// (via the scope's recordingSink) and touches NO GPU. Ops not yet capturable
|
||||||
|
// (text / image / clip / saveLayer / shadow) mark the list `unsupported` so the
|
||||||
|
// SdlDisplayListRenderNode falls back to a crisp block-replay for that leaf — so
|
||||||
|
// no un-captured op ever leaks to the screen. See SdlDisplayList.
|
||||||
|
private val fCaptureList: SdlDisplayList? = null,
|
||||||
) : Canvas,
|
) : Canvas,
|
||||||
com.compose.sdl.text.NativeTextCanvas,
|
com.compose.sdl.text.NativeTextCanvas,
|
||||||
com.compose.sdl.graphics.NativePainterCanvas,
|
com.compose.sdl.graphics.NativePainterCanvas,
|
||||||
com.compose.sdl.graphics.NativeShapeClipCanvas,
|
com.compose.sdl.graphics.NativeShapeClipCanvas,
|
||||||
com.compose.sdl.graphics.NativeShadowCanvas {
|
com.compose.sdl.graphics.NativeShadowCanvas,
|
||||||
|
com.compose.sdl.graphics.NativeFinishableCanvas {
|
||||||
|
|
||||||
// The tessellating scope. It emits geometry in the canvas's user space; the
|
// The tessellating scope. It emits geometry in the canvas's user space; the
|
||||||
// canvas hands it the current affine each draw (setMatrix) so scale / rotate /
|
// canvas hands it the current affine each draw (setMatrix) so scale / rotate /
|
||||||
// translate reach the vertices.
|
// translate reach the vertices.
|
||||||
private val fScope = Sdl3DrawScope(fRenderer, 0f, 0f, fSize)
|
private val fScope = Sdl3DrawScope(fRenderer, 0f, 0f, fSize)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Capture mode routes the scope's geometry to the display list instead of the GPU.
|
||||||
|
if (fCaptureList != null) fScope.recordingSink = fCaptureList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture-mode gate for ops we can't record yet: flag the list unsupported (the
|
||||||
|
// node will replay the block instead) and skip the GPU work. Returns true if the
|
||||||
|
// caller should return early. No-op (false) outside capture mode.
|
||||||
|
private fun captureUnsupported(): Boolean {
|
||||||
|
val list = fCaptureList ?: return false
|
||||||
|
list.unsupported = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replay a captured display list into this (real) canvas under its CURRENT
|
||||||
|
// transform, in order (z-order preserved). Geometry batches re-emit their
|
||||||
|
// layer-local vertices through the affine; text runs flush pending geometry then
|
||||||
|
// re-look-up + blit at the transformed origin (glyph size stays logical). Used by
|
||||||
|
// SdlDisplayListRenderNode.drawInto.
|
||||||
|
internal fun replayDisplayList(list: SdlDisplayList) {
|
||||||
|
fScope.flush()
|
||||||
|
fScope.setMatrix(fMa, fMb, fMc, fMd, fMe, fMf)
|
||||||
|
for (i in list.commands.indices) {
|
||||||
|
when (val cmd = list.commands[i]) {
|
||||||
|
is GeometryBatch -> fScope.replayBatch(cmd.vertexData, cmd.vertexCount)
|
||||||
|
is TextRun -> {
|
||||||
|
fScope.flush() // submit geometry before this run (z-order)
|
||||||
|
// Transform the run ORIGIN by the affine; keep glyph size logical
|
||||||
|
// (matches the immediate path: layer scale reaches position, not size).
|
||||||
|
fTextRenderer?.blitRun(
|
||||||
|
inFontFamily = cmd.fontFamily,
|
||||||
|
inText = cmd.text,
|
||||||
|
inFontSize = cmd.fontSizePx,
|
||||||
|
inFontVariations = cmd.variations,
|
||||||
|
inStyle = cmd.style,
|
||||||
|
inDeviceX = mapX(cmd.x, cmd.y),
|
||||||
|
inDeviceY = mapY(cmd.x, cmd.y),
|
||||||
|
inLogW = cmd.w,
|
||||||
|
inLogH = cmd.h,
|
||||||
|
inColor = androidx.compose.ui.graphics.Color(cmd.colorArgb),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is IconRun -> {
|
||||||
|
fScope.flush() // submit geometry before this glyph (z-order)
|
||||||
|
// Map the box origin through the affine; box size stays logical (the
|
||||||
|
// glyph centres in it — layer scale reaches position, not glyph size).
|
||||||
|
fTextRenderer?.blitIconRun(
|
||||||
|
inFontFamily = cmd.fontFamily,
|
||||||
|
inText = cmd.text,
|
||||||
|
inFontSize = cmd.fontSizePx,
|
||||||
|
inFontVariations = cmd.variations,
|
||||||
|
inDeviceBoxX = mapX(cmd.boxX, cmd.boxY),
|
||||||
|
inDeviceBoxY = mapY(cmd.boxX, cmd.boxY),
|
||||||
|
inBoxW = cmd.boxW,
|
||||||
|
inBoxH = cmd.boxH,
|
||||||
|
inColor = androidx.compose.ui.graphics.Color(cmd.colorArgb),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fScope.flush()
|
||||||
|
}
|
||||||
|
|
||||||
// Current user→screen affine (a,b,c,d,e,f): screen = (a*x+c*y+e, b*x+d*y+f).
|
// Current user→screen affine (a,b,c,d,e,f): screen = (a*x+c*y+e, b*x+d*y+f).
|
||||||
// Accumulates Canvas.translate/scale/rotate/concat; identity + a translate is
|
// Accumulates Canvas.translate/scale/rotate/concat; identity + a translate is
|
||||||
// the common layout-positioning case.
|
// the common layout-positioning case.
|
||||||
@@ -126,7 +199,7 @@ internal class Sdl3Canvas(
|
|||||||
// offscreen clip opened inside this save frame (see fClipLayers below).
|
// offscreen clip opened inside this save frame (see fClipLayers below).
|
||||||
private data class State(
|
private data class State(
|
||||||
val a: Float, val b: Float, val c: Float, val d: Float, val e: Float, val f: Float,
|
val a: Float, val b: Float, val c: Float, val d: Float, val e: Float, val f: Float,
|
||||||
val clip: IntArray?, val alpha: Float, val clipLayers: Int,
|
val clip: IntArray?, val alpha: Float, val clipLayers: Int, val pendingClips: Int,
|
||||||
)
|
)
|
||||||
private val fStack = ArrayDeque<State>()
|
private val fStack = ArrayDeque<State>()
|
||||||
|
|
||||||
@@ -146,9 +219,139 @@ internal class Sdl3Canvas(
|
|||||||
)
|
)
|
||||||
private val fClipLayers = ArrayDeque<OffscreenClip>()
|
private val fClipLayers = ArrayDeque<OffscreenClip>()
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Lazy rounded clips. Opening an offscreen mask per clip is the SINGLE
|
||||||
|
// most expensive thing this canvas does (two render-target switches +
|
||||||
|
// clear + corner cut + feather + composite) - and the dominant Compose
|
||||||
|
// idiom `Modifier.clip(CircleShape).background(...)` never needs one:
|
||||||
|
// the background is a full-cover rect drawable AS the rounded shape, and
|
||||||
|
// content wholly inside the rounded region needs no mask at all. So
|
||||||
|
// clipRoundRect only RECORDS the clip; each draw either proves itself
|
||||||
|
// safe (containment / cover) or realizes the offscreen on demand.
|
||||||
|
|
||||||
|
private class PendingRoundClip(
|
||||||
|
val deviceRound: RoundRect, // device-space outline (clamped radii)
|
||||||
|
val localRound: RoundRect, // as passed in (for cover -> shape-fill emission)
|
||||||
|
val region: IntArray, // fClip intersect bbox at push time
|
||||||
|
val bbox: IntArray, // device bbox of the outline
|
||||||
|
val prevClip: IntArray?, // fClip before this clip was pushed
|
||||||
|
// Affine snapshot at push - cover -> fill re-emits in LOCAL space and
|
||||||
|
// must bail if the transform moved since.
|
||||||
|
val ma: Float, val mb: Float, val mc: Float, val md: Float, val me: Float, val mf: Float,
|
||||||
|
// fStack.size when the clip was pushed: a lazily-realized mask must be
|
||||||
|
// composited by the save-frame that OWNS the clip, not by whatever
|
||||||
|
// inner frame happened to trigger realization (a press draws the state
|
||||||
|
// layer and the ripple in separate inner clipRect{} frames - the first
|
||||||
|
// realize must not let the inner restore close the mask, or the second
|
||||||
|
// draw runs unmasked: the "square on click" artifact).
|
||||||
|
val saveDepth: Int,
|
||||||
|
)
|
||||||
|
private val fPendingClips = ArrayDeque<PendingRoundClip>()
|
||||||
|
|
||||||
|
/* Point-in-round-rect (device space) - inside the rect and, within a
|
||||||
|
corner cut square, inside its ellipse. */
|
||||||
|
private fun roundRectContains(inRR: RoundRect, inX: Float, inY: Float): Boolean {
|
||||||
|
if (inX < inRR.left || inX > inRR.right || inY < inRR.top || inY > inRR.bottom) return false
|
||||||
|
fun corner(inRx: Float, inRy: Float, inDx: Float, inDy: Float): Boolean {
|
||||||
|
if (inRx <= 0f || inRy <= 0f) return true
|
||||||
|
val vNx = inDx / inRx; val vNy = inDy / inRy
|
||||||
|
return vNx * vNx + vNy * vNy <= 1f
|
||||||
|
}
|
||||||
|
val vTl = inRR.topLeftCornerRadius; val vTr = inRR.topRightCornerRadius
|
||||||
|
val vBr = inRR.bottomRightCornerRadius; val vBl = inRR.bottomLeftCornerRadius
|
||||||
|
if (inX < inRR.left + vTl.x && inY < inRR.top + vTl.y)
|
||||||
|
return corner(vTl.x, vTl.y, inX - (inRR.left + vTl.x), inY - (inRR.top + vTl.y))
|
||||||
|
if (inX > inRR.right - vTr.x && inY < inRR.top + vTr.y)
|
||||||
|
return corner(vTr.x, vTr.y, inX - (inRR.right - vTr.x), inY - (inRR.top + vTr.y))
|
||||||
|
if (inX > inRR.right - vBr.x && inY > inRR.bottom - vBr.y)
|
||||||
|
return corner(vBr.x, vBr.y, inX - (inRR.right - vBr.x), inY - (inRR.bottom - vBr.y))
|
||||||
|
if (inX < inRR.left + vBl.x && inY > inRR.bottom - vBl.y)
|
||||||
|
return corner(vBl.x, vBl.y, inX - (inRR.left + vBl.x), inY - (inRR.bottom - vBl.y))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A device-space aabb is inside the round rect iff its four corners are. */
|
||||||
|
private fun roundRectContainsAabb(inRR: RoundRect, inL: Float, inT: Float, inR: Float, inB: Float): Boolean =
|
||||||
|
roundRectContains(inRR, inL, inT) && roundRectContains(inRR, inR, inT) &&
|
||||||
|
roundRectContains(inRR, inR, inB) && roundRectContains(inRR, inL, inB)
|
||||||
|
|
||||||
|
/* Device aabb of a LOCAL rect under the current affine. */
|
||||||
|
private fun deviceAabb(inL: Float, inT: Float, inR: Float, inB: Float): FloatArray {
|
||||||
|
val vX0 = mapX(inL, inT); val vX1 = mapX(inR, inT); val vX2 = mapX(inR, inB); val vX3 = mapX(inL, inB)
|
||||||
|
val vY0 = mapY(inL, inT); val vY1 = mapY(inR, inT); val vY2 = mapY(inR, inB); val vY3 = mapY(inL, inB)
|
||||||
|
return floatArrayOf(
|
||||||
|
minOf(vX0, vX1, vX2, vX3), minOf(vY0, vY1, vY2, vY3),
|
||||||
|
maxOf(vX0, vX1, vX2, vX3), maxOf(vY0, vY1, vY2, vY3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the LOCAL-space aabb stays inside every pending rounded clip, drawing
|
||||||
|
it under the already-active rect clip is correct - no mask needed.
|
||||||
|
Otherwise realize the pending masks before the draw proceeds. */
|
||||||
|
private fun admitDraw(inL: Float, inT: Float, inR: Float, inB: Float) {
|
||||||
|
if (fPendingClips.isEmpty()) return
|
||||||
|
val vBox = deviceAabb(inL, inT, inR, inB)
|
||||||
|
for (vPending in fPendingClips) {
|
||||||
|
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
|
||||||
|
realizePendingClips()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert pending clips into real offscreen mask layers (outermost first).
|
||||||
|
|
||||||
|
Each clip realizes with its OWN push-time region (pending.region =
|
||||||
|
enclosing-clip-at-push ∩ bbox), NOT the current fClip: a lazily-realized
|
||||||
|
clip may fire arbitrarily deep inside descendants whose own clips have
|
||||||
|
since narrowed fClip. Re-intersecting with that narrow clip shrank an
|
||||||
|
outer clip's offscreen to the innermost descendant's rect, so its
|
||||||
|
composite blitted back only that sliver and every sibling drawn into the
|
||||||
|
same offscreen was discarded (the TLS-chain regression: only the first
|
||||||
|
card + nothing below it, until a scroll shifted which card triggered
|
||||||
|
realization). The real (already-narrowed) drawing clip is preserved
|
||||||
|
across the loop and restored at the end, so subsequent draws into the
|
||||||
|
innermost target stay correctly clipped. */
|
||||||
|
private fun realizePendingClips() {
|
||||||
|
if (fPendingClips.isEmpty()) return
|
||||||
|
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||||
|
val vDrawClip = fClip
|
||||||
|
while (fPendingClips.isNotEmpty()) {
|
||||||
|
val vPending = fPendingClips.removeFirst()
|
||||||
|
fScope.flush()
|
||||||
|
val vRegion = vPending.region
|
||||||
|
val vTarget = fClipTargets?.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt())
|
||||||
|
if (vTarget == null || vRegion[2] <= vRegion[0] || vRegion[3] <= vRegion[1]) {
|
||||||
|
// Degrade to the rect clip that is already active.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
|
||||||
|
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
|
||||||
|
fClip = vRegion
|
||||||
|
applyClip()
|
||||||
|
clearRegion(vRegion)
|
||||||
|
com.compose.sdl.graphics.DrawStats.maskRealizations++
|
||||||
|
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPending.prevClip, vRegion, vPending.bbox, vPending.deviceRound))
|
||||||
|
// Attribute the realized mask to the save-frame that pushed the
|
||||||
|
// clip: inner frames entered since must NOT composite it on their
|
||||||
|
// restore - bump their thresholds past the new layer.
|
||||||
|
for (vI in vPending.saveDepth until fStack.size) {
|
||||||
|
val vState = fStack[vI]
|
||||||
|
if (vState.clipLayers < fClipLayers.size) {
|
||||||
|
fStack[vI] = vState.copy(clipLayers = fClipLayers.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore the real drawing clip (already narrowed by inner clipRects /
|
||||||
|
// scroll bounds) for draws into the now-active innermost target.
|
||||||
|
fClip = vDrawClip
|
||||||
|
applyClip()
|
||||||
|
}
|
||||||
|
|
||||||
// Flushes any pending batched geometry to SDL, then frees the scope's
|
// Flushes any pending batched geometry to SDL, then frees the scope's
|
||||||
// native buffer + clears the SDL clip. Call once per frame after the draw.
|
// native buffer + clears the SDL clip. Call once per frame after the draw.
|
||||||
fun finish() {
|
override fun finish() {
|
||||||
|
fPendingClips.clear()
|
||||||
fScope.release()
|
fScope.release()
|
||||||
// Safety: an unbalanced save/restore must never leave the renderer pointed
|
// Safety: an unbalanced save/restore must never leave the renderer pointed
|
||||||
// at a scratch target when the frame is presented.
|
// at a scratch target when the frame is presented.
|
||||||
@@ -166,11 +369,14 @@ internal class Sdl3Canvas(
|
|||||||
// Offscreen canvas: the outermost save (CanvasDrawScope brackets its draw with
|
// Offscreen canvas: the outermost save (CanvasDrawScope brackets its draw with
|
||||||
// save/restore) switches the SDL render target to the ImageBitmap's texture.
|
// save/restore) switches the SDL render target to the ImageBitmap's texture.
|
||||||
if (fOffscreenTexture != null && fStack.isEmpty()) beginOffscreen()
|
if (fOffscreenTexture != null && fStack.isEmpty()) beginOffscreen()
|
||||||
fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size))
|
fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size, fPendingClips.size))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restore() {
|
override fun restore() {
|
||||||
val vPrev = fStack.removeLastOrNull() ?: return
|
val vPrev = fStack.removeLastOrNull() ?: return
|
||||||
|
// Pending (never-realized) rounded clips opened inside this save frame:
|
||||||
|
// nothing was drawn through an offscreen for them - just drop them.
|
||||||
|
while (fPendingClips.size > vPrev.pendingClips) fPendingClips.removeLast()
|
||||||
// Composite + pop any offscreen rounded clips opened inside this save frame
|
// Composite + pop any offscreen rounded clips opened inside this save frame
|
||||||
// before restoring the plain state (each composite restores render target
|
// before restoring the plain state (each composite restores render target
|
||||||
// and SDL clip to what it was when the clip was opened).
|
// and SDL clip to what it was when the clip was opened).
|
||||||
@@ -208,6 +414,14 @@ internal class Sdl3Canvas(
|
|||||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||||
fSavedOffscreenTarget = SDL_GetRenderTarget(vRenderer)
|
fSavedOffscreenTarget = SDL_GetRenderTarget(vRenderer)
|
||||||
SDL_SetRenderTarget(vRenderer, vTex.reinterpret())
|
SDL_SetRenderTarget(vRenderer, vTex.reinterpret())
|
||||||
|
// Clear to transparent. Render-target textures aren't auto-cleared, and a
|
||||||
|
// retained-layer texture (SdlRenderNode) is REUSED across re-records — without
|
||||||
|
// this, a re-record ghosts the previous content, which showed up as multi-frame
|
||||||
|
// instability on churny screens (Pickers). Fresh textures happen to be zeroed
|
||||||
|
// so static leaves were fine, but reuse needs an explicit clear. RenderClear
|
||||||
|
// fills with the draw colour ignoring blend, so (0,0,0,0) = transparent.
|
||||||
|
SDL_SetRenderDrawColor(vRenderer, 0u, 0u, 0u, 0u)
|
||||||
|
SDL_RenderClear(vRenderer)
|
||||||
fClip = null
|
fClip = null
|
||||||
SDL_SetRenderClipRect(vRenderer, null)
|
SDL_SetRenderClipRect(vRenderer, null)
|
||||||
}
|
}
|
||||||
@@ -220,6 +434,7 @@ internal class Sdl3Canvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun saveLayer(bounds: Rect, paint: Paint) {
|
override fun saveLayer(bounds: Rect, paint: Paint) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
// No real offscreen buffer — SDL3 lacks a portable render-target-in-a-batched-scope
|
// No real offscreen buffer — SDL3 lacks a portable render-target-in-a-batched-scope
|
||||||
// primitive here. Instead, multiplicatively propagate the layer's alpha through
|
// primitive here. Instead, multiplicatively propagate the layer's alpha through
|
||||||
// every enclosed primitive: each draw() reads `fAlpha` and multiplies against
|
// every enclosed primitive: each draw() reads `fAlpha` and multiplies against
|
||||||
@@ -251,6 +466,7 @@ internal class Sdl3Canvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
|
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float, clipOp: ClipOp) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
if (clipOp == ClipOp.Difference) {
|
if (clipOp == ClipOp.Difference) {
|
||||||
clipRectDifference(left, top, right, bottom)
|
clipRectDifference(left, top, right, bottom)
|
||||||
return
|
return
|
||||||
@@ -267,6 +483,7 @@ internal class Sdl3Canvas(
|
|||||||
// treating Difference as intersect clipped the border TO the notch instead
|
// treating Difference as intersect clipped the border TO the notch instead
|
||||||
// (a floating line under the label, no outline anywhere else).
|
// (a floating line under the label, no outline anywhere else).
|
||||||
private fun clipRectDifference(inL: Float, inT: Float, inR: Float, inB: Float) {
|
private fun clipRectDifference(inL: Float, inT: Float, inR: Float, inB: Float) {
|
||||||
|
realizePendingClips()
|
||||||
val vDiff = mapRectAABB(inL, inT, inR, inB)
|
val vDiff = mapRectAABB(inL, inT, inR, inB)
|
||||||
val vRegion = fClip ?: intArrayOf(0, 0, fSize.width.toInt(), fSize.height.toInt())
|
val vRegion = fClip ?: intArrayOf(0, 0, fSize.width.toInt(), fSize.height.toInt())
|
||||||
if (fClipTargets == null || fSize.width < 1f || fSize.height < 1f ||
|
if (fClipTargets == null || fSize.width < 1f || fSize.height < 1f ||
|
||||||
@@ -290,6 +507,7 @@ internal class Sdl3Canvas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun clipPath(path: ComposePath, clipOp: ClipOp) {
|
override fun clipPath(path: ComposePath, clipOp: ClipOp) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
// SDL has only rectangular render clipping; approximate a path clip by
|
// SDL has only rectangular render clipping; approximate a path clip by
|
||||||
// its bounding box (TODO: stencil/mask for non-rect clips).
|
// its bounding box (TODO: stencil/mask for non-rect clips).
|
||||||
val vB = pathBounds(path) ?: return
|
val vB = pathBounds(path) ?: return
|
||||||
@@ -330,11 +548,20 @@ internal class Sdl3Canvas(
|
|||||||
// arbitrary paths keep the clipPath bbox fallback.
|
// arbitrary paths keep the clipPath bbox fallback.
|
||||||
|
|
||||||
override fun clipRoundRect(inRoundRect: RoundRect) {
|
override fun clipRoundRect(inRoundRect: RoundRect) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
|
// The mask is cut in DEVICE space (the bbox below goes through the
|
||||||
|
// affine), but the corner radii arrive in LOCAL user space — scale
|
||||||
|
// them by the affine too, or a graphicsLayer-scaled circle clip cuts
|
||||||
|
// with oversized corners and degenerates toward a lozenge (seen as
|
||||||
|
// diamond-shaped "popped" bubbles under scale 0.55). Column norms
|
||||||
|
// approximate per-axis scale; rotation already degrades to AABBs here.
|
||||||
|
val vScaleX = sqrt(fMa * fMa + fMb * fMb)
|
||||||
|
val vScaleY = sqrt(fMc * fMc + fMd * fMd)
|
||||||
val vMaxRadius = maxOf(
|
val vMaxRadius = maxOf(
|
||||||
inRoundRect.topLeftCornerRadius.x, inRoundRect.topLeftCornerRadius.y,
|
inRoundRect.topLeftCornerRadius.x * vScaleX, inRoundRect.topLeftCornerRadius.y * vScaleY,
|
||||||
inRoundRect.topRightCornerRadius.x, inRoundRect.topRightCornerRadius.y,
|
inRoundRect.topRightCornerRadius.x * vScaleX, inRoundRect.topRightCornerRadius.y * vScaleY,
|
||||||
inRoundRect.bottomRightCornerRadius.x, inRoundRect.bottomRightCornerRadius.y,
|
inRoundRect.bottomRightCornerRadius.x * vScaleX, inRoundRect.bottomRightCornerRadius.y * vScaleY,
|
||||||
inRoundRect.bottomLeftCornerRadius.x, inRoundRect.bottomLeftCornerRadius.y,
|
inRoundRect.bottomLeftCornerRadius.x * vScaleX, inRoundRect.bottomLeftCornerRadius.y * vScaleY,
|
||||||
)
|
)
|
||||||
// Effectively-square corners or no offscreen pool → plain rectangular clip.
|
// Effectively-square corners or no offscreen pool → plain rectangular clip.
|
||||||
if (vMaxRadius < 0.5f || fClipTargets == null || fSize.width < 1f || fSize.height < 1f) {
|
if (vMaxRadius < 0.5f || fClipTargets == null || fSize.width < 1f || fSize.height < 1f) {
|
||||||
@@ -343,6 +570,20 @@ internal class Sdl3Canvas(
|
|||||||
}
|
}
|
||||||
fScope.flush()
|
fScope.flush()
|
||||||
val vBbox = mapRectAABB(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
|
val vBbox = mapRectAABB(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
|
||||||
|
// Clamp to the DEVICE half-box: the bbox is int-truncated, so a scaled
|
||||||
|
// radius can exceed it by a fraction — opposite corner cuts then overlap
|
||||||
|
// and leave flat spots at N/E/S/W (a circle read as an octagon).
|
||||||
|
val vHalfW = (vBbox[2] - vBbox[0]) / 2f
|
||||||
|
val vHalfH = (vBbox[3] - vBbox[1]) / 2f
|
||||||
|
fun deviceRadius(inR: CornerRadius) =
|
||||||
|
CornerRadius(minOf(inR.x * vScaleX, vHalfW), minOf(inR.y * vScaleY, vHalfH))
|
||||||
|
val vDeviceRound = RoundRect(
|
||||||
|
vBbox[0].toFloat(), vBbox[1].toFloat(), vBbox[2].toFloat(), vBbox[3].toFloat(),
|
||||||
|
topLeftCornerRadius = deviceRadius(inRoundRect.topLeftCornerRadius),
|
||||||
|
topRightCornerRadius = deviceRadius(inRoundRect.topRightCornerRadius),
|
||||||
|
bottomRightCornerRadius = deviceRadius(inRoundRect.bottomRightCornerRadius),
|
||||||
|
bottomLeftCornerRadius = deviceRadius(inRoundRect.bottomLeftCornerRadius),
|
||||||
|
)
|
||||||
val vRegion = intersect(fClip, vBbox)
|
val vRegion = intersect(fClip, vBbox)
|
||||||
// Nothing visible: keep behaviour of a normal clip (cull) without an
|
// Nothing visible: keep behaviour of a normal clip (cull) without an
|
||||||
// offscreen pass. The enclosing save/restore restores the clip afterwards.
|
// offscreen pass. The enclosing save/restore restores the clip afterwards.
|
||||||
@@ -351,20 +592,18 @@ internal class Sdl3Canvas(
|
|||||||
applyClip()
|
applyClip()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val vTarget = fClipTargets.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt())
|
// LAZY: record only — the rect part of the clip applies immediately,
|
||||||
if (vTarget == null) {
|
// the offscreen mask is opened by the first draw that actually needs
|
||||||
// Couldn't allocate a scratch target — degrade to a rectangular clip.
|
// it (admitDraw / realizePendingClips). The common clip+background
|
||||||
clipRect(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
|
// idiom never does.
|
||||||
return
|
|
||||||
}
|
|
||||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
|
||||||
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
|
|
||||||
val vPrevClip = fClip
|
val vPrevClip = fClip
|
||||||
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
|
|
||||||
fClip = vRegion
|
fClip = vRegion
|
||||||
applyClip()
|
applyClip()
|
||||||
clearRegion(vRegion)
|
fPendingClips.addLast(PendingRoundClip(
|
||||||
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPrevClip, vRegion, vBbox, inRoundRect))
|
vDeviceRound, inRoundRect, vRegion, vBbox, vPrevClip,
|
||||||
|
fMa, fMb, fMc, fMd, fMe, fMf,
|
||||||
|
fStack.size,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pops the top offscreen clip: flush its subtree, cut the rounded corners out
|
// Pops the top offscreen clip: flush its subtree, cut the rounded corners out
|
||||||
@@ -427,22 +666,24 @@ internal class Sdl3Canvas(
|
|||||||
private fun zeroRoundRectCorners(inBbox: IntArray, inRoundRect: RoundRect) {
|
private fun zeroRoundRectCorners(inBbox: IntArray, inRoundRect: RoundRect) {
|
||||||
val vLeft = inBbox[0].toFloat(); val vTop = inBbox[1].toFloat()
|
val vLeft = inBbox[0].toFloat(); val vTop = inBbox[1].toFloat()
|
||||||
val vRight = inBbox[2].toFloat(); val vBottom = inBbox[3].toFloat()
|
val vRight = inBbox[2].toFloat(); val vBottom = inBbox[3].toFloat()
|
||||||
val vSeg = 12
|
|
||||||
val vTl = inRoundRect.topLeftCornerRadius
|
val vTl = inRoundRect.topLeftCornerRadius
|
||||||
val vTr = inRoundRect.topRightCornerRadius
|
val vTr = inRoundRect.topRightCornerRadius
|
||||||
val vBr = inRoundRect.bottomRightCornerRadius
|
val vBr = inRoundRect.bottomRightCornerRadius
|
||||||
val vBl = inRoundRect.bottomLeftCornerRadius
|
val vBl = inRoundRect.bottomLeftCornerRadius
|
||||||
// Upper bound: 4 corners × vSeg triangles × 3 vertices. Written straight
|
// Segments per quarter arc ~ every 4 device px along the arc, matching
|
||||||
// into the native SDL_Vertex buffer — the old ArrayList<Float> staging
|
// Sdl3DrawScope's density (~64/full circle at typical radii).
|
||||||
// boxed ~300 floats per clip layer per frame.
|
val vMaxR = maxOf(vTl.x, vTl.y, vTr.x, vTr.y, vBr.x, vBr.y, vBl.x, vBl.y)
|
||||||
fun cornerCount(inR: androidx.compose.ui.geometry.CornerRadius): Int =
|
val vSeg = ((vMaxR * 1.5708f) / 4f).toInt().coerceIn(8, 32)
|
||||||
if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3
|
fun cornerCount(inR: CornerRadius): Int = if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3
|
||||||
val vMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl)
|
val vFanMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl)
|
||||||
if (vMax == 0) return
|
if (vFanMax == 0) return
|
||||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||||
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
|
|
||||||
memScoped {
|
memScoped {
|
||||||
val vBuf = allocArray<SDL_Vertex>(vMax)
|
// ============
|
||||||
|
// Pass 1 — hard cut: zero everything outside each corner arc
|
||||||
|
// (triangle fan from the corner apex, blend NONE writes RGBA 0).
|
||||||
|
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
|
||||||
|
val vBuf = allocArray<SDL_Vertex>(vFanMax)
|
||||||
var vIdx = 0
|
var vIdx = 0
|
||||||
fun put(inX: Float, inY: Float) {
|
fun put(inX: Float, inY: Float) {
|
||||||
vBuf[vIdx].position.x = inX; vBuf[vIdx].position.y = inY
|
vBuf[vIdx].position.x = inX; vBuf[vIdx].position.y = inY
|
||||||
@@ -470,6 +711,51 @@ internal class Sdl3Canvas(
|
|||||||
cutout(vRight, vBottom, vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
|
cutout(vRight, vBottom, vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
|
||||||
cutout(vLeft, vBottom, vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
|
cutout(vLeft, vBottom, vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
|
||||||
if (vIdx > 0) SDL_RenderGeometry(vRenderer, null, vBuf, vIdx, null, 0)
|
if (vIdx > 0) SDL_RenderGeometry(vRenderer, null, vBuf, vIdx, null, 0)
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Pass 2 — feathered edge: a ~1.2px ring band straddling each arc
|
||||||
|
// whose vertex alpha ramps 1 → 0 outward, drawn with a custom
|
||||||
|
// "dst *= srcAlpha" blend. SDL_RenderGeometry has no antialiasing;
|
||||||
|
// without this the mask boundary is a hard polygon edge while the
|
||||||
|
// drawn shapes get Sdl3DrawScope's fringe AA (visibly rougher —
|
||||||
|
// scaled-down circle clips read as low-poly).
|
||||||
|
val vMul = SDL_ComposeCustomBlendMode(
|
||||||
|
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
|
||||||
|
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
|
||||||
|
)
|
||||||
|
SDL_SetRenderDrawBlendMode(vRenderer, vMul)
|
||||||
|
val vFeather = 1.2f
|
||||||
|
val vBandMax = 4 * vSeg * 6
|
||||||
|
val vBand = allocArray<SDL_Vertex>(vBandMax)
|
||||||
|
var vBIdx = 0
|
||||||
|
fun putBand(inX: Float, inY: Float, inKeep: Float) {
|
||||||
|
vBand[vBIdx].position.x = inX; vBand[vBIdx].position.y = inY
|
||||||
|
vBand[vBIdx].color.r = 1f; vBand[vBIdx].color.g = 1f; vBand[vBIdx].color.b = 1f; vBand[vBIdx].color.a = inKeep
|
||||||
|
vBand[vBIdx].tex_coord.x = 0f; vBand[vBIdx].tex_coord.y = 0f
|
||||||
|
vBIdx++
|
||||||
|
}
|
||||||
|
fun feather(inCx: Float, inCy: Float, inRx: Float, inRy: Float, inStartDeg: Float) {
|
||||||
|
if (inRx <= 1f || inRy <= 1f) return
|
||||||
|
var vPrevInX = 0f; var vPrevInY = 0f; var vPrevOutX = 0f; var vPrevOutY = 0f
|
||||||
|
for (vI in 0..vSeg) {
|
||||||
|
val vAngle = ((inStartDeg + 90f * vI / vSeg) * (PI / 180.0)).toFloat()
|
||||||
|
val vCos = cos(vAngle); val vSin = sin(vAngle)
|
||||||
|
val vInX = inCx + (inRx - vFeather) * vCos
|
||||||
|
val vInY = inCy + (inRy - vFeather) * vSin
|
||||||
|
val vOutX = inCx + inRx * vCos
|
||||||
|
val vOutY = inCy + inRy * vSin
|
||||||
|
if (vI > 0) {
|
||||||
|
putBand(vPrevInX, vPrevInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vInX, vInY, 1f)
|
||||||
|
putBand(vInX, vInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vOutX, vOutY, 0f)
|
||||||
|
}
|
||||||
|
vPrevInX = vInX; vPrevInY = vInY; vPrevOutX = vOutX; vPrevOutY = vOutY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
feather(vLeft + vTl.x, vTop + vTl.y, vTl.x, vTl.y, 180f)
|
||||||
|
feather(vRight - vTr.x, vTop + vTr.y, vTr.x, vTr.y, 270f)
|
||||||
|
feather(vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
|
||||||
|
feather(vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
|
||||||
|
if (vBIdx > 0) SDL_RenderGeometry(vRenderer, null, vBand, vBIdx, null, 0)
|
||||||
}
|
}
|
||||||
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_BLEND)
|
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_BLEND)
|
||||||
}
|
}
|
||||||
@@ -502,6 +788,8 @@ internal class Sdl3Canvas(
|
|||||||
inAmbientColor: androidx.compose.ui.graphics.Color,
|
inAmbientColor: androidx.compose.ui.graphics.Color,
|
||||||
inSpotColor: androidx.compose.ui.graphics.Color,
|
inSpotColor: androidx.compose.ui.graphics.Color,
|
||||||
) {
|
) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
|
realizePendingClips()
|
||||||
if (inElevationPx <= 0f) return
|
if (inElevationPx <= 0f) return
|
||||||
|
|
||||||
// Generic paths (CutCornerShape, GenericShape) don't 9-slice — blur the
|
// Generic paths (CutCornerShape, GenericShape) don't 9-slice — blur the
|
||||||
@@ -636,6 +924,7 @@ internal class Sdl3Canvas(
|
|||||||
// before re-rasterising a vector). Write transparent directly instead of the
|
// before re-rasterising a vector). Write transparent directly instead of the
|
||||||
// paint colour.
|
// paint colour.
|
||||||
if (paint.blendMode == BlendMode.Clear) {
|
if (paint.blendMode == BlendMode.Clear) {
|
||||||
|
realizePendingClips()
|
||||||
fScope.flush()
|
fScope.flush()
|
||||||
clearRegion(intArrayOf(
|
clearRegion(intArrayOf(
|
||||||
mapX(left, top).toInt(), mapY(left, top).toInt(),
|
mapX(left, top).toInt(), mapY(left, top).toInt(),
|
||||||
@@ -643,21 +932,75 @@ internal class Sdl3Canvas(
|
|||||||
))
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (fPendingClips.isNotEmpty() && tryDrawRectUnderPendingClips(left, top, right, bottom, paint)) return
|
||||||
prep().rectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
prep().rectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The `clip(shape).background(color)` idiom: a FILL rect that fully covers
|
||||||
|
the innermost pending clip becomes a direct rounded-shape fill (with the
|
||||||
|
scope's fringe AA — no offscreen mask at all). A rect fully INSIDE every
|
||||||
|
pending clip draws as-is. Returns true when the draw has been emitted. */
|
||||||
|
private fun tryDrawRectUnderPendingClips(inL: Float, inT: Float, inR: Float, inB: Float, inPaint: Paint): Boolean {
|
||||||
|
val vBox = deviceAabb(inL, inT, inR, inB)
|
||||||
|
val vInner = fPendingClips.last()
|
||||||
|
// Cover case: fill paint, no stroke, rect covers the whole clip outline,
|
||||||
|
// affine unchanged since the clip was pushed (local-space re-emission),
|
||||||
|
// uniform corner radii (the scope's roundRectCore takes one radius), and
|
||||||
|
// the clip outline itself contained in every OUTER pending clip.
|
||||||
|
val vLr = vInner.localRound
|
||||||
|
val vUniform = vLr.topLeftCornerRadius.x == vLr.topLeftCornerRadius.y &&
|
||||||
|
vLr.topLeftCornerRadius == vLr.topRightCornerRadius &&
|
||||||
|
vLr.topLeftCornerRadius == vLr.bottomRightCornerRadius &&
|
||||||
|
vLr.topLeftCornerRadius == vLr.bottomLeftCornerRadius
|
||||||
|
val vAffineUnchanged = vInner.ma == fMa && vInner.mb == fMb && vInner.mc == fMc &&
|
||||||
|
vInner.md == fMd && vInner.me == fMe && vInner.mf == fMf
|
||||||
|
if (inPaint.style == PaintingStyle.Fill && inPaint.blendMode == BlendMode.SrcOver &&
|
||||||
|
vUniform && vAffineUnchanged &&
|
||||||
|
vBox[0] <= vInner.bbox[0] && vBox[1] <= vInner.bbox[1] &&
|
||||||
|
vBox[2] >= vInner.bbox[2] && vBox[3] >= vInner.bbox[3]
|
||||||
|
) {
|
||||||
|
var vOutersContain = true
|
||||||
|
for (vPending in fPendingClips) {
|
||||||
|
if (vPending === vInner) continue
|
||||||
|
if (!roundRectContainsAabb(vPending.deviceRound, vInner.deviceRound.left, vInner.deviceRound.top, vInner.deviceRound.right, vInner.deviceRound.bottom)) {
|
||||||
|
vOutersContain = false; break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vOutersContain) {
|
||||||
|
prep().roundRectCore(
|
||||||
|
brushFor(inPaint),
|
||||||
|
Offset(vLr.left, vLr.top), Size(vLr.width, vLr.height),
|
||||||
|
vLr.topLeftCornerRadius.x,
|
||||||
|
(inPaint.alpha * fAlpha), Fill,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Containment: inside every pending outline → the active rect clip is enough.
|
||||||
|
for (vPending in fPendingClips) {
|
||||||
|
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
|
||||||
|
realizePendingClips()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun drawRoundRect(
|
override fun drawRoundRect(
|
||||||
left: Float, top: Float, right: Float, bottom: Float,
|
left: Float, top: Float, right: Float, bottom: Float,
|
||||||
radiusX: Float, radiusY: Float, paint: Paint,
|
radiusX: Float, radiusY: Float, paint: Paint,
|
||||||
) {
|
) {
|
||||||
|
admitDraw(left, top, right, bottom)
|
||||||
prep().roundRectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), radiusX, (paint.alpha * fAlpha), styleFor(paint))
|
prep().roundRectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), radiusX, (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
|
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
|
||||||
|
admitDraw(left, top, right, bottom)
|
||||||
prep().ovalCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
prep().ovalCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
|
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
|
||||||
|
admitDraw(center.x - radius, center.y - radius, center.x + radius, center.y + radius)
|
||||||
prep().circleCore(brushFor(paint), radius, center, (paint.alpha * fAlpha), styleFor(paint))
|
prep().circleCore(brushFor(paint), radius, center, (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,14 +1008,17 @@ internal class Sdl3Canvas(
|
|||||||
left: Float, top: Float, right: Float, bottom: Float,
|
left: Float, top: Float, right: Float, bottom: Float,
|
||||||
startAngle: Float, sweepAngle: Float, useCenter: Boolean, paint: Paint,
|
startAngle: Float, sweepAngle: Float, useCenter: Boolean, paint: Paint,
|
||||||
) {
|
) {
|
||||||
|
realizePendingClips()
|
||||||
prep().arcCore(brushFor(paint), startAngle, sweepAngle, useCenter, Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
prep().arcCore(brushFor(paint), startAngle, sweepAngle, useCenter, Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
|
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
|
||||||
|
realizePendingClips()
|
||||||
prep().lineCore(brushFor(paint), p1, p2, paint.strokeWidth, paint.strokeCap, (paint.alpha * fAlpha))
|
prep().lineCore(brushFor(paint), p1, p2, paint.strokeWidth, paint.strokeCap, (paint.alpha * fAlpha))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drawPath(path: ComposePath, paint: Paint) {
|
override fun drawPath(path: ComposePath, paint: Paint) {
|
||||||
|
realizePendingClips()
|
||||||
prep().pathCore(path, brushFor(paint), (paint.alpha * fAlpha), styleFor(paint))
|
prep().pathCore(path, brushFor(paint), (paint.alpha * fAlpha), styleFor(paint))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +1042,32 @@ internal class Sdl3Canvas(
|
|||||||
inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?,
|
inFontVariations: List<androidx.compose.ui.text.font.FontVariation.Setting>?,
|
||||||
inBaseItalic: Boolean,
|
inBaseItalic: Boolean,
|
||||||
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
|
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
|
||||||
|
inLineHeightPx: Float,
|
||||||
|
inTrimFirstLine: Boolean,
|
||||||
) {
|
) {
|
||||||
|
// Capture mode: icon-font glyphs (Material Symbols via FreeType) record as an
|
||||||
|
// IconRun — flush pending geometry first for z-order, then capture the box in
|
||||||
|
// LAYER-LOCAL device coords (replay maps it through the layer affine + re-draws
|
||||||
|
// via FreeTypeIcons' cache). Plain/decorated AND spanned text ARE captured too
|
||||||
|
// (runSink set below; each styled run becomes a TextRun) — only a run with a
|
||||||
|
// SpanStyle.background still bails inside Sdl3TextRenderer.drawText.
|
||||||
|
val vCaptureList = fCaptureList
|
||||||
|
if (vCaptureList != null && inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
|
||||||
|
fScope.flush()
|
||||||
|
val vIconColor = if (fAlpha >= 1f) inColor else inColor.copy(alpha = inColor.alpha * fAlpha)
|
||||||
|
val vArgb = (vIconColor.a8 shl 24) or (vIconColor.r8 shl 16) or (vIconColor.g8 shl 8) or vIconColor.b8
|
||||||
|
vCaptureList.captureIconRun(
|
||||||
|
inFontFamily, inText, inFontSizePx, inFontVariations,
|
||||||
|
mapX(inX, inY), mapY(inX, inY), inBoxWidth, inBoxHeight, vArgb,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Small centred labels (a popped bubble's "pop") usually sit fully
|
||||||
|
// inside a rounded clip — gate on containment like other draws, with
|
||||||
|
// a 2px margin for glyph overhang / AA bleed, instead of paying a
|
||||||
|
// mask pass per text run.
|
||||||
|
admitDraw(inX - 2f, inY - 2f, inX + inBoxWidth + 2f, inY + inBoxHeight + 2f)
|
||||||
|
com.compose.sdl.graphics.DrawStats.textDraws++
|
||||||
fScope.flush()
|
fScope.flush()
|
||||||
// Paragraph-level decoration bits forwarded to every wrapped line.
|
// Paragraph-level decoration bits forwarded to every wrapped line.
|
||||||
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
|
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
|
||||||
@@ -712,6 +1083,16 @@ internal class Sdl3Canvas(
|
|||||||
// node's real box. The per-line path below centres within a lineHeight band
|
// node's real box. The per-line path below centres within a lineHeight band
|
||||||
// (1.2 em for Material Symbols), taller than the size-clamped icon node,
|
// (1.2 em for Material Symbols), taller than the size-clamped icon node,
|
||||||
// which pushed every icon ~0.1 em below centre.
|
// which pushed every icon ~0.1 em below centre.
|
||||||
|
// Text is rasterised at its LOGICAL font size (stable glyph-texture cache
|
||||||
|
// + stable hinting) and blit 1:1 at the affine-mapped pen position; the
|
||||||
|
// layer's scale reaches position but NOT glyph size. Re-rasterising at a
|
||||||
|
// per-frame device size (an earlier attempt) made text under an animated
|
||||||
|
// layer scale — every Material popup/dropdown enter transition
|
||||||
|
// (Menu.kt scaleX/scaleY) — step through integer sizes with visible
|
||||||
|
// size/weight jumps, since SDL3_ttf re-hints at each size. Smoothly
|
||||||
|
// scaling glyphs would need blit-scaling a logical-size rasterisation
|
||||||
|
// (JVM/Skia GPU-scales the layer) — a future text-renderer enhancement;
|
||||||
|
// until then logical-size + no glyph scale is the stable choice.
|
||||||
if (inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
|
if (inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
|
||||||
vTr.drawText(
|
vTr.drawText(
|
||||||
inText = inText,
|
inText = inText,
|
||||||
@@ -738,29 +1119,46 @@ internal class Sdl3Canvas(
|
|||||||
// right edge instead of wrapping when the sidebar was resized narrower.
|
// right edge instead of wrapping when the sidebar was resized narrower.
|
||||||
val vWrapWidth = if (inSoftWrap && inBoxWidth > 0f) inBoxWidth.toInt() else Int.MAX_VALUE
|
val vWrapWidth = if (inSoftWrap && inBoxWidth > 0f) inBoxWidth.toInt() else Int.MAX_VALUE
|
||||||
val vWrapped = vRenderer.wrap(inText, inFontSizePx, vWrapWidth, inFontFamily, inFontVariations)
|
val vWrapped = vRenderer.wrap(inText, inFontSizePx, vWrapWidth, inFontFamily, inFontVariations)
|
||||||
val vBaseLineH = vRenderer.lineHeight(inFontSizePx, inFontFamily, inFontVariations)
|
// Line stacking mirrors SdlParagraph's per-mode model: compat trim (raw styles)
|
||||||
|
// keeps the FIRST line at the tight font cell; M3's Trim.None (inTrimFirstLine =
|
||||||
|
// false) makes every line the full band. Following lines always advance by the
|
||||||
|
// band (inLineHeightPx) when supplied, else the font cell.
|
||||||
|
val vFontCellH = vRenderer.lineHeight(inFontSizePx, inFontFamily, inFontVariations)
|
||||||
|
val vAdvance = if (inLineHeightPx > 0f) inLineHeightPx else vFontCellH
|
||||||
|
val vFirstLineH = if (inTrimFirstLine || inLineHeightPx <= 0f) vFontCellH else vAdvance
|
||||||
// Per-run fontSize spans make a line's box the TALLEST run cell on it —
|
// Per-run fontSize spans make a line's box the TALLEST run cell on it —
|
||||||
// same styledLineCellHeight the paragraph measured with, so paint stacks
|
// same styledLineCellHeight the paragraph measured with, so paint stacks
|
||||||
// lines exactly where layout put them.
|
// lines exactly where layout put them.
|
||||||
val vMetricSpans = com.compose.sdl.text.spansAffectMetrics(inSpans)
|
val vMetricSpans = com.compose.sdl.text.spansAffectMetrics(inSpans)
|
||||||
|
|
||||||
|
// Capture mode: route each plain line's run to the display list (spanned lines
|
||||||
|
// bail inside drawText via markUnsupported). Reset after the loop. No-op when
|
||||||
|
// not capturing (vCaptureList == null).
|
||||||
|
fTextRenderer?.runSink = vCaptureList
|
||||||
|
|
||||||
var vLineY = inY
|
var vLineY = inY
|
||||||
for ((vIdx, vLine) in vWrapped.lines.withIndex()) {
|
for ((vIdx, vLine) in vWrapped.lines.withIndex()) {
|
||||||
val vLineH =
|
val vLineH =
|
||||||
if (vMetricSpans && inSpans != null) {
|
if (vMetricSpans && inSpans != null) {
|
||||||
com.compose.sdl.text.styledLineCellHeight(
|
// Size spans can exceed the band, never shrink below it — the same
|
||||||
vLine, vWrapped.lineStarts.getOrElse(vIdx) { 0 }, inSpans,
|
// max() the paragraph's lineHeights used, so paint tracks layout
|
||||||
inFontSizePx, vTr.dpr, vRenderer, inFontFamily, inFontVariations,
|
// (first line keeps its tight cell under compat trim).
|
||||||
|
kotlin.math.max(
|
||||||
|
com.compose.sdl.text.styledLineCellHeight(
|
||||||
|
vLine, vWrapped.lineStarts.getOrElse(vIdx) { 0 }, inSpans,
|
||||||
|
inFontSizePx, vTr.dpr, vRenderer, inFontFamily, inFontVariations,
|
||||||
|
),
|
||||||
|
if ((vIdx == 0 && inTrimFirstLine) || inLineHeightPx <= 0f) 0f else inLineHeightPx,
|
||||||
)
|
)
|
||||||
} else vBaseLineH
|
} else if (vIdx == 0) vFirstLineH else vAdvance
|
||||||
// Cull lines that would fall entirely below the box (softWrap keeps the
|
// Cull lines that would fall entirely below the box (softWrap keeps the
|
||||||
// natural line count; the box just clips at draw time).
|
// natural line count; the box just clips at draw time).
|
||||||
if (vLineY >= inY + inBoxHeight) break
|
if (vLineY >= inY + inBoxHeight) break
|
||||||
vTr.drawText(
|
vTr.drawText(
|
||||||
inText = vLine,
|
inText = vLine,
|
||||||
// Position maps through the affine (translate/scale reach the origin);
|
// Position maps through the affine; size/boxes switch to DEVICE
|
||||||
// glyph scaling/rotation isn't wired, so text in a rotated/scaled layer
|
// units here (scaled by the affine's column norms) so glyphs
|
||||||
// repositions but doesn't itself scale or rotate.
|
// scale with the layer. Rotation still only repositions.
|
||||||
inX = mapX(inX, vLineY).toInt(),
|
inX = mapX(inX, vLineY).toInt(),
|
||||||
inY = mapY(inX, vLineY).toInt(),
|
inY = mapY(inX, vLineY).toInt(),
|
||||||
inBoxWidth = inBoxWidth.toInt(),
|
inBoxWidth = inBoxWidth.toInt(),
|
||||||
@@ -778,6 +1176,7 @@ internal class Sdl3Canvas(
|
|||||||
)
|
)
|
||||||
vLineY += vLineH
|
vLineY += vLineH
|
||||||
}
|
}
|
||||||
|
fTextRenderer?.runSink = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
@@ -794,6 +1193,8 @@ internal class Sdl3Canvas(
|
|||||||
inContentScale: androidx.compose.ui.layout.ContentScale,
|
inContentScale: androidx.compose.ui.layout.ContentScale,
|
||||||
inAlpha: Float,
|
inAlpha: Float,
|
||||||
) {
|
) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
|
admitDraw(inX, inY, inX + inWidth, inY + inHeight)
|
||||||
fScope.flush()
|
fScope.flush()
|
||||||
// Map the origin through the affine and scale the size by the matrix's axis
|
// Map the origin through the affine and scale the size by the matrix's axis
|
||||||
// magnitudes (rotation contributes 1, so it only repositions the blit — the
|
// magnitudes (rotation contributes 1, so it only repositions the blit — the
|
||||||
@@ -811,7 +1212,9 @@ internal class Sdl3Canvas(
|
|||||||
// Not-yet-wired ops (B5 image leaf / B6 layers). Accept-and-ignore.
|
// Not-yet-wired ops (B5 image leaf / B6 layers). Accept-and-ignore.
|
||||||
|
|
||||||
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
|
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
val vBmp = image as? SdlImageBitmap ?: return
|
val vBmp = image as? SdlImageBitmap ?: return
|
||||||
|
admitDraw(topLeftOffset.x, topLeftOffset.y, topLeftOffset.x + vBmp.width, topLeftOffset.y + vBmp.height)
|
||||||
drawImageRect(
|
drawImageRect(
|
||||||
image,
|
image,
|
||||||
androidx.compose.ui.unit.IntOffset.Zero,
|
androidx.compose.ui.unit.IntOffset.Zero,
|
||||||
@@ -834,7 +1237,10 @@ internal class Sdl3Canvas(
|
|||||||
dstSize: androidx.compose.ui.unit.IntSize,
|
dstSize: androidx.compose.ui.unit.IntSize,
|
||||||
paint: Paint,
|
paint: Paint,
|
||||||
) {
|
) {
|
||||||
|
if (captureUnsupported()) return
|
||||||
|
realizePendingClips()
|
||||||
val vTex = (image as? SdlImageBitmap)?.texture ?: return
|
val vTex = (image as? SdlImageBitmap)?.texture ?: return
|
||||||
|
com.compose.sdl.graphics.DrawStats.imageBlits++
|
||||||
// Commit pending frame geometry and re-assert this canvas's target + clip
|
// Commit pending frame geometry and re-assert this canvas's target + clip
|
||||||
// (an offscreen render just borrowed the render target).
|
// (an offscreen render just borrowed the render target).
|
||||||
fScope.flush()
|
fScope.flush()
|
||||||
|
|||||||
+104
-16
@@ -112,6 +112,14 @@ internal class Sdl3DrawScope(
|
|||||||
internal fun setMatrix(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) {
|
internal fun setMatrix(a: Float, b: Float, c: Float, d: Float, e: Float, f: Float) {
|
||||||
fMa = a; fMb = b; fMc = c; fMd = d; fMe = e; fMf = f
|
fMa = a; fMb = b; fMc = c; fMd = d; fMe = e; fMf = f
|
||||||
}
|
}
|
||||||
|
// Current device affine (a,b,c,d,e,f) — screen = (a*x+c*y+e, b*x+d*y+f). Phase 4
|
||||||
|
// replay reads this to re-transform captured layer-local vertices.
|
||||||
|
internal fun currentDeviceMatrix(): FloatArray = floatArrayOf(fMa, fMb, fMc, fMd, fMe, fMf)
|
||||||
|
|
||||||
|
// Phase 4: when set, flush() CAPTURES the tessellated batch (layer-local, if this
|
||||||
|
// scope's base CTM is identity) instead of submitting to the GPU. null = normal
|
||||||
|
// immediate submit. See SdlDisplayList.
|
||||||
|
internal var recordingSink: GeometrySink? = null
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Vertex batch — heap-allocated buffer of SDL_Vertex shared across
|
// Vertex batch — heap-allocated buffer of SDL_Vertex shared across
|
||||||
@@ -133,6 +141,13 @@ internal class Sdl3DrawScope(
|
|||||||
call mid-scope when the buffer fills up (no cross-triangle state). */
|
call mid-scope when the buffer fills up (no cross-triangle state). */
|
||||||
fun flush() {
|
fun flush() {
|
||||||
if (fBatchCount == 0) return
|
if (fBatchCount == 0) return
|
||||||
|
val vSink = recordingSink
|
||||||
|
if (vSink != null) {
|
||||||
|
// Phase 4 recording: capture the batch (layer-local) instead of submitting.
|
||||||
|
vSink.captureGeometry(fVertexData, fBatchCount)
|
||||||
|
fBatchCount = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
fVertexData.usePinned { vPinned ->
|
fVertexData.usePinned { vPinned ->
|
||||||
platform.posix.memcpy(
|
platform.posix.memcpy(
|
||||||
fBatch,
|
fBatch,
|
||||||
@@ -141,6 +156,8 @@ internal class Sdl3DrawScope(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
SDL_RenderGeometry(fRenderer.reinterpret(), null, fBatch, fBatchCount, null, 0)
|
SDL_RenderGeometry(fRenderer.reinterpret(), null, fBatch, fBatchCount, null, 0)
|
||||||
|
com.compose.sdl.graphics.DrawStats.geometrySubmits++
|
||||||
|
com.compose.sdl.graphics.DrawStats.vertices += fBatchCount
|
||||||
fBatchCount = 0
|
fBatchCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +260,10 @@ internal class Sdl3DrawScope(
|
|||||||
val vB = vT + size.height
|
val vB = vT + size.height
|
||||||
when (style) {
|
when (style) {
|
||||||
Fill -> {
|
Fill -> {
|
||||||
emitQuad(vL, vT, vR, vT, vR, vB, vL, vB, vSampler)
|
// Gradient fills need interior samples (see emitRectMesh); solid
|
||||||
|
// fills are a single flat quad.
|
||||||
|
if (brush is SolidColor) emitQuad(vL, vT, vR, vT, vR, vB, vL, vB, vSampler)
|
||||||
|
else emitRectMesh(vL, vT, vR, vB, vSampler)
|
||||||
// Axis-aligned rects are pixel-crisp and need no AA. When the affine
|
// Axis-aligned rects are pixel-crisp and need no AA. When the affine
|
||||||
// rotates/shears them, the edges become diagonal — feather each edge
|
// rotates/shears them, the edges become diagonal — feather each edge
|
||||||
// outward (normals in local space; the matrix orients them on screen).
|
// outward (normals in local space; the matrix orients them on screen).
|
||||||
@@ -524,20 +544,30 @@ internal class Sdl3DrawScope(
|
|||||||
val vY = fOriginY + topLeft.y
|
val vY = fOriginY + topLeft.y
|
||||||
val vW = size.width
|
val vW = size.width
|
||||||
val vH = size.height
|
val vH = size.height
|
||||||
|
// Gradient fills sample per-vertex, so the interior must be meshed
|
||||||
|
// (corner-only sampling renders radial/sweep flat — see emitRectMesh);
|
||||||
|
// solid fills stay single quads.
|
||||||
|
val vGrad = brush !is SolidColor
|
||||||
if (vR <= 0f) {
|
if (vR <= 0f) {
|
||||||
// Trivial: just two triangles.
|
if (vGrad) emitRectMesh(vX, vY, vX + vW, vY + vH, vSampler)
|
||||||
emitQuad(vX, vY, vX + vW, vY, vX + vW, vY + vH, vX, vY + vH, vSampler)
|
else emitQuad(vX, vY, vX + vW, vY, vX + vW, vY + vH, vX, vY + vH, vSampler)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Body in 3 strips: middle (full width × inner height), top edge,
|
// Body in 3 strips: middle (full width × inner height), top edge,
|
||||||
// bottom edge — plus the 4 corner arcs.
|
// bottom edge — plus the 4 corner arcs.
|
||||||
if (style == Fill) {
|
if (style == Fill) {
|
||||||
// Middle strip
|
if (vGrad) {
|
||||||
emitQuad(vX, vY + vR, vX + vW, vY + vR, vX + vW, vY + vH - vR, vX, vY + vH - vR, vSampler)
|
emitRectMesh(vX, vY + vR, vX + vW, vY + vH - vR, vSampler) // middle
|
||||||
// Top edge (between left+right corners)
|
emitRectMesh(vX + vR, vY, vX + vW - vR, vY + vR, vSampler) // top edge
|
||||||
emitQuad(vX + vR, vY, vX + vW - vR, vY, vX + vW - vR, vY + vR, vX + vR, vY + vR, vSampler)
|
emitRectMesh(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH, vSampler) // bottom edge
|
||||||
// Bottom edge
|
} else {
|
||||||
emitQuad(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH - vR, vX + vW - vR, vY + vH, vX + vR, vY + vH, vSampler)
|
// Middle strip
|
||||||
|
emitQuad(vX, vY + vR, vX + vW, vY + vR, vX + vW, vY + vH - vR, vX, vY + vH - vR, vSampler)
|
||||||
|
// Top edge (between left+right corners)
|
||||||
|
emitQuad(vX + vR, vY, vX + vW - vR, vY, vX + vW - vR, vY + vR, vX + vR, vY + vR, vSampler)
|
||||||
|
// Bottom edge
|
||||||
|
emitQuad(vX + vR, vY + vH - vR, vX + vW - vR, vY + vH - vR, vX + vW - vR, vY + vH, vX + vR, vY + vH, vSampler)
|
||||||
|
}
|
||||||
// 4 corner fills — segment count adapts to the corner radius.
|
// 4 corner fills — segment count adapts to the corner radius.
|
||||||
val vSeg = arcSegments(90f, vR)
|
val vSeg = arcSegments(90f, vR)
|
||||||
emitFilledArc(vX + vR, vY + vR, vR, vR, 180f, 90f, false, vSeg, vSampler)
|
emitFilledArc(vX + vR, vY + vR, vR, vR, 180f, 90f, false, vSeg, vSampler)
|
||||||
@@ -854,6 +884,34 @@ internal class Sdl3DrawScope(
|
|||||||
emitTri(ax, ay, cx, cy, dx, dy, inSampler)
|
emitTri(ax, ay, cx, cy, dx, dy, inSampler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Axis-aligned rect subdivided into a grid, so a per-vertex sampler is
|
||||||
|
// evaluated across the INTERIOR, not just the 4 corners. Corner-only
|
||||||
|
// sampling renders radial/sweep gradients flat (a square's corners are all
|
||||||
|
// equidistant from its centre → identical t → solid fill) and clips
|
||||||
|
// non-uniform multi-stop linear ramps. SDL interpolates linearly within
|
||||||
|
// each cell, so ~16 cells/axis tracks the gradient curve closely. Solid
|
||||||
|
// fills never come here (they use emitQuad — one quad).
|
||||||
|
private fun emitRectMesh(
|
||||||
|
inL: Float, inT: Float, inR: Float, inB: Float, inSampler: Sampler,
|
||||||
|
) {
|
||||||
|
// ~one cell per 8 device px per axis (1..24), so thin edge strips stay
|
||||||
|
// cheap while large fills get enough interior samples for the gradient.
|
||||||
|
val vCols = (((inR - inL) / 8f).toInt()).coerceIn(1, 24)
|
||||||
|
val vRows = (((inB - inT) / 8f).toInt()).coerceIn(1, 24)
|
||||||
|
val vW = (inR - inL) / vCols
|
||||||
|
val vH = (inB - inT) / vRows
|
||||||
|
for (vRow in 0 until vRows) {
|
||||||
|
val vY0 = inT + vRow * vH
|
||||||
|
val vY1 = if (vRow == vRows - 1) inB else vY0 + vH
|
||||||
|
for (vCol in 0 until vCols) {
|
||||||
|
val vX0 = inL + vCol * vW
|
||||||
|
val vX1 = if (vCol == vCols - 1) inR else vX0 + vW
|
||||||
|
emitTri(vX0, vY0, vX1, vY0, vX1, vY1, inSampler)
|
||||||
|
emitTri(vX0, vY0, vX1, vY1, vX0, vY1, inSampler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun emitTri(
|
private fun emitTri(
|
||||||
ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float,
|
ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float,
|
||||||
inSampler: Sampler,
|
inSampler: Sampler,
|
||||||
@@ -889,11 +947,39 @@ internal class Sdl3DrawScope(
|
|||||||
emitFringeQuad(x0, y0, x1, y1, x1 + inNx, y1 + inNy, x0 + inNx, y0 + inNy, inSampler)
|
emitFringeQuad(x0, y0, x1, y1, x1 + inNx, y1 + inNy, x0 + inNx, y0 + inNy, inSampler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4 replay: re-emit a captured (layer-local) batch through the CURRENT CTM.
|
||||||
|
// The batch was recorded with an identity base CTM, so applying fMa..fMf here
|
||||||
|
// places it at the layer's transform. Colours/UVs pass through unchanged. Runs
|
||||||
|
// in NORMAL (non-recording) mode → the next flush() submits via SDL_RenderGeometry.
|
||||||
|
internal fun replayBatch(vd: FloatArray, count: Int) {
|
||||||
|
for (i in 0 until count) {
|
||||||
|
if (fBatchCount >= kBatchCapacity && fBatchCount % 3 == 0) flush()
|
||||||
|
val s = i * kFloatsPerVertex
|
||||||
|
val x = vd[s]; val y = vd[s + 1]
|
||||||
|
val base = fBatchCount * kFloatsPerVertex
|
||||||
|
fVertexData[base + 0] = fMa * x + fMc * y + fMe
|
||||||
|
fVertexData[base + 1] = fMb * x + fMd * y + fMf
|
||||||
|
fVertexData[base + 2] = vd[s + 2]
|
||||||
|
fVertexData[base + 3] = vd[s + 3]
|
||||||
|
fVertexData[base + 4] = vd[s + 4]
|
||||||
|
fVertexData[base + 5] = vd[s + 5]
|
||||||
|
fVertexData[base + 6] = vd[s + 6]
|
||||||
|
fVertexData[base + 7] = vd[s + 7]
|
||||||
|
fBatchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stage one vertex into fVertexData (auto-flush when full). Position goes
|
// Stage one vertex into fVertexData (auto-flush when full). Position goes
|
||||||
// through the current affine so scale/rotate reach the GPU; the colour is
|
// through the current affine so scale/rotate reach the GPU; the colour is
|
||||||
// sampled at the pre-transform point so a gradient rides its shape.
|
// sampled at the pre-transform point so a gradient rides its shape.
|
||||||
private fun writeVertex(inX: Float, inY: Float, inColor: ComposeColor, inAlphaScale: Float = 1f) {
|
private fun writeVertex(inX: Float, inY: Float, inColor: ComposeColor, inAlphaScale: Float = 1f) {
|
||||||
if (fBatchCount >= kBatchCapacity) flush()
|
// Auto-flush ONLY at a triangle boundary: SDL_RenderGeometry renders
|
||||||
|
// floor(count/3) triangles and silently drops the tail, so flushing
|
||||||
|
// mid-triangle tears it AND misaligns every following triangle in the
|
||||||
|
// new batch (seen as ~350-degree pac-man circles and glitch shapes
|
||||||
|
// once a frame's geometry outgrew one batch - e.g. 84 unclipped
|
||||||
|
// drawBehind bubbles). kBatchCapacity is a multiple of 3.
|
||||||
|
if (fBatchCount >= kBatchCapacity && fBatchCount % 3 == 0) flush()
|
||||||
val vBase = fBatchCount * kFloatsPerVertex
|
val vBase = fBatchCount * kFloatsPerVertex
|
||||||
fVertexData[vBase + 0] = fMa * inX + fMc * inY + fMe
|
fVertexData[vBase + 0] = fMa * inX + fMc * inY + fMe
|
||||||
fVertexData[vBase + 1] = fMb * inX + fMd * inY + fMf
|
fVertexData[vBase + 1] = fMb * inX + fMd * inY + fMf
|
||||||
@@ -914,15 +1000,17 @@ private const val kAaFeather: Float = 1.0f
|
|||||||
private const val kAaHalf: Float = kAaFeather * 0.5f
|
private const val kAaHalf: Float = kAaFeather * 0.5f
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Batch capacity — 8192 vertices = ~2730 triangles per submission. At
|
// Batch capacity — 8190 vertices = 2730 triangles per submission (kept a
|
||||||
// 64 segments per full circle that's room for ~21 full-circle filled
|
// MULTIPLE OF 3: the auto-flush in writeVertex only fires on triangle
|
||||||
// shapes per Canvas{} before any flush. Bigger gives fewer GPU
|
// boundaries, so the cap must land on one). At 64 segments per full circle
|
||||||
// submissions; smaller saves RAM. ~128 KB at 16 bytes per SDL_Vertex.
|
// that's room for ~21 full-circle filled shapes per Canvas{} before any
|
||||||
private const val kBatchCapacity: Int = 8192
|
// flush. Bigger gives fewer GPU submissions; smaller saves RAM. ~128 KB at
|
||||||
|
// 16 bytes per SDL_Vertex.
|
||||||
|
private const val kBatchCapacity: Int = 8190
|
||||||
|
|
||||||
// Floats per SDL_Vertex: position(x,y) + color(r,g,b,a) + tex_coord(x,y), tightly
|
// Floats per SDL_Vertex: position(x,y) + color(r,g,b,a) + tex_coord(x,y), tightly
|
||||||
// packed. Used to stage vertices in a Kotlin FloatArray and memcpy them across.
|
// packed. Used to stage vertices in a Kotlin FloatArray and memcpy them across.
|
||||||
private const val kFloatsPerVertex: Int = 8
|
internal const val kFloatsPerVertex: Int = 8
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Brush → per-vertex colour sampler
|
// MARK: Brush → per-vertex colour sampler
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user