mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-06 11:17:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c76f629971 | ||
|
|
c4564b3a10 | ||
|
|
fb8e97b74d | ||
|
|
6c3bfd8679 | ||
|
|
ec2d9b06ad | ||
|
|
1d6e5a76de | ||
|
|
eb44db915e | ||
|
|
1120abc488 | ||
|
|
9d2656f905 | ||
|
|
48cfa5da4d | ||
|
|
856cbe5ef3 | ||
|
|
5c7308c614 | ||
|
|
2fb322776a | ||
|
|
7c7bb63cae | ||
|
|
02fb2ddf4e | ||
|
|
126e83c056 | ||
|
|
0c09109e6e | ||
|
|
5804128e1d | ||
|
|
04d1df58de | ||
|
|
a87ff2d7f6 | ||
|
|
10380e5422 | ||
|
|
2fe26cf7f8 | ||
|
|
71aab5d502 | ||
|
|
d19948059b | ||
|
|
20714b3eeb | ||
|
|
d029b48e10 | ||
|
|
47445ab7ab | ||
|
|
b965c505ee | ||
|
|
05faaa7f7c | ||
|
|
59bb159351 | ||
|
|
fcba77b622 | ||
|
|
82ffd7cf4d | ||
|
|
d3eef74803 | ||
|
|
a07f4634b2 | ||
|
|
164d2b5e66 |
@@ -23,7 +23,7 @@ jobs:
|
|||||||
# Each host runs only the publish tasks Gradle actually generated for it.
|
# Each host runs only the publish tasks Gradle actually generated for it.
|
||||||
# WINDOWS also publishes the shared kotlinMultiplatform metadata modules
|
# WINDOWS also publishes the shared kotlinMultiplatform metadata modules
|
||||||
# (see its matrix entry) + the jvm jar + the bridge plugin.
|
# (see its matrix entry) + the jvm jar + the bridge plugin.
|
||||||
# `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
|
||||||
# are host-arch only; cross-compile would fail at link time.
|
# are host-arch only; cross-compile would fail at link time.
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is
|
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is
|
||||||
# the ONLY host that declares every target (vHostSupportsMingw is
|
# the ONLY host that declares every target (vHostSupportsMingw is
|
||||||
# host-gated), so only its generated .module files carry the full
|
# host-gated), so only its generated .module files carry the full
|
||||||
# variant table — the macOS-published roots of v0.1.15 had NO
|
# variant table - the macOS-published roots of v0.1.15 had NO
|
||||||
# mingwX64 variants and Windows consumers could not resolve them.
|
# mingwX64 variants and Windows consumers could not resolve them.
|
||||||
# The host-independent jvm jar (:material-symbols) and the bridge
|
# The host-independent jvm jar (:material-symbols) and the bridge
|
||||||
# plugin ride along on the same job.
|
# plugin ride along on the same job.
|
||||||
@@ -102,13 +102,13 @@ jobs:
|
|||||||
|
|
||||||
# windows-2022 runners pre-install Chocolatey's mingw-w64 at C:\mingw64.
|
# windows-2022 runners pre-install Chocolatey's mingw-w64 at C:\mingw64.
|
||||||
# SDL3 >= 3.4 needs a C++ compiler for its GameInput backend (K/N's
|
# SDL3 >= 3.4 needs a C++ compiler for its GameInput backend (K/N's
|
||||||
# bundled mingw is C-only) — this pre-install covers that. The setjmp
|
# bundled mingw is C-only) - this pre-install covers that. The setjmp
|
||||||
# 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 the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
|
# Build the static SDL3 lib (scripts/build-sdl/build-sdl.properties). Every
|
||||||
# target renders through Skia now, so all platforms need only base SDL3 for
|
# target renders through Skia now, so all platforms need only base SDL3 for
|
||||||
# the main loop (windowing / input / audio / filesystem) — the from-scratch
|
# the main loop (windowing / input / audio / filesystem) - the from-scratch
|
||||||
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
|
# SDL renderer and its SDL3_ttf / SDL3_image / FreeType deps were removed.
|
||||||
# Cached against the manifest + scripts so version bumps invalidate cleanly;
|
# Cached against the manifest + scripts so version bumps invalidate cleanly;
|
||||||
# bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N
|
# bump the `v3` key when the toolchain (runner OS, compiler flavour, K/N
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: python3 scripts/build-sdl/build-all.py
|
run: python3 scripts/build-sdl/build-all.py
|
||||||
|
|
||||||
# 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
|
||||||
# pinned in scripts/compose-fork/compose.properties. Needs python3 + git;
|
# pinned in scripts/compose-fork/compose.properties. Needs python3 + git;
|
||||||
# both are pre-installed on every runner.
|
# both are pre-installed on every runner.
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
# SEQUENTIALLY from the first one. So when an upload dies mid-publication
|
# SEQUENTIALLY from the first one. So when an upload dies mid-publication
|
||||||
# (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409
|
# (the v0.1.13 macOS run lost DNS mid-publish), a blind retry hits 409
|
||||||
# Conflict on the first already-uploaded file and aborts BEFORE reaching
|
# Conflict on the first already-uploaded file and aborts BEFORE reaching
|
||||||
# the missing tail files — the version is poisoned and can never
|
# the missing tail files - the version is poisoned and can never
|
||||||
# complete. A 409 is therefore only benign when the publication had
|
# complete. A 409 is therefore only benign when the publication had
|
||||||
# already completed; you can't tell from the 409 alone. The fix: between
|
# already completed; you can't tell from the 409 alone. The fix: between
|
||||||
# attempts, DELETE this host's package versions for the current release
|
# attempts, DELETE this host's package versions for the current release
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
||||||
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
||||||
esac
|
esac
|
||||||
# Published group per module — mirrors the per-area com.bitsycore groups
|
# Published group per module - mirrors the per-area com.bitsycore groups
|
||||||
# in root build.gradle.kts. Project-only modules (sdl-core, material-symbols)
|
# in root build.gradle.kts. Project-only modules (sdl-core, material-symbols)
|
||||||
# fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven
|
# fall through to com.bitsycore.compose.sdl; the GitHub Packages Maven
|
||||||
# package name is "<group>.<artifact>".
|
# package name is "<group>.<artifact>".
|
||||||
@@ -202,7 +202,7 @@ jobs:
|
|||||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
|
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
|
||||||
if [ "$code" != "204" ]; then
|
if [ "$code" != "204" ]; then
|
||||||
# Deleting the LAST version of a package is refused (400) —
|
# Deleting the LAST version of a package is refused (400) -
|
||||||
# a package new in this release: drop the whole package.
|
# a package new in this release: drop the whole package.
|
||||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
if [ $attempt -lt 3 ]; then
|
if [ $attempt -lt 3 ]; then
|
||||||
echo "::warning::Publish attempt $attempt failed — cleaning this host's $VER packages and retrying"
|
echo "::warning::Publish attempt $attempt failed - cleaning this host's $VER packages and retrying"
|
||||||
cleanup_host_versions || true
|
cleanup_host_versions || true
|
||||||
sleep 30
|
sleep 30
|
||||||
fi
|
fi
|
||||||
@@ -247,19 +247,19 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
if [ $attempt -lt 3 ]; then
|
if [ $attempt -lt 3 ]; then
|
||||||
echo "::warning::Bitsycore publish attempt $attempt failed — retrying"
|
echo "::warning::Bitsycore publish attempt $attempt failed - retrying"
|
||||||
sleep 30
|
sleep 30
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo "::error::Bitsycore publish failed after 3 attempts"
|
echo "::error::Bitsycore publish failed after 3 attempts"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
# Release binaries for the host target — the binary plus the data.kres
|
# Release binaries for the host target - the binary plus the data.kres
|
||||||
# (bundled composeResources + fonts + subset icon-font). Each app's
|
# (bundled composeResources + fonts + subset icon-font). Each app's
|
||||||
# linkReleaseExecutable<Target> auto-runs its data.kres zip task —
|
# linkReleaseExecutable<Target> auto-runs its data.kres zip task -
|
||||||
# :demo's own copy<Variant>ComposeResources<Target>, :apidemo's
|
# :demo's own copy<Variant>ComposeResources<Target>, :apidemo's
|
||||||
# bridge-plugin package<Variant>ComposeResources<Target> — so data.kres
|
# bridge-plugin package<Variant>ComposeResources<Target> - so data.kres
|
||||||
# is produced next to the .kexe / .exe. On Linux only linuxX64 is built —
|
# is produced next to the .kexe / .exe. On Linux only linuxX64 is built -
|
||||||
# linuxArm64 is skipped since the SDL3 static libs on the ubuntu runner
|
# linuxArm64 is skipped since the SDL3 static libs on the ubuntu runner
|
||||||
# are host-arch (x64).
|
# are host-arch (x64).
|
||||||
- name: Build demo + apidemo release binaries
|
- name: Build demo + apidemo release binaries
|
||||||
@@ -274,7 +274,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Lower-case first letter — gradle target tokens are Capitalised but
|
# Lower-case first letter - gradle target tokens are Capitalised but
|
||||||
# the build/bin dir uses the plain target name (macosArm64 etc).
|
# the build/bin dir uses the plain target name (macosArm64 etc).
|
||||||
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}"
|
||||||
@@ -283,8 +283,8 @@ jobs:
|
|||||||
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"
|
||||||
files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres")
|
files=("$src/$app.${{ matrix.exe_ext }}" "$src/data.kres")
|
||||||
# Windows Skia leg ships two runtime sidecars next to the exe — the
|
# Windows Skia leg ships two runtime sidecars next to the exe - the
|
||||||
# skiko fork DLL and Skia's ICU data (icudtl.dat) — provisioned by the
|
# skiko fork DLL and Skia's ICU data (icudtl.dat) - provisioned by the
|
||||||
# bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run
|
# bridge plugin after linkReleaseExecutableMingwX64. The exe cannot run
|
||||||
# without them, so include them (and fail loudly if a provision regressed).
|
# without them, so include them (and fail loudly if a provision regressed).
|
||||||
if [ "${{ matrix.exe_ext }}" = "exe" ]; then
|
if [ "${{ matrix.exe_ext }}" = "exe" ]; then
|
||||||
|
|||||||
+2
-1
@@ -53,7 +53,7 @@ bin/
|
|||||||
|
|
||||||
### Vendored Compose sources ###
|
### Vendored Compose sources ###
|
||||||
# Byte-for-byte verbatim copies from JetBrains/compose-multiplatform-core,
|
# Byte-for-byte verbatim copies from JetBrains/compose-multiplatform-core,
|
||||||
# regenerated on demand by scripts/compose-fork/sync.sh. Not committed — only the
|
# regenerated on demand by scripts/compose-fork/sync.sh. Not committed - only the
|
||||||
# sync tooling (script + <module>/compose-fork.txt manifests + pinned ref + README)
|
# sync tooling (script + <module>/compose-fork.txt manifests + pinned ref + README)
|
||||||
# is tracked.
|
# is tracked.
|
||||||
**/src/vendor/
|
**/src/vendor/
|
||||||
@@ -64,3 +64,4 @@ __pycache__/
|
|||||||
|
|
||||||
# Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest
|
# Probe artifacts written to the repo root by demo --nav3test / --searchesctest / --dialoganimtest
|
||||||
/*.bmp
|
/*.bmp
|
||||||
|
/local.properties
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
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
|
## Documentation map
|
||||||
|
|
||||||
- [README.md](README.md) — public overview + quickstart (bridge plugin,
|
- [README.md](README.md) - public overview + quickstart (bridge plugin,
|
||||||
`nativeComposeWindow`, sample apps).
|
`nativeComposeWindow`, sample apps).
|
||||||
- Renderer — one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows /
|
- Renderer - one Skia `RenderBackend` (Metal on macOS / OpenGL on Linux+Windows /
|
||||||
CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine.
|
CPU-raster fallback) driving upstream's vendored GraphicsLayer + Canvas engine.
|
||||||
Retained per-node display lists: transform / alpha / clip changes REPLAY without
|
Retained per-node display lists: transform / alpha / clip changes REPLAY without
|
||||||
re-recording (only a content change or resize re-records; dirty-region rendering
|
re-recording (only a content change or resize re-records; dirty-region rendering
|
||||||
is a non-goal). Text is a reduced-local port over Skia `skparagraph`. Read these
|
is a non-goal). Text is a reduced-local port over Skia `skparagraph`. Read these
|
||||||
before touching renderer / graphics-actual / layer-engine code:
|
before touching renderer / graphics-actual / layer-engine code:
|
||||||
`SkiaRenderBackend.kt`, `RenderBackend.kt`, `GpuMode.kt`, `ComposeRootHost.kt`
|
`SkiaRenderBackend.kt`, `RenderBackend.kt`, `GpuMode.kt`, `ComposeRootHost.kt`
|
||||||
(root host + snapshot-observation sweep — omitting `clearInvalidObservations()`
|
(root host + snapshot-observation sweep - omitting `clearInvalidObservations()`
|
||||||
once leaked the whole graph), `ComposeOwner.kt`, and `SkiaParagraph.native.kt` /
|
once leaked the whole graph), `ComposeOwner.kt`, and `SkiaParagraph.native.kt` /
|
||||||
`SkiaParagraphEngine.kt`. Open renderer/fidelity work lives in [PLAN.md](PLAN.md).
|
`SkiaParagraphEngine.kt`. Open renderer/fidelity work lives in [PLAN.md](PLAN.md).
|
||||||
- [TOOLING.md](TOOLING.md) — build/vendor/verify scripts and workflows
|
- [TOOLING.md](TOOLING.md) - build/vendor/verify scripts and workflows
|
||||||
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
|
(build-sdl, sync + drift checks, parity, probe, profiler, coverage,
|
||||||
verify-mac) + the version map and the ref-bump / release runbooks.
|
verify-mac) + the version map and the ref-bump / release runbooks.
|
||||||
- [PLAN.md](PLAN.md) §2 — audited list of no-ops, stubs, and hardcodes left in the
|
- [PLAN.md](PLAN.md) §2 - audited list of no-ops, stubs, and hardcodes left in the
|
||||||
port, with P0/P1/P2 severity, plus the road-to-1.0.0 fidelity / SDL / release work
|
port, with P0/P1/P2 severity, plus the road-to-1.0.0 fidelity / SDL / release work
|
||||||
(this subsumes the former TODO.md, which was never committed).
|
(this subsumes the former TODO.md, which was never committed).
|
||||||
- This file — architecture, module layout, vendoring rules, source-set
|
- This file - architecture, module layout, vendoring rules, source-set
|
||||||
hierarchy, density flow, conventions, and common pitfalls.
|
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
|
||||||
on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux
|
on SDL3, no JVM. Compiles to native binaries for macOS (arm64), Linux
|
||||||
(x64/arm64), Windows (mingwX64).
|
(x64/arm64), Windows (mingwX64).
|
||||||
|
|
||||||
Rendering is **Skia everywhere** behind one `RenderBackend` — Metal / OpenGL
|
Rendering is **Skia everywhere** behind one `RenderBackend` - Metal / OpenGL
|
||||||
/ CPU raster:
|
/ CPU raster:
|
||||||
|
|
||||||
- **macOS + Linux** link the OFFICIAL Skiko klibs from Maven.
|
- **macOS + Linux** link the OFFICIAL Skiko klibs from Maven.
|
||||||
- **Windows (mingwX64)** links the bitsycore skiko FORK — skiko+Skia compiled
|
- **Windows (mingwX64)** links the bitsycore skiko FORK - skiko+Skia compiled
|
||||||
into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via
|
into `skiko-windows-x64.dll` with a flat extern-C surface, bound from K/N via
|
||||||
an embedded GNU import lib, published to GitHub Packages as
|
an embedded GNU import lib, published to https://maven.bitsycore.com/releases as
|
||||||
`com.bitsycore.skiko:skiko:0.150.1-mingw.1` (override with
|
`com.bitsycore.skiko:skiko:0.150.1-mingw.2` (override with
|
||||||
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
|
`-PskikoMingwVersion`). The runtime DLL is auto-provisioned next to the exe by
|
||||||
the bridge plugin (`installWindowsSkiaDll`).
|
the bridge plugin (`installWindowsSkiaDll`).
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ Windowing, input, audio, filesystem access, and the OS-integration surface
|
|||||||
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
|
(file dialogs, clipboard, "open in Finder/Explorer"…) all go through
|
||||||
**SDL3**. The runtime (`androidx.compose.runtime.*`: composition, snapshots,
|
**SDL3**. The runtime (`androidx.compose.runtime.*`: composition, snapshots,
|
||||||
recomposer, `mutableStateOf`, `remember`, …) is the **official
|
recomposer, `mutableStateOf`, `remember`, …) is the **official
|
||||||
`org.jetbrains.compose.runtime` klibs from Maven** — this project only
|
`org.jetbrains.compose.runtime` klibs from Maven** - this project only
|
||||||
re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
|
re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
|
||||||
`.animation.*`, `.material3.*`).
|
`.animation.*`, `.material3.*`).
|
||||||
|
|
||||||
@@ -55,67 +55,91 @@ re-implements the layers on top (`androidx.compose.ui.*`, `.foundation.*`,
|
|||||||
|
|
||||||
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
|
Library modules mirror upstream Compose Multiplatform's `compose/` tree.
|
||||||
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
|
The SDL layer is two modules: `:sdl-core` (the NAKED sdl3 cinterop + platform
|
||||||
primitives — zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and
|
primitives - zero Compose dep, like `skiko`) at `sdl/sdl-core/`, and
|
||||||
`:desktop-native-window` (the SDL3 main-loop shell + app entry point) at
|
`:desktop-native-window` (the SDL3 main-loop shell + app entry point) at
|
||||||
`compose/desktop/native/window/`. `:ui` depends on `:sdl-core` and its renderer
|
`compose/desktop/native/desktop-native-window/`. `:ui` depends on `:sdl-core` and its renderer
|
||||||
+ SDL↔Compose bridges pick the cinterop from it.
|
+ SDL↔Compose bridges pick the cinterop from it.
|
||||||
|
|
||||||
One Gradle module per upstream artifact; the directory mirrors the upstream
|
One Gradle module per upstream artifact; the directory mirrors the upstream
|
||||||
`compose/` path, the gradle path is kept short (redirected via `projectDir`).
|
`compose/` path, and the gradle path mirrors the directory (no `projectDir`
|
||||||
|
redirection).
|
||||||
|
|
||||||
```
|
```
|
||||||
compose/
|
compose/
|
||||||
├── ui/
|
├── ui/
|
||||||
│ ├── ui/ → :ui — androidx.compose.ui.* CORE (Modifier, LayoutNode,
|
│ ├── ui/ → :compose:ui:ui - androidx.compose.ui.* CORE (Modifier, LayoutNode,
|
||||||
│ │ composition, semantics, input, focus) + com.compose.sdl.* —
|
│ │ composition, semantics, input, focus) + com.compose.sdl.* -
|
||||||
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
|
│ │ the Skia RenderBackend + GPU bridges + the SDL↔Compose
|
||||||
│ │ bridges (events / clipboard / cursors / window). Depends on
|
│ │ bridges (events / clipboard / cursors / window). Depends on
|
||||||
│ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text +
|
│ │ :ui-graphics, :ui-text, :sdl-core. (ui-graphics + ui-text +
|
||||||
│ │ the sdl3 cinterop were split OUT — upstream layout.)
|
│ │ the sdl3 cinterop were split OUT - upstream layout.)
|
||||||
│ ├── ui-graphics/ → :ui-graphics — androidx.compose.ui.graphics.* + the Skia actuals
|
│ ├── ui-graphics/ → :compose:ui:ui-graphics - androidx.compose.ui.graphics.* + the Skia actuals
|
||||||
│ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache,
|
│ │ (SkiaBackedCanvas/Path/Paint, GraphicsLayer, SkiaImageCache,
|
||||||
│ │ painter/image + resource seams). → skiko; SDL-free.
|
│ │ painter/image + resource seams). → skiko; SDL-free.
|
||||||
│ ├── ui-text/ → :ui-text — androidx.compose.ui.text.* + the skiko text engine
|
│ ├── ui-text/ → :compose:ui:ui-text - androidx.compose.ui.text.* + the skiko text engine
|
||||||
│ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont,
|
│ │ (SkiaParagraph → NativeParagraphOps → SkiaFonts, IconFont,
|
||||||
│ │ NamedFont). → :ui-graphics, skiko; SDL-free.
|
│ │ NamedFont). → :ui-graphics, skiko; SDL-free.
|
||||||
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
│ ├── ui-util/ → :compose:ui:ui-util - androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
||||||
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
│ ├── ui-geometry/ → :compose:ui:ui-geometry - androidx.compose.ui.geometry.*
|
||||||
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
│ ├── ui-unit/ → :compose:ui:ui-unit - androidx.compose.ui.unit.*
|
||||||
│ ├── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
|
│ ├── ui-backhandler/ → :compose:ui:ui-backhandler - androidx.compose.ui.backhandler.*
|
||||||
│ └── ui-tooling-preview/ → :ui-tooling-preview — androidx.compose.ui.tooling.preview.*
|
│ └── ui-tooling-preview/ → :compose:ui:ui-tooling-preview - androidx.compose.ui.tooling.preview.*
|
||||||
│ (the common @Preview + PreviewParameterProvider,
|
│ (the common @Preview + PreviewParameterProvider,
|
||||||
│ vendored verbatim; the Maven artifact ships no
|
│ vendored verbatim; the Maven artifact ships no
|
||||||
│ mingwX64/linux klibs). IDE-only metadata — previews
|
│ mingwX64/linux klibs). IDE-only metadata - previews
|
||||||
│ render through the apps' jvm parity targets.
|
│ render through the apps' jvm parity targets.
|
||||||
├── animation/
|
├── animation/
|
||||||
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.*
|
│ ├── animation-core/ → :compose:animation:animation-core - androidx.compose.animation.core.*
|
||||||
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core)
|
│ ├── animation/ → :compose:animation:animation - androidx.compose.animation.* (non-core)
|
||||||
│ └── animation-graphics/ → :animation-graphics — androidx.compose.animation.graphics.*
|
│ └── animation-graphics/ → :compose:animation:animation-graphics - androidx.compose.animation.graphics.*
|
||||||
├── foundation/
|
├── foundation/
|
||||||
│ ├── foundation/ → :foundation — androidx.compose.foundation.*
|
│ ├── foundation/ → :compose:foundation:foundation - androidx.compose.foundation.*
|
||||||
│ └── foundation-layout/ → :foundation-layout — androidx.compose.foundation.layout.*
|
│ └── foundation-layout/ → :compose:foundation:foundation-layout - androidx.compose.foundation.layout.*
|
||||||
├── material3/
|
├── material3/
|
||||||
│ └── material3/ → :material3 — androidx.compose.material3.*
|
│ ├── material3/ → :compose:material3:material3 - androidx.compose.material3.*
|
||||||
|
│ └── adaptive/ → the material3-adaptive family, VENDORED VERBATIM.
|
||||||
|
│ │ org.jetbrains.compose.material3.adaptive:* publishes
|
||||||
|
│ │ ios + macosArm64 ONLY - no linux, no mingw (issue #4).
|
||||||
|
│ │ Nothing in the sources blocks those targets: every
|
||||||
|
│ │ actual the native legs need already exists upstream
|
||||||
|
│ │ (nonAndroidMain / skikoMain / nativeMain), and the one
|
||||||
|
│ │ real dependency, androidx.window:window-core, ALREADY
|
||||||
|
│ │ publishes linux + mingw + macos. The artifacts were
|
||||||
|
│ │ missing purely because upstream never enabled the
|
||||||
|
│ │ targets - so this port is vendor-only, ZERO source
|
||||||
|
│ │ edits. Upstream's skikoMain (the ~75 l10n tables +
|
||||||
|
│ │ Strings / drag-handle actuals) lands in src/vendor/
|
||||||
|
│ │ native/ next to upstream's own nativeMain actuals;
|
||||||
|
│ │ they actualise disjoint expects so they coexist.
|
||||||
|
│ │ com.bitsycore.compose.material3.adaptive:<module>.
|
||||||
|
│ ├── adaptive/ → :compose:material3:adaptive:adaptive - WindowAdaptiveInfo /
|
||||||
|
│ │ Posture / window size class. api-exposes window-core.
|
||||||
|
│ ├── adaptive-layout/ → :compose:material3:adaptive:adaptive-layout - ListDetail /
|
||||||
|
│ │ SupportingPane / ThreePane scaffolds + pane motion
|
||||||
|
│ ├── adaptive-navigation/ → :compose:material3:adaptive:adaptive-navigation -
|
||||||
|
│ │ ThreePaneScaffoldNavigator + back behaviour
|
||||||
|
│ └── adaptive-navigation3/ → :compose:material3:adaptive:adaptive-navigation3 -
|
||||||
|
│ SceneStrategy bindings for nav3 (→ :navigation3-ui)
|
||||||
├── material/
|
├── material/
|
||||||
│ └── material-ripple/ → :material-ripple — androidx.compose.material.ripple.*
|
│ └── material-ripple/ → :compose:material:material-ripple - androidx.compose.material.ripple.*
|
||||||
└── desktop/native/window/ → :desktop-native-window — nativeComposeApp { Window(...) {} }
|
└── desktop/native/desktop-native-window/ → :compose:desktop:native:desktop-native-window - nativeComposeApp { Window(...) {} }
|
||||||
multi-window shell + SDL3 main loop; nativeComposeWindow()
|
multi-window shell + SDL3 main loop; nativeComposeWindow()
|
||||||
wrapper (project app-shell, not an upstream CMP artifact).
|
wrapper (project app-shell, not an upstream CMP artifact).
|
||||||
Published as com.bitsycore.compose:desktop-native-window.
|
Published as com.bitsycore.compose:desktop-native-window.
|
||||||
|
|
||||||
sdl/
|
sdl/
|
||||||
└── sdl-core/ → :sdl-core — the NAKED sdl3 cinterop + platform primitives
|
└── sdl-core/ → :sdl:sdl-core - the NAKED sdl3 cinterop + platform primitives
|
||||||
(zero Compose dep, like skiko); the single `sdl3` cinterop
|
(zero Compose dep, like skiko); the single `sdl3` cinterop
|
||||||
lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core.
|
lives here. :ui depends on it. com.bitsycore.compose.sdl:sdl-core.
|
||||||
|
|
||||||
utils/
|
utils/
|
||||||
└── material-symbols/ → :material-symbols — codepoints + all three style objects
|
└── material-symbols/ → :utils:material-symbols - codepoints + all three style objects
|
||||||
(Outlined / Rounded / Sharp). COMMON API (usable from
|
(Outlined / Rounded / Sharp). COMMON API (usable from
|
||||||
shared app code) + per-stack actuals: native renders
|
shared app code) + per-stack actuals: native renders
|
||||||
via :foundation IconFontIcon (Skia), jvm() via Skiko directly
|
via :foundation IconFontIcon (Skia), jvm() via Skiko directly
|
||||||
(Typeface.makeClone per axes — upstream's FontCache
|
(Typeface.makeClone per axes - upstream's FontCache
|
||||||
drops variationSettings from its key). Its commonMain
|
drops variationSettings from its key). Its commonMain
|
||||||
declares official Maven compose coords — the root
|
declares official Maven compose coords - the root
|
||||||
build's FULL-COMMONIZATION BRIDGE substitutes the
|
build's FULL-COMMONIZATION BRIDGE substitutes the
|
||||||
whole ui / foundation / animation / material3 /
|
whole ui / foundation / animation / material3 /
|
||||||
nav3-ui / components-resources family to project
|
nav3-ui / components-resources family to project
|
||||||
@@ -127,7 +151,7 @@ utils/
|
|||||||
jvmProcessResources stages the same fonts (jvm).
|
jvmProcessResources stages the same fonts (jvm).
|
||||||
|
|
||||||
components/
|
components/
|
||||||
└── resources/library/ → :components-resources — the OFFICIAL Compose resources
|
└── resources/components-resources/ → :components:resources:components-resources - the OFFICIAL Compose resources
|
||||||
runtime (org.jetbrains.compose.components:
|
runtime (org.jetbrains.compose.components:
|
||||||
components-resources), VENDORED from the
|
components-resources), VENDORED from the
|
||||||
compose-multiplatform UMBRELLA repo (the first
|
compose-multiplatform UMBRELLA repo (the first
|
||||||
@@ -137,11 +161,63 @@ components/
|
|||||||
DomXmlParser (upstream's is Darwin NSXMLParser),
|
DomXmlParser (upstream's is Darwin NSXMLParser),
|
||||||
image decode via the :ui EncodedImageDecoder hook
|
image decode via the :ui EncodedImageDecoder hook
|
||||||
(Skia), NamedFont registration, SDL locale/theme env.
|
(Skia), NamedFont registration, SDL locale/theme env.
|
||||||
Apps' JVM targets keep the Maven artifact — the
|
Apps' JVM targets keep the Maven artifact - the
|
||||||
generated Res accessors work against BOTH.
|
generated Res accessors work against BOTH.
|
||||||
|
|
||||||
|
koin/ → the Koin DI modules upstream stops publishing for desktop native.
|
||||||
|
├── koin-core-viewmodel/ → :koin:koin-core-viewmodel - org.koin.viewmodel.* + module DSL
|
||||||
|
├── koin-compose/ → :koin:koin-compose - org.koin.compose.*
|
||||||
|
├── koin-compose-viewmodel/ → :koin:koin-compose-viewmodel - org.koin.compose.viewmodel.*
|
||||||
|
└── koin-compose-navigation3/ → :koin:koin-compose-navigation3 - org.koin.compose.navigation3.*
|
||||||
|
VENDORED verbatim from InsertKoinIO/koin
|
||||||
|
(KOIN_REF). koin-core ITSELF publishes mingwX64
|
||||||
|
+ linux and is a plain Maven dep - only these
|
||||||
|
four stop at apple+android upstream. NOTE it is
|
||||||
|
the NAV3 module, not koin-compose-viewmodel-
|
||||||
|
navigation: that one sits on nav2's
|
||||||
|
navigation-compose, which has no mingwX64/linux
|
||||||
|
klibs under EITHER coordinate set.
|
||||||
|
com.bitsycore.koin:<module>.
|
||||||
|
|
||||||
|
coil/ → Coil 3 image loading. Upstream ships linux but NO mingwX64 for
|
||||||
|
│ core/network/svg, and NO desktop native at all for the compose
|
||||||
|
│ layer. VENDORED verbatim from coil-kt/coil (COIL_REF).
|
||||||
|
├── coil-core/ → :coil:coil-core - coil3.* core, Skia-based decode
|
||||||
|
├── coil/ → :coil:coil - singleton ImageLoader facade
|
||||||
|
├── coil-compose-core/ → :coil:coil-compose-core - AsyncImage internals
|
||||||
|
├── coil-compose/ → :coil:coil-compose - public compose entry points
|
||||||
|
├── coil-svg/ → :coil:coil-svg - SVG via skiko SVGDOM
|
||||||
|
├── coil-network-core/ → :coil:coil-network-core - fetcher plumbing
|
||||||
|
└── coil-network-ktor3/ → :coil:coil-network-ktor3 - Ktor 3 fetcher
|
||||||
|
Upstream's nonAndroid / nonJvmCommon /
|
||||||
|
nonJsCommon sets all apply here (no android /
|
||||||
|
jvm / js target) but must stay ABOVE nativeMain
|
||||||
|
- they declare expects whose actuals live in
|
||||||
|
nativeMain. coil-core / coil-svg /
|
||||||
|
coil-compose-core use org.jetbrains.skia
|
||||||
|
directly, so they carry TWO PARALLEL skiko
|
||||||
|
branches (official for macOS/Linux, the
|
||||||
|
bitsycore fork for mingwX64) exactly like
|
||||||
|
:compose:ui:ui-graphics. com.bitsycore.coil3:<m>.
|
||||||
|
|
||||||
|
pulse/ → Pulse MVI. Upstream (bitsycore/pulse-mvi, PULSE_REF) targets
|
||||||
|
│ android / jvm / ios / js / wasm and publishes NO desktop-native
|
||||||
|
│ artifact, so all four modules are vendored. commonMain only.
|
||||||
|
├── pulse/ → :pulse:pulse - MVI core (store/state/intents)
|
||||||
|
├── pulse-viewmodel/ → :pulse:pulse-viewmodel - ViewModel binding
|
||||||
|
├── pulse-savedstate/ → :pulse:pulse-savedstate - SavedStateHandle persistence
|
||||||
|
└── pulse-compose/ → :pulse:pulse-compose - Compose bindings
|
||||||
|
Published as com.bitsycore.compose.desktop.native
|
||||||
|
.pulse:<module>, NOT upstream's com.bitsycore.lib:
|
||||||
|
<module>. Pulse is the one vendored dep that is
|
||||||
|
ITSELF a com.bitsycore library, so the republish is
|
||||||
|
namespaced under this project rather than a bare
|
||||||
|
com.bitsycore.pulse that would read like upstream;
|
||||||
|
the bridge substitutes the upstream coord on
|
||||||
|
native configs.
|
||||||
|
|
||||||
navigation3/
|
navigation3/
|
||||||
└── navigation3-ui/ → :navigation3-ui — androidx.navigation3.ui.* + scene machinery,
|
└── navigation3-ui/ → :navigation3:navigation3-ui - androidx.navigation3.ui.* + scene machinery,
|
||||||
VENDORED verbatim from upstream (SET_FOLDER manifest).
|
VENDORED verbatim from upstream (SET_FOLDER manifest).
|
||||||
Navigation 3's runtime layers (navigation3-runtime,
|
Navigation 3's runtime layers (navigation3-runtime,
|
||||||
lifecycle-viewmodel-navigation3) are real Maven KMP
|
lifecycle-viewmodel-navigation3) are real Maven KMP
|
||||||
@@ -152,21 +228,21 @@ navigation3/
|
|||||||
(700ms fades, predictive-pop spring/scaleOut) because
|
(700ms fades, predictive-pop spring/scaleOut) because
|
||||||
the upstream macos actual ships all-None (no animation).
|
the upstream macos actual ships all-None (no animation).
|
||||||
|
|
||||||
demo/ → :demo — flagship showcase app (30+ screens) + the CLI probe suite.
|
demo/ → :demo - flagship showcase app (30+ screens) + the CLI probe suite.
|
||||||
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. MULTIPLATFORM like :demo
|
apidemo/ → :apidemo - Postman-style REST API manager. MULTIPLATFORM like :demo
|
||||||
(`./gradlew :apidemo:run`): the whole UI lives in commonMain
|
(`./gradlew :apidemo:run`): the whole UI lives in commonMain
|
||||||
against the official Maven coords; SDL-backed APIs go through
|
against the official Maven coords; SDL-backed APIs go through
|
||||||
expect/actual seams (compat/Compat.kt — native actuals delegate
|
expect/actual seams (compat/Compat.kt - native actuals delegate
|
||||||
to com.compose.sdl, jvm actuals use AWT + upstream desktop).
|
to com.compose.sdl, jvm actuals use AWT + upstream desktop).
|
||||||
mTLS / TLS-chain inspection stays native-only (bundled libcurl).
|
mTLS / TLS-chain inspection stays native-only (bundled libcurl).
|
||||||
DOGFOODS the bridge plugin: data.kres packaging + app icon come
|
DOGFOODS the bridge plugin: data.kres packaging + app icon come
|
||||||
from compose.desktop.native {}; the font pipeline (Noto +
|
from compose.desktop.native {}; the font pipeline (Noto +
|
||||||
Material Symbols subsetting) comes from the shared buildSrc
|
Material Symbols subsetting) comes from the shared buildSrc
|
||||||
helper (registerComposeFontBundling).
|
helper (registerComposeFontBundling).
|
||||||
buildSrc/ → shared build logic for the app modules: ComposeFontBundling.kt —
|
buildSrc/ → shared build logic for the app modules: ComposeFontBundling.kt -
|
||||||
registerComposeFontBundling { } (everything OPT-IN; no flag = no-op)
|
registerComposeFontBundling { } (everything OPT-IN; no flag = no-op)
|
||||||
registers downloadNotoFonts (bundleNotoSans / bundleNotoSansMono /
|
registers downloadNotoFonts (bundleNotoSans / bundleNotoSansMono /
|
||||||
autoDetectNotoSansMono), detects the Material Symbols styles the
|
autoDetectNotoSansMono), detects the Material Symbols styles the
|
||||||
@@ -179,23 +255,23 @@ gradle-plugin/
|
|||||||
└── compose-desktop-native-bridge/ → the CONSUMER-side bridge as a published Gradle plugin
|
└── compose-desktop-native-bridge/ → the CONSUMER-side bridge as a published Gradle plugin
|
||||||
(id com.bitsycore.compose-desktop-native.bridge, applies to
|
(id com.bitsycore.compose-desktop-native.bridge, applies to
|
||||||
Settings or Project). An INCLUDED build (pluginManagement.
|
Settings or Project). An INCLUDED build (pluginManagement.
|
||||||
includeBuild in settings.gradle.kts), NOT a subproject — a
|
includeBuild in settings.gradle.kts), NOT a subproject - a
|
||||||
plugins{} block can only resolve plugins from repositories or
|
plugins{} block can only resolve plugins from repositories or
|
||||||
included builds; :demo and :apidemo apply it from source
|
included builds; :demo and :apidemo apply it from source
|
||||||
(dogfooding: data.kres packaging — and apidemo's app icon —
|
(dogfooding: data.kres packaging - and apidemo's app icon -
|
||||||
run through the plugin).
|
run through the plugin).
|
||||||
Three halves: (1) substitution — third-party apps declare
|
Three halves: (1) substitution - third-party apps declare
|
||||||
OFFICIAL CMP coords in commonMain and native configurations
|
OFFICIAL CMP coords in commonMain and native configurations
|
||||||
swap in the published com.bitsycore.compose.sdl klibs
|
swap in the published com.bitsycore.compose.sdl klibs
|
||||||
(version defaults to the plugin's own; override:
|
(version defaults to the plugin's own; override:
|
||||||
composeDesktopNative.version property; disable:
|
composeDesktopNative.version property; disable:
|
||||||
composeDesktopNative.substitution=false — set repo-wide in
|
composeDesktopNative.substitution=false - set repo-wide in
|
||||||
gradle.properties since the root build's FULL-COMMONIZATION
|
gradle.properties since the root build's FULL-COMMONIZATION
|
||||||
BRIDGE substitutes to project modules in-repo); (2) data.kres
|
BRIDGE substitutes to project modules in-repo); (2) data.kres
|
||||||
packaging (package<Variant>ComposeResources<Target> per native
|
packaging (package<Variant>ComposeResources<Target> per native
|
||||||
executable, honours -PcompressResources); (3) the
|
executable, honours -PcompressResources); (3) the
|
||||||
compose.desktop.native { entryPoint / icon {} } DSL (.rgba
|
compose.desktop.native { entryPoint / icon {} } DSL (.rgba
|
||||||
runtime icons + windres .exe embed — injected into
|
runtime icons + windres .exe embed - injected into
|
||||||
hand-declared executables too). Published by the WINDOWS
|
hand-declared executables too). Published by the WINDOWS
|
||||||
publish job.
|
publish job.
|
||||||
scripts/ → vendor-sync + python helper scripts (compose-coverage = API
|
scripts/ → vendor-sync + python helper scripts (compose-coverage = API
|
||||||
@@ -206,14 +282,37 @@ libs/ → gitignored per-host static SDL3 output of
|
|||||||
scripts/build-sdl/build-all.py on Windows
|
scripts/build-sdl/build-all.py on Windows
|
||||||
```
|
```
|
||||||
|
|
||||||
Module PATHS stay short (`:ui`, `:foundation`, `:desktop-native-window`, …) —
|
Module PATHS mirror the DIRECTORY 1:1 - `:compose:ui:ui`,
|
||||||
`settings.gradle.kts` redirects `projectDir` for each so build files across
|
`:compose:foundation:foundation`, `:compose:desktop:native:desktop-native-window`,
|
||||||
the repo stay terse. `androidx.collection` is a plain Maven dependency
|
`:sdl:sdl-core`, `:utils:material-symbols`, `:navigation3:navigation3-ui`,
|
||||||
(`androidx.collection:collection`), not a module — same as other simple
|
`:components:resources:components-resources`. There is NO `projectDir` redirection in
|
||||||
|
`settings.gradle.kts`; a plain `include(...)` per module is the whole story, and
|
||||||
|
the intermediate container projects (`:compose`, `:compose:ui`, …) hold no code.
|
||||||
|
|
||||||
|
**INVARIANT: the leaf directory IS the published artifactId.** Don't break it to
|
||||||
|
mirror an upstream directory name. KMP derives every target publication's
|
||||||
|
coordinate as `<project.name>-<target>`, and a target DISABLED on the building
|
||||||
|
host (all Apple targets on Windows/Linux) has no publication object to retarget
|
||||||
|
afterwards - while the root `kotlinMultiplatform` metadata, which the WINDOWS
|
||||||
|
publish job owns, still emits an `available-at` for it. A mismatched leaf
|
||||||
|
therefore ships a root module pointing at a nonexistent `<leaf>-macosarm64`,
|
||||||
|
breaking macOS consumers, and no `artifactId = …` override can reach it. Two
|
||||||
|
directories are renamed away from upstream's layout to hold this:
|
||||||
|
`compose/desktop/native/desktop-native-window` (upstream: `window`) and
|
||||||
|
`components/resources/components-resources` (upstream: `library` + a hand-set
|
||||||
|
artifactId). `scripts/compose-coverage.py`'s `kTargets` carries the
|
||||||
|
upstream→local mapping for the latter.
|
||||||
|
|
||||||
|
`androidx.collection` is a plain Maven dependency
|
||||||
|
(`androidx.collection:collection`), not a module - same as other simple
|
||||||
androidx KMP libs.
|
androidx KMP libs.
|
||||||
|
|
||||||
## Dependency graph
|
## Dependency graph
|
||||||
|
|
||||||
|
Written with LEAF names for readability (`:ui` = `:compose:ui:ui`,
|
||||||
|
`:foundation` = `:compose:foundation:foundation`, …); build files use the full
|
||||||
|
path shown in the module layout above.
|
||||||
|
|
||||||
```
|
```
|
||||||
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
|
:ui ← :animation-core ← :animation ← :foundation ← :material3 ← :demo, :apidemo
|
||||||
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
|
:ui ← :foundation-layout ←──────────────────────┘ ↑ ↑
|
||||||
@@ -224,7 +323,7 @@ androidx KMP libs.
|
|||||||
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
|
All edges are `api`, so a consumer of `:foundation` / `:material3` transitively
|
||||||
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
|
sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :ui-util`;
|
||||||
`:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`;
|
`:ui-unit → :ui-geometry, :ui-util`; `:ui-backhandler → :ui-util, navigationevent`;
|
||||||
`:sdl-core → sdl3 cinterop (NAKED — no Compose)`;
|
`:sdl-core → sdl3 cinterop (NAKED - no Compose)`;
|
||||||
`:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`;
|
`:ui-graphics → :ui-geometry, :ui-unit, :ui-util, skiko`;
|
||||||
`:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`;
|
`:ui-text → :ui-graphics, :ui-unit, :ui-util, skiko`;
|
||||||
`:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
|
`:ui → :ui-graphics, :ui-text, :sdl-core, :ui-util, :ui-geometry, :ui-unit, :ui-backhandler`; `:animation-core → :ui`;
|
||||||
@@ -232,6 +331,14 @@ sees the split modules. Full DAG: `:ui-util → collection`; `:ui-geometry → :
|
|||||||
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
|
`:foundation → :animation, :foundation-layout, :animation-core, :ui`;
|
||||||
`:material-ripple → :foundation, :animation-core`;
|
`:material-ripple → :foundation, :animation-core`;
|
||||||
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
|
`:material3 → :foundation, :material-ripple, :animation-core, :foundation-layout`.
|
||||||
|
The adaptive family sits beside material3 rather than under it - it depends on
|
||||||
|
`:ui` / `:foundation` / `:animation-core`, NOT on `:material3`:
|
||||||
|
`:adaptive → :ui, :foundation, window-core`;
|
||||||
|
`:adaptive-layout → :adaptive, :ui, :animation-core, :animation, :foundation,
|
||||||
|
:foundation-layout, :ui-geometry, collection, window-core`;
|
||||||
|
`:adaptive-navigation → :adaptive-layout, :foundation, :ui-util`;
|
||||||
|
`:adaptive-navigation3 → :adaptive-navigation, :navigation3-ui, collection,
|
||||||
|
navigationevent-compose`.
|
||||||
|
|
||||||
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
|
`:ui` is the Compose core + the Skia RenderBackend + the SDL↔Compose bridges; it
|
||||||
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
|
sits on `:ui-graphics` / `:ui-text` (the graphics/text primitives + their skiko
|
||||||
@@ -241,28 +348,28 @@ Everything above `:ui` touches renderer internals only via its public surface.
|
|||||||
`:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
|
`:desktop-native-window` depends on `:ui` + `:foundation` (needs `LazyList`-style scaffolding to
|
||||||
install the popup / scaffold layer at the composition root).
|
install the popup / scaffold layer at the composition root).
|
||||||
|
|
||||||
## Vendoring philosophy — read this before writing any `androidx.compose.*` code
|
## Vendoring philosophy - read this before writing any `androidx.compose.*` code
|
||||||
|
|
||||||
**Prefer vendoring verbatim from upstream Compose Multiplatform over hand-rolling anything.**
|
**Prefer vendoring verbatim from upstream Compose Multiplatform over hand-rolling anything.**
|
||||||
|
|
||||||
Every module that ships `androidx.compose.*` code carries a
|
Every module that ships `androidx.compose.*` code carries a
|
||||||
`<module>/compose-fork.txt` manifest. Each one declares its upstream repo +
|
`<module>/compose-fork.txt` manifest. Each one declares its upstream repo +
|
||||||
pinned ref up top with `SET_REPO=<https-url>@<ref>`, where `<ref>` is normally a
|
pinned ref up top with `SET_REPO=<https-url>@<ref>`, where `<ref>` is normally a
|
||||||
`<VARNAME>` resolved from `scripts/compose-fork/compose.properties` — a
|
`<VARNAME>` resolved from `scripts/compose-fork/compose.properties` - a
|
||||||
`NAME=value` file that version-tags every pinned ref in ONE place (e.g.
|
`NAME=value` file that version-tags every pinned ref in ONE place (e.g.
|
||||||
`COMPOSE_CORE_REF` for compose-multiplatform-core, `COMPOSE_REF` for the
|
`COMPOSE_CORE_REF` for compose-multiplatform-core, `COMPOSE_REF` for the
|
||||||
compose-multiplatform umbrella repo that `:components-resources` vendors from).
|
compose-multiplatform umbrella repo that `:components-resources` vendors from).
|
||||||
There is no implicit default — `SET_REPO` is required. `scripts/compose-fork/sync.sh`
|
There is no implicit default - `SET_REPO` is required. `scripts/compose-fork/sync.sh`
|
||||||
walks all manifests and copies each selected file byte-for-byte from the pinned
|
walks all manifests and copies each selected file byte-for-byte from the pinned
|
||||||
checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into
|
checkout (each distinct repo sparse-cloned to `../cmp-ref[-<name>]`) into
|
||||||
`<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The
|
`<module>/src/vendor/{common,native,skikoRenderer}/kotlin/`. The
|
||||||
`src/vendor/` tree is **gitignored** — you don't check it in, you re-sync
|
`src/vendor/` tree is **gitignored** - you don't check it in, you re-sync
|
||||||
on demand.
|
on demand.
|
||||||
|
|
||||||
Manifests are **folder-style**: a `SET_FOLDER=<module>/src` line sets an upstream
|
Manifests are **folder-style**: a `SET_FOLDER=<module>/src` line sets an upstream
|
||||||
base, then `<sourceSet>/kotlin/ -> src/vendor/<area>/kotlin/` grabs a whole
|
base, then `<sourceSet>/kotlin/ -> src/vendor/<area>/kotlin/` grabs a whole
|
||||||
source set (every `.kt` under it). `!<sourceSet>/kotlin/<pkg>/<File>.kt` refuses
|
source set (every `.kt` under it). `!<sourceSet>/kotlin/<pkg>/<File>.kt` refuses
|
||||||
one file inside a grabbed folder — use it for files hand-vendored + edited under
|
one file inside a grabbed folder - use it for files hand-vendored + edited under
|
||||||
`src/{commonMain,…}` so the folder copy doesn't shadow them, or for upstream
|
`src/{commonMain,…}` so the folder copy doesn't shadow them, or for upstream
|
||||||
files the port doesn't want. A plain `<src> -> <dest>` still pins (or renames on
|
files the port doesn't want. A plain `<src> -> <dest>` still pins (or renames on
|
||||||
copy) a single file. Re-declare `SET_FOLDER` to draw from a second upstream module
|
copy) a single file. Re-declare `SET_FOLDER` to draw from a second upstream module
|
||||||
@@ -271,17 +378,17 @@ manifest in place: commented `# | src -> dest` lines under each folder
|
|||||||
directive list what it expands to, and a trailing `# >>> DIAGNOSTIC GAPS` block
|
directive list what it expands to, and a trailing `# >>> DIAGNOSTIC GAPS` block
|
||||||
lists every upstream `.kt` under SET_FOLDER that no directive selects (grouped by
|
lists every upstream `.kt` under SET_FOLDER that no directive selects (grouped by
|
||||||
source set) so new upstream files surface as comments to uncomment. That whole
|
source set) so new upstream files surface as comments to uncomment. That whole
|
||||||
annotated tail is generated — never hand-edit it; edit only the directives up
|
annotated tail is generated - never hand-edit it; edit only the directives up
|
||||||
top and re-run sync.
|
top and re-run sync.
|
||||||
|
|
||||||
Two categories of code live in each module:
|
Two categories of code live in each module:
|
||||||
|
|
||||||
1. **Vendored (in `src/vendor/…`)** — copied byte-for-byte from upstream.
|
1. **Vendored (in `src/vendor/…`)** - copied byte-for-byte from upstream.
|
||||||
Never hand-edit these. If upstream diverges, adjust the manifest or the
|
Never hand-edit these. If upstream diverges, adjust the manifest or the
|
||||||
pinned ref and re-sync. This is the bulk of the codebase (~1500 files
|
pinned ref and re-sync. This is the bulk of the codebase (~1500 files
|
||||||
across the modules).
|
across the modules).
|
||||||
2. **Project code (in `src/commonMain/`, `src/nativeMain/`,
|
2. **Project code (in `src/commonMain/`, `src/nativeMain/`,
|
||||||
`src/skikoRendererMain/`)** — code we author (project actuals, glue between
|
`src/skikoRendererMain/`)** - code we author (project actuals, glue between
|
||||||
Compose and SDL3, project-specific extensions).
|
Compose and SDL3, project-specific extensions).
|
||||||
|
|
||||||
### The 5 rules for adding upstream Compose surface
|
### The 5 rules for adding upstream Compose surface
|
||||||
@@ -299,7 +406,7 @@ Two categories of code live in each module:
|
|||||||
`src/vendor/native/kotlin/`).
|
`src/vendor/native/kotlin/`).
|
||||||
|
|
||||||
3. **If an upstream file needs a small edit to compile / behave
|
3. **If an upstream file needs a small edit to compile / behave
|
||||||
correctly for us, copy it locally and edit — MANUAL VENDORING,
|
correctly for us, copy it locally and edit - MANUAL VENDORING,
|
||||||
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
|
||||||
@@ -312,18 +419,27 @@ Two categories of code live in each module:
|
|||||||
// VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files
|
// VENDOR-BASE(COMPOSE_REF): <path> @ <tag> ← for umbrella-repo files
|
||||||
```
|
```
|
||||||
|
|
||||||
`scripts/compose-fork/check-vendor-drift.py` reads these at every ref
|
A file that is a fresh REIMPLEMENTATION rather than a derived copy (same
|
||||||
bump: it flags any file whose recorded base lags the current pin and
|
package + signatures, different body - see the fallback rule at the end of
|
||||||
|
this section) gets `// VENDOR-REIMPL: <path> @ <tag>` instead. That marks it
|
||||||
|
as deliberately NOT drift-tracked: upstream edits to the counterpart need no
|
||||||
|
reconciling, only a SIGNATURE change does.
|
||||||
|
`scripts/compose-fork/audit-exclusions.py` classifies every `!` exclusion as
|
||||||
|
manual vendor / reimpl / orphan and FAILS on any local counterpart carrying
|
||||||
|
neither marker, so nothing rots invisibly.
|
||||||
|
|
||||||
|
`scripts/compose-fork/check-vendor-drift.py` reads the VENDOR-BASE lines 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
|
(with the local clone) reports whether the upstream base ACTUALLY
|
||||||
changed base..pin — i.e. whether the copy needs hand-reconciling or
|
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
|
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
|
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
|
reconciled by hand. This is fine; do it when the edit is small and
|
||||||
the file is unlikely to churn upstream.
|
the file is unlikely to churn upstream.
|
||||||
|
|
||||||
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
|
4. **Skiko-specific things go in a `skikoRendererMain` source set.** When
|
||||||
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
|
upstream ships a `.skiko.kt` file that uses Skiko's Canvas / Paragraph / …,
|
||||||
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` — graphics
|
the `.skiko.kt` variant is fine to vendor into `skikoRendererMain` - graphics
|
||||||
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
|
into `:ui-graphics` (`SkiaBackedCanvas.skiko.kt`, `SkiaImageCache.kt`, …),
|
||||||
text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into
|
text into `:ui-text` (`SkiaParagraphEngine.kt`), the render backend into
|
||||||
`:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set;
|
`:ui` (`SkiaRenderBackend.kt`). All native targets attach this source set;
|
||||||
@@ -333,14 +449,14 @@ Two categories of code live in each module:
|
|||||||
ifdefs.** SDL3 already handles the platform differences for filesystem
|
ifdefs.** SDL3 already handles the platform differences for filesystem
|
||||||
paths (`SDL_GetBasePath`, `SDL_GetPrefPath`), clipboard, cursor,
|
paths (`SDL_GetBasePath`, `SDL_GetPrefPath`), clipboard, cursor,
|
||||||
fullscreen, high-DPI, subprocess launch, MessageBox, and more.
|
fullscreen, high-DPI, subprocess launch, MessageBox, and more.
|
||||||
`file dialog / "reveal in Finder or Explorer" / app-data path` — do
|
`file dialog / "reveal in Finder or Explorer" / app-data path` - do
|
||||||
it through SDL3 first, hand-roll the target-specific version only if
|
it through SDL3 first, hand-roll the target-specific version only if
|
||||||
SDL3 doesn't expose it (currently the file-open/save dialog uses
|
SDL3 doesn't expose it (currently the file-open/save dialog uses
|
||||||
SDL3's `SDL_ShowOpenFileDialog` / `SDL_ShowSaveFileDialog`).
|
SDL3's `SDL_ShowOpenFileDialog` / `SDL_ShowSaveFileDialog`).
|
||||||
|
|
||||||
If a piece of upstream is too Compose-specific to make sense on our
|
If a piece of upstream is too Compose-specific to make sense on our
|
||||||
stack (e.g. Android AWT layer, iOS `UIView`, JVM `Toolkit`), do a fresh
|
stack (e.g. Android AWT layer, iOS `UIView`, JVM `Toolkit`), do a fresh
|
||||||
reimpl in project code — same signature (same package, same params) so
|
reimpl in project code - same signature (same package, same params) so
|
||||||
call sites don't care.
|
call sites don't care.
|
||||||
|
|
||||||
## Source-set hierarchy (:ui only)
|
## Source-set hierarchy (:ui only)
|
||||||
@@ -353,11 +469,11 @@ the `sdl3` cinterop lives in `:sdl-core` (see below). `:ui`'s tree:
|
|||||||
commonMain
|
commonMain
|
||||||
└── nativeMain (vendored .native.kt + project native code)
|
└── nativeMain (vendored .native.kt + project native code)
|
||||||
├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath)
|
├── skikoRendererMain (Skia drawing pipeline; official Skiko on classpath)
|
||||||
│ ├── skikoRendererMacosMain (macOS-only Skia actuals — Metal bridge)
|
│ ├── skikoRendererMacosMain (macOS-only Skia actuals - Metal bridge)
|
||||||
│ └── skikoRendererLinuxMain (Linux-only Skia actuals — OpenGL)
|
│ └── skikoRendererLinuxMain (Linux-only Skia actuals - OpenGL)
|
||||||
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main.
|
│ attached to: macosArm64Main / linuxX64Main / linuxArm64Main.
|
||||||
└── skikoRendererMingwSharedMain
|
└── skikoRendererMingwSharedMain
|
||||||
└── skikoRendererMingwMain (mingwX64-only Skia actuals — the bitsycore
|
└── skikoRendererMingwMain (mingwX64-only Skia actuals - the bitsycore
|
||||||
skiko FORK: skiko-windows-x64.dll bound via
|
skiko FORK: skiko-windows-x64.dll bound via
|
||||||
an embedded GNU import lib; OpenGL context)
|
an embedded GNU import lib; OpenGL context)
|
||||||
attached to: mingwX64Main.
|
attached to: mingwX64Main.
|
||||||
@@ -375,22 +491,22 @@ The `sdl3` cinterop lives in the NAKED `:sdl-core` module
|
|||||||
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
|
(`sdl/sdl-core/src/nativeInterop/cinterop/sdl3.def`: SDL_Window / SDL_Event /
|
||||||
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
|
SDL_GetBasePath / clipboard / dialogs / GL+Metal context / SDL_Renderer-for-CPU-
|
||||||
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
|
raster). `:ui` depends on `:sdl-core` (`api`), so `:ui`'s renderer + SDL↔Compose
|
||||||
bridges — and `:desktop-native-window` downstream — see `sdl3.*` and inherit SDL3's static-lib +
|
bridges - and `:desktop-native-window` downstream - see `sdl3.*` and inherit SDL3's static-lib +
|
||||||
linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the
|
linker-opt propagation (the `.def` bakes in `staticLibraries = libSDL3.a` + the
|
||||||
per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps
|
per-OS `linkerOpts`, carried through the klib chain to the final exe, so apps
|
||||||
link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose
|
link SDL just by depending on `:ui`/`:desktop-native-window`). `:sdl-core` has NO Compose
|
||||||
dependency, so `:ui → :sdl-core` is cycle-free — the compose way (like `ui → skiko`).
|
dependency, so `:ui → :sdl-core` is cycle-free - the compose way (like `ui → skiko`).
|
||||||
|
|
||||||
## Density flow (Option B — layout in physical pixels)
|
## Density flow (Option B - layout in physical pixels)
|
||||||
|
|
||||||
We use the **physical-pixel layout flow** on HiDPI. Concretely:
|
We use the **physical-pixel layout flow** on HiDPI. Concretely:
|
||||||
|
|
||||||
- `SDL_WINDOW_HIGH_PIXEL_DENSITY` is set on the SDL window.
|
- `SDL_WINDOW_HIGH_PIXEL_DENSITY` is set on the SDL window.
|
||||||
- `LocalDensity` = the DPR (2.0 on Retina, 1.0 otherwise) — from
|
- `LocalDensity` = the DPR (2.0 on Retina, 1.0 otherwise) - from
|
||||||
`pixelWidth / windowWidth`.
|
`pixelWidth / windowWidth`.
|
||||||
- Constraints passed to `rootNode.measure(…)` are the **physical pixel** size,
|
- Constraints passed to `rootNode.measure(…)` are the **physical pixel** size,
|
||||||
not logical points.
|
not logical points.
|
||||||
- `renderBackend.beginFrame(1f)` — no renderer-side scale; layout already ran
|
- `renderBackend.beginFrame(1f)` - no renderer-side scale; layout already ran
|
||||||
in physical pixels.
|
in physical pixels.
|
||||||
- Pointer coords from SDL are logical points; they're multiplied by DPR
|
- Pointer coords from SDL are logical points; they're multiplied by DPR
|
||||||
before dispatch so the whole event pipeline is in the same physical-px
|
before dispatch so the whole event pipeline is in the same physical-px
|
||||||
@@ -399,7 +515,7 @@ We use the **physical-pixel layout flow** on HiDPI. Concretely:
|
|||||||
Consequence: `Modifier.width(20.dp)` at density 2 → 40 physical pixels wide.
|
Consequence: `Modifier.width(20.dp)` at density 2 → 40 physical pixels wide.
|
||||||
`onSizeChanged { it.width }` reports physical pixels. If you're passing pixel
|
`onSizeChanged { it.width }` reports physical pixels. If you're passing pixel
|
||||||
integers into pixel-based modifiers, use the lambda forms
|
integers into pixel-based modifiers, use the lambda forms
|
||||||
(`Modifier.offset { IntOffset(x, y) }`) — they take pixels directly.
|
(`Modifier.offset { IntOffset(x, y) }`) - they take pixels directly.
|
||||||
Passing raw `px.dp` will double-scale on Retina.
|
Passing raw `px.dp` will double-scale on Retina.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -413,14 +529,14 @@ Passing raw `px.dp` will double-scale on Retina.
|
|||||||
./gradlew :demo:runDebugExecutableLinuxX64
|
./gradlew :demo:runDebugExecutableLinuxX64
|
||||||
./gradlew :apidemo:runDebugExecutableLinuxX64
|
./gradlew :apidemo:runDebugExecutableLinuxX64
|
||||||
|
|
||||||
# Windows (from Windows — mingw cross-build from macOS/Linux fails at cinterop).
|
# Windows (from Windows - mingw cross-build from macOS/Linux fails at cinterop).
|
||||||
# Links the bitsycore skiko FORK from GitHub Packages (com.bitsycore.skiko:skiko:
|
# Links the bitsycore skiko FORK from maven.bitsycore.com (com.bitsycore.skiko:skiko:
|
||||||
# 0.150.1-mingw.1, override -PskikoMingwVersion); the bridge plugin drops
|
# 0.150.1-mingw.2, override -PskikoMingwVersion); the bridge plugin drops
|
||||||
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
|
# skiko-windows-x64.dll next to the exe (installWindowsSkiaDll).
|
||||||
gradlew.bat :demo:runDebugExecutableMingwX64
|
gradlew.bat :demo:runDebugExecutableMingwX64
|
||||||
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||||
|
|
||||||
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
|
# Stock JVM Compose Desktop (any host) - the parity reference: the SAME shared
|
||||||
# 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
|
./gradlew :apidemo:run
|
||||||
@@ -429,41 +545,41 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|||||||
### System dependencies
|
### System dependencies
|
||||||
|
|
||||||
SDL3 is **built from source as a static library on every OS** and linked
|
SDL3 is **built from source as a static library on every OS** and linked
|
||||||
straight into the executable — no brew/apt SDL packages. Build it once per host
|
straight into the executable - no brew/apt SDL packages. Build it once per host
|
||||||
with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements
|
with `python scripts/build-sdl/build-all.py`. Per-host toolchain requirements
|
||||||
and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
|
and the step breakdown are in [TOOLING.md](TOOLING.md#native-libraries).
|
||||||
|
|
||||||
Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux
|
Skia comes in through the Skiko klibs: the official Maven ones on macOS/Linux
|
||||||
(no runtime .so/.dylib — Skia is statically inside the klib), and the bitsycore
|
(no runtime .so/.dylib - Skia is statically inside the klib), and the bitsycore
|
||||||
fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next
|
fork on Windows, whose `skiko-windows-x64.dll` the bridge plugin provisions next
|
||||||
to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows
|
to the exe. So a macOS/Linux distributable is `<app>` + `data.kres`; a Windows
|
||||||
one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`.
|
one is `<app>.exe` + `data.kres` + `skiko-windows-x64.dll`.
|
||||||
|
|
||||||
## Runtime bundling — data.kres
|
## Runtime bundling - data.kres
|
||||||
|
|
||||||
Every app ships `<app>.exe` + `data.kres` (a STORED zip alongside the
|
Every app ships `<app>.exe` + `data.kres` (a STORED zip alongside the
|
||||||
executable, loaded via `SDL_GetBasePath()`). Contents:
|
executable, loaded via `SDL_GetBasePath()`). Contents:
|
||||||
|
|
||||||
- App drawables + files under `composeResources/{drawable,files}/`
|
- App drawables + files under `composeResources/{drawable,files}/`
|
||||||
- `font/NotoSans.ttf` — the default variable font. Bundling it is the APP's
|
- `font/NotoSans.ttf` - the default variable font. Bundling it is the APP's
|
||||||
job: each app opts in via `registerComposeFontBundling { bundleNotoSans =
|
job: each app opts in via `registerComposeFontBundling { bundleNotoSans =
|
||||||
true; … }` (buildSrc — every flag is opt-in, no flag = no-op), which
|
true; … }` (buildSrc - every flag is opt-in, no flag = no-op), which
|
||||||
registers its `downloadNotoFonts` task (into `<app>/build/fonts/`); the
|
registers its `downloadNotoFonts` task (into `<app>/build/fonts/`); the
|
||||||
library ships no download task. Pass `-PbundleDefaultFont=false` to skip.
|
library ships no download task. Pass `-PbundleDefaultFont=false` to skip.
|
||||||
- `font/NotoSansMono.ttf` — `autoDetectNotoSansMono = true` bundles it when
|
- `font/NotoSansMono.ttf` - `autoDetectNotoSansMono = true` bundles it when
|
||||||
`FontFamily.Monospace` appears in the app sources (demo);
|
`FontFamily.Monospace` appears in the app sources (demo);
|
||||||
`bundleNotoSansMono = true` forces it (apidemo's body font, loaded through
|
`bundleNotoSansMono = true` forces it (apidemo's body font, loaded through
|
||||||
its own seam)
|
its own seam)
|
||||||
- Material Symbols fonts for the styles the app **actually uses**
|
- Material Symbols fonts for the styles the app **actually uses**
|
||||||
(`bundleMaterialSymbols = true`) — the Zip task scans the app's Kotlin
|
(`bundleMaterialSymbols = true`) - the Zip task scans the app's Kotlin
|
||||||
sources for `MaterialSymbolsOutlined` / `Rounded` / `Sharp` and only
|
sources for `MaterialSymbolsOutlined` / `Rounded` / `Sharp` and only
|
||||||
bundles the fonts referenced.
|
bundles the fonts referenced.
|
||||||
- `-PsubsetIcons=true` (default on, opted into per app via
|
- `-PsubsetIcons=true` (default on, opted into per app via
|
||||||
`enableIconSubsetting = true` — apidemo yes, demo no):
|
`enableIconSubsetting = true` - apidemo yes, demo no):
|
||||||
`scripts/subset-material-symbols.py` scans app sources for
|
`scripts/subset-material-symbols.py` scans app sources for
|
||||||
`MaterialSymbols.<Name>` usage and hb-subsets each bundled font down to
|
`MaterialSymbols.<Name>` usage and hb-subsets each bundled font down to
|
||||||
just those glyphs. Needs `hb-subset` on PATH (`brew install harfbuzz` /
|
just those glyphs. Needs `hb-subset` on PATH (`brew install harfbuzz` /
|
||||||
`apt install harfbuzz-utils`) — falls back to the full font if absent.
|
`apt install harfbuzz-utils`) - falls back to the full font if absent.
|
||||||
|
|
||||||
## Vendor sync workflow
|
## Vendor sync workflow
|
||||||
|
|
||||||
@@ -472,61 +588,75 @@ executable, loaded via `SDL_GetBasePath()`). Contents:
|
|||||||
scripts/compose-fork/sync.sh
|
scripts/compose-fork/sync.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Upstream ref: `scripts/compose-fork/compose.properties` — set to a durable tag
|
Upstream refs live in `scripts/compose-fork/compose.properties`, one `NAME=value`
|
||||||
of `JetBrains/compose-multiplatform-core`. Bump the ref → re-sync → let the
|
per repo, each pinned to a durable TAG. Five repos are vendored now:
|
||||||
|
`COMPOSE_CORE_REF` (JetBrains/compose-multiplatform-core), `COMPOSE_REF` (the
|
||||||
|
compose-multiplatform umbrella), `KOIN_REF` (InsertKoinIO/koin), `COIL_REF`
|
||||||
|
(coil-kt/coil) and `PULSE_REF` (bitsycore/pulse-mvi). Each gets its own sibling
|
||||||
|
sparse clone (`../cmp-ref`, `../cmp-ref-koin`, …). Bump the ref → re-sync → let the
|
||||||
build tell you what broke. Per-module sync, manifest re-formatting, and the
|
build tell you what broke. Per-module sync, manifest re-formatting, and the
|
||||||
drift / vendor-clean guardrails are in
|
drift / vendor-clean guardrails are in
|
||||||
[TOOLING.md](TOOLING.md#vendoring-upstream-compose).
|
[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
|
||||||
|
|
||||||
### Renderer + main loop
|
### Renderer + main loop
|
||||||
- `compose/desktop/native/window/src/nativeMain/…/ComposeWindow.kt` — main loop,
|
- `compose/desktop/native/desktop-native-window/src/nativeMain/…/` - the window
|
||||||
recomposer lifecycle, SDL event dispatch, composition-local seeding.
|
module, one concern per file:
|
||||||
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` — the interface.
|
- `ComposeWindow.kt` - the PUBLIC surface (`ApplicationScope`, `Window()`,
|
||||||
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` — sealed driver picker
|
`nativeComposeApp`, `nativeComposeWindow`) + the main loop.
|
||||||
|
- `WindowInstance.kt` - one SDL window: backend + render backend + root host +
|
||||||
|
its own recomposer/composition, event handling, frame pump, teardown. This is
|
||||||
|
where composition-local seeding lives.
|
||||||
|
- `AppRuntime.kt` - the live-window registry + create/destroy lifecycle.
|
||||||
|
- `WindowAttributes.kt` - the Compose Desktop `Window()` attribute bundle.
|
||||||
|
- `FrameTiming.kt` - deterministic/virtual frame clocks and the
|
||||||
|
probe-quiescence hooks (`disableInfiniteAnimations`, `useVirtualFrameTime`,
|
||||||
|
`windowHasInvalidations`).
|
||||||
|
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` - the interface.
|
||||||
|
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` - sealed driver picker
|
||||||
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
|
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
|
||||||
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` —
|
- `compose/ui/ui/src/skikoRendererMain/…/renderer/skia/SkiaRenderBackend.kt` -
|
||||||
the Skia render backend (shared by every native target).
|
the Skia render backend (shared by every native target).
|
||||||
- `compose/ui/ui/src/skikoRendererMingwMain/…` — mingwX64-only Skia actuals
|
- `compose/ui/ui/src/skikoRendererMingwMain/…` - mingwX64-only Skia actuals
|
||||||
bound against the skiko fork's flat extern-C surface.
|
bound against the skiko fork's flat extern-C surface.
|
||||||
|
|
||||||
### Layout / composition wiring
|
### Layout / composition wiring
|
||||||
- `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` — root LayoutNode
|
- `compose/ui/ui/src/commonMain/…/node/ComposeRootHost.kt` - root LayoutNode
|
||||||
host, hit-test, event dispatch, snapshot observer.
|
host, hit-test, event dispatch, snapshot observer.
|
||||||
- `compose/ui/ui/src/commonMain/…/node/impl/ComposeOwner.kt` — the
|
- `compose/ui/ui/src/commonMain/…/node/impl/ComposeOwner.kt` - the
|
||||||
project `Owner` implementation + `ProjectOwnedLayer` (graphicsLayer / clip /
|
project `Owner` implementation + `ProjectOwnedLayer` (graphicsLayer / clip /
|
||||||
alpha bridge).
|
alpha bridge).
|
||||||
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
|
- `compose/ui/ui/src/commonMain/…/node/NodeApplier.kt`.
|
||||||
|
|
||||||
### Text (all in `:ui-text` now)
|
### Text (all in `:ui-text` now)
|
||||||
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` — the
|
- `compose/ui/ui-text/src/nativeMain/…/ui/text/SkiaParagraph.native.kt` - the
|
||||||
`Paragraph` actual (skiko-free; the 30 interface methods over plain data),
|
`Paragraph` actual (skiko-free; the 30 interface methods over plain data),
|
||||||
driving the skiko engine through the `NativeParagraphOps` seam.
|
driving the skiko engine through the `NativeParagraphOps` seam.
|
||||||
- `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt`
|
- `compose/ui/ui-text/src/skikoRendererMain/…/renderer/skia/SkiaParagraphEngine.kt`
|
||||||
— `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts`
|
- `SkiaParagraphOps` (builds/queries the real skiko `skparagraph`) + `SkiaFonts`
|
||||||
(data.kres bytes → skiko Typeface; the port's family/variable-axis model).
|
(data.kres bytes → skiko Typeface; the port's family/variable-axis model).
|
||||||
- `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` —
|
- `compose/ui/ui-text/src/nativeMain/…/ui/text/ParagraphFactories.native.kt` -
|
||||||
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
|
actuals for the `Paragraph(…)` / `ParagraphIntrinsics(…)` factory family.
|
||||||
|
|
||||||
### Icons
|
### Icons
|
||||||
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` —
|
- `compose/foundation/foundation/src/nativeMain/…/icons/IconFontIcon.kt` -
|
||||||
codepoint-based `Icon` composable + `MaterialIconAxes` /
|
codepoint-based `Icon` composable + `MaterialIconAxes` /
|
||||||
`MaterialIconAxisDefaults`.
|
`MaterialIconAxisDefaults`.
|
||||||
- `utils/material-symbols/src/…/MaterialSymbols{Outlined,Rounded,Sharp}.kt`.
|
- `utils/material-symbols/src/…/MaterialSymbols{Outlined,Rounded,Sharp}.kt`.
|
||||||
|
|
||||||
### Resources
|
### Resources
|
||||||
- `compose/ui/ui/src/commonMain/…/res/Res.kt` — the project's
|
- `compose/ui/ui/src/commonMain/…/res/Res.kt` - the project's
|
||||||
`androidx.compose.ui.res` reimpl (`Painter`, `ImageLoader`).
|
`androidx.compose.ui.res` reimpl (`Painter`, `ImageLoader`).
|
||||||
- `compose/ui/ui/src/nativeMain/…/ResourceIO.kt` — opens `data.kres` once via
|
- `compose/ui/ui/src/nativeMain/…/ResourceIO.kt` - opens `data.kres` once via
|
||||||
`SDL_GetBasePath()` + parses central directory; each entry served by an
|
`SDL_GetBasePath()` + parses central directory; each entry served by an
|
||||||
`fseek + fread`.
|
`fseek + fread`.
|
||||||
|
|
||||||
### Apps
|
### Apps
|
||||||
- `demo/src/nativeMain/kotlin/Main.kt` — sidebar demo (`--gpu`, `--screen`,
|
- `demo/src/nativeMain/kotlin/Main.kt` - sidebar demo (`--gpu`, `--screen`,
|
||||||
`--screenshot`).
|
`--screenshot`).
|
||||||
- `apidemo/src/nativeMain/kotlin/Main.kt` — API manager entry.
|
- `apidemo/src/nativeMain/kotlin/Main.kt` - API manager entry.
|
||||||
`apidemo/src/nativeMain/kotlin/UiCompat.kt` — project-local
|
`apidemo/src/nativeMain/kotlin/UiCompat.kt` - project-local
|
||||||
`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).
|
||||||
|
|
||||||
@@ -547,9 +677,23 @@ Open renderer / fidelity / release work is tracked in [PLAN.md](PLAN.md).
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
Kotlin standard style — plain `camelCase` for parameters, local variables,
|
Kotlin standard style for formatting (braces, wrapping, indentation).
|
||||||
and fields. **No `f`/`in`/`v` prefixes**, no SPIRTECH scheme (see the global
|
|
||||||
CLAUDE.md — that scheme is C-only).
|
Naming, as the code actually is - this paragraph used to claim "no prefixes",
|
||||||
|
which had not been true for a long time:
|
||||||
|
|
||||||
|
- **`m` for private/protected members** - `mWidth`, `mSurface`, `mCache`. NOT
|
||||||
|
`f`: the `f` prefix was retired across the project, so do not reintroduce it.
|
||||||
|
- **`v` for local variables** - `vWindow`, `vResult`.
|
||||||
|
- **`in` for function parameters** - `inTitle`, `inWidth`. Note Compose
|
||||||
|
`@Composable` parameters are the exception: they stay plain, because the name
|
||||||
|
is part of the call-site API (`title = `, `onCloseRequest = `).
|
||||||
|
- **`k` for private file-level constants** - `kMaxFrames`, `kIsWindows`.
|
||||||
|
- Public API surface is plain `camelCase` - prefixes are for implementation
|
||||||
|
detail only, and nothing prefixed should ever reach the klib dump.
|
||||||
|
|
||||||
|
VENDORED code under `src/vendor/` follows UPSTREAM's naming, untouched - it is
|
||||||
|
copied byte-for-byte, so never re-prefix anything in it.
|
||||||
|
|
||||||
Section headers inside a file, when useful:
|
Section headers inside a file, when useful:
|
||||||
|
|
||||||
@@ -566,31 +710,31 @@ In-function smaller scope:
|
|||||||
// Name
|
// Name
|
||||||
```
|
```
|
||||||
|
|
||||||
Function-level comments only where the name isn't self-documenting — avoid
|
Function-level comments only where the name isn't self-documenting - avoid
|
||||||
line-by-line commentary. Prefer KDoc (`/** … */`) over `/* … */` for docs
|
line-by-line commentary. Prefer KDoc (`/** … */`) over `/* … */` for docs
|
||||||
that should surface in tooling.
|
that should surface in tooling.
|
||||||
|
|
||||||
## Common pitfalls
|
## Common pitfalls
|
||||||
|
|
||||||
- **State changes don't repaint the UI** — check
|
- **State changes don't repaint the UI** - check
|
||||||
`Snapshot.sendApplyNotifications()` is being called each frame in the main
|
`Snapshot.sendApplyNotifications()` is being called each frame in the main
|
||||||
loop (`ComposeWindow.kt`). Without it, `mutableStateOf` writes never
|
loop (`ComposeWindow.kt`). Without it, `mutableStateOf` writes never
|
||||||
reach the recomposer.
|
reach the recomposer.
|
||||||
- **Physical-pixel modifiers double-scale on Retina** — under Option-B
|
- **Physical-pixel modifiers double-scale on Retina** - under Option-B
|
||||||
density, layout runs in physical pixels but `Modifier.width(20.dp)` still
|
density, layout runs in physical pixels but `Modifier.width(20.dp)` still
|
||||||
goes through `density.toPx()`. If you have a value already in physical
|
goes through `density.toPx()`. If you have a value already in physical
|
||||||
pixels (from `onSizeChanged`), convert it back to Dp via
|
pixels (from `onSizeChanged`), convert it back to Dp via
|
||||||
`with(LocalDensity.current) { pxInt.toDp() }` before passing to
|
`with(LocalDensity.current) { pxInt.toDp() }` before passing to
|
||||||
`Modifier.width(…)`, or use pixel-based lambdas
|
`Modifier.width(…)`, or use pixel-based lambdas
|
||||||
(`Modifier.offset { IntOffset(x, y) }`).
|
(`Modifier.offset { IntOffset(x, y) }`).
|
||||||
- **Skia's `saveLayer(bounds, paint)`** — GPU backends allocate the offscreen
|
- **Skia's `saveLayer(bounds, paint)`** - GPU backends allocate the offscreen
|
||||||
to `bounds`. If content inside translates beyond those bounds, it gets
|
to `bounds`. If content inside translates beyond those bounds, it gets
|
||||||
clipped. `SkiaCanvas.saveLayer` passes a huge fixed bounds to sidestep this.
|
clipped. `SkiaCanvas.saveLayer` passes a huge fixed bounds to sidestep this.
|
||||||
- **`Modifier.alpha` clips to bounds** — upstream contract:
|
- **`Modifier.alpha` clips to bounds** - upstream contract:
|
||||||
`Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`.
|
`Modifier.alpha(x)` desugars to `graphicsLayer(alpha = x, clip = true)`.
|
||||||
For a drag ghost that also translates, put `alpha` and `translationX` on
|
For a drag ghost that also translates, put `alpha` and `translationX` on
|
||||||
the SAME `graphicsLayer(...)` so clip stays false.
|
the SAME `graphicsLayer(...)` so clip stays false.
|
||||||
- **Substituted Maven modules hide their transitives from common metadata** —
|
- **Substituted Maven modules hide their transitives from common metadata** -
|
||||||
when the bridge swaps an official coord for a project module on native
|
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
|
configs, KGP's granular-metadata visibility check drops that Maven module's
|
||||||
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
|
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
|
||||||
@@ -598,74 +742,95 @@ that should surface in tooling.
|
|||||||
while per-target compilation is fine). Declare EVERY artifact the common
|
while per-target compilation is fine). Declare EVERY artifact the common
|
||||||
code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its
|
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
|
own bridge rule. Note only the WINDOWS publish job compiles common metadata
|
||||||
(it owns the root KotlinMultiplatform publications — the only host that
|
(it owns the root KotlinMultiplatform publications - the only host that
|
||||||
declares every target, so only its .module files carry the full variant
|
declares every target, so only its .module files carry the full variant
|
||||||
table; macOS-published roots left v0.1.15 without mingwX64 variants) —
|
table; macOS-published roots left v0.1.15 without mingwX64 variants) -
|
||||||
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
|
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
|
||||||
|
|
||||||
## Useful Gradle tricks
|
## Useful Gradle tricks
|
||||||
|
|
||||||
- `--args="--gpu=skia.opengl --screen=Buttons"` — pass CLI to the demo.
|
- `--args="--gpu=skia.opengl --screen=Buttons"` - pass CLI to the demo.
|
||||||
- `--info` — see cinterop classpath + include paths actually used.
|
- `--info` - see cinterop classpath + include paths actually used.
|
||||||
- After a module rename or IC-cache mismatch: nuke
|
- After a module rename or IC-cache mismatch: nuke
|
||||||
`demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native
|
`demo/build/kotlin-native-ic-cache` (or `apidemo/build/…`). Kotlin/Native
|
||||||
pins module IDs into its klib metadata; a stale cache surfaces as
|
pins module IDs into its klib metadata; a stale cache surfaces as
|
||||||
`Unknown dependent library com.bitsycore.compose.sdl:core` (or
|
`Unknown dependent library com.bitsycore.compose.sdl:core` (or
|
||||||
whatever the old module name was).
|
whatever the old module name was).
|
||||||
|
|
||||||
## Known Compatible — official Maven KMP artifacts used AS-IS
|
## Known Compatible - official Maven KMP artifacts used AS-IS
|
||||||
|
|
||||||
Before reimplementing or vendoring ANYTHING androidx, check Maven: a lot of
|
Before reimplementing or vendoring ANYTHING androidx, check Maven: a lot of
|
||||||
the architecture stack publishes real Kotlin/Native desktop klibs
|
the architecture stack publishes real Kotlin/Native desktop klibs
|
||||||
(mingwX64 + linuxX64/arm64 + macosArm64) and runs on this port unmodified.
|
(mingwX64 + linuxX64/arm64 + macosArm64) and runs on this port unmodified.
|
||||||
Sometimes the GOOGLE coordinates (`androidx.*`) have the K/N variant,
|
Sometimes the GOOGLE coordinates (`androidx.*`) have the K/N variant,
|
||||||
sometimes the JETBRAINS ones (`org.jetbrains.*`) — check both before
|
sometimes the JETBRAINS ones (`org.jetbrains.*`) - check both before
|
||||||
concluding an artifact "doesn't exist" for our targets.
|
concluding an artifact "doesn't exist" for our targets.
|
||||||
|
|
||||||
Verified in-tree (api-exposed by `:ui` unless noted):
|
Verified in-tree (api-exposed by `:ui` unless noted):
|
||||||
|
|
||||||
- `org.jetbrains.compose.runtime:runtime` / `runtime-saveable` 1.11.1 —
|
- `org.jetbrains.compose.runtime:runtime` / `runtime-saveable` 1.11.1 -
|
||||||
THE Compose runtime (composition, snapshots, recomposer). Never vendored.
|
THE Compose runtime (composition, snapshots, recomposer). Never vendored.
|
||||||
- `androidx.compose.runtime:runtime-retain` 1.11.1 (google coordinates).
|
- `androidx.compose.runtime:runtime-retain` 1.11.1 (google coordinates).
|
||||||
- `androidx.lifecycle:*` **2.11.0** (google): `lifecycle-runtime-compose`,
|
- `androidx.lifecycle:*` **2.11.0** (google): `lifecycle-runtime-compose`,
|
||||||
`lifecycle-viewmodel`, `lifecycle-viewmodel-compose`,
|
`lifecycle-viewmodel`, `lifecycle-viewmodel-compose`,
|
||||||
`lifecycle-viewmodel-savedstate`, **`lifecycle-viewmodel-navigation3`** —
|
`lifecycle-viewmodel-savedstate`, **`lifecycle-viewmodel-navigation3`** -
|
||||||
the whole ViewModel + SavedStateHandle + nav3-decorator stack needed ZERO
|
the whole ViewModel + SavedStateHandle + nav3-decorator stack needed ZERO
|
||||||
reimplementation; only the window-side owners had to be provided (see
|
reimplementation; only the window-side owners had to be provided (see
|
||||||
caveats).
|
caveats).
|
||||||
- `androidx.savedstate:savedstate` / `savedstate-compose` **1.5.0** (google).
|
- `androidx.savedstate:savedstate` / `savedstate-compose` **1.5.0** (google).
|
||||||
- `androidx.navigation3:navigation3-runtime` **1.1.4** — backstack / NavEntry
|
- `androidx.navigation3:navigation3-runtime` **1.1.4** - backstack / NavEntry
|
||||||
/ decorators. Only navigation3-UI (NavDisplay) lacks a K/N desktop artifact
|
/ decorators. Only navigation3-UI (NavDisplay) lacks a K/N desktop artifact
|
||||||
→ vendored as `:navigation3-ui`.
|
→ vendored as `:navigation3-ui`.
|
||||||
- `androidx.navigationevent:navigationevent-compose` 1.1.2 — predictive-back
|
- `androidx.navigationevent:navigationevent-compose` 1.1.2 - predictive-back
|
||||||
event plumbing (BackHandler, NavDisplay gestures).
|
event plumbing (BackHandler, NavDisplay gestures).
|
||||||
- `androidx.collection:collection` — plain Maven dep, not a module.
|
- `androidx.collection:collection` - plain Maven dep, not a module.
|
||||||
|
- `androidx.window:window-core` **1.5.0** - WindowSizeClass / the adaptive
|
||||||
|
window model. Publishes mingwX64 + linux + macos under BOTH coordinate sets;
|
||||||
|
the port takes the google one like the rest of the androidx stack. This is
|
||||||
|
what makes the material3-adaptive vendoring a pure re-target: the dependency
|
||||||
|
everybody assumed was the blocker was never missing.
|
||||||
|
- `io.insert-koin:koin-core` **4.2.2** - the DI container itself publishes
|
||||||
|
mingwX64 + linux + macos. Only Koin's viewmodel / compose layers stop at
|
||||||
|
apple+android and are vendored (`koin/`).
|
||||||
|
- `androidx.navigationevent:navigationevent-compose` 1.1.2 AND the jetbrains
|
||||||
|
`org.jetbrains.androidx.navigationevent:navigationevent-compose` 1.1.0 - BOTH
|
||||||
|
publish mingwX64 + linux + macos. Nothing to vendor here.
|
||||||
- NOT compatible (vendored instead): `components-resources` (no mingw/linux
|
- NOT compatible (vendored instead): `components-resources` (no mingw/linux
|
||||||
klibs → `:components-resources`), `navigation3-ui` (same → `:navigation3-ui`).
|
klibs → `:components:resources:components-resources`), `navigation3-ui` (same →
|
||||||
|
`:navigation3:navigation3-ui`), Koin's viewmodel/compose modules (→ `koin/`),
|
||||||
|
the whole Coil 3 stack (no mingwX64 anywhere; no desktop native at all for its
|
||||||
|
compose layer → `coil/`), and Pulse MVI (no desktop native → `pulse/`).
|
||||||
|
- **Always check BOTH coordinate sets before concluding an artifact has no
|
||||||
|
desktop-native klibs.** `androidx.*` and `org.jetbrains.androidx.*` are
|
||||||
|
different artifacts with the same classes; sometimes only one carries the K/N
|
||||||
|
variant, and where both do, pick ONE - putting both on the path duplicates
|
||||||
|
every class. This port standardises on the GOOGLE `androidx.lifecycle:*`
|
||||||
|
coordinates, so vendored upstream code that referenced the jetbrains mirrors
|
||||||
|
is repointed at the google ones in the module's build file.
|
||||||
- Infra: `kotlinx-coroutines-core`, `atomicfu`, `okio`,
|
- Infra: `kotlinx-coroutines-core`, `atomicfu`, `okio`,
|
||||||
`kotlinx-serialization`.
|
`kotlinx-serialization`.
|
||||||
|
|
||||||
Caveats that make these work here (all already wired — listed so nobody
|
Caveats that make these work here (all already wired - listed so nobody
|
||||||
"fixes" them away):
|
"fixes" them away):
|
||||||
|
|
||||||
- The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` /
|
- The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` /
|
||||||
`LocalLifecycleOwner` are PLAIN composition locals — the JB HostDefault
|
`LocalLifecycleOwner` are PLAIN composition locals - the JB HostDefault
|
||||||
mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google
|
mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google
|
||||||
artifacts. `WindowArchitectureOwner` (ComposeWindow.kt) provides all three
|
artifacts. `WindowArchitectureOwner` (WindowInstance.kt) provides all three
|
||||||
per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner,
|
per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner,
|
||||||
calls `enableSavedStateHandles()` at construction, and follows SDL focus /
|
calls `enableSavedStateHandles()` at construction, and follows SDL focus /
|
||||||
visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED).
|
visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED).
|
||||||
- The window composes its FIRST composition at CREATED and resumes after —
|
- The window composes its FIRST composition at CREATED and resumes after -
|
||||||
`enableSavedStateHandles()` callers running in composition require
|
`enableSavedStateHandles()` callers running in composition require
|
||||||
lifecycle ≤ CREATED. Related contract: `rememberViewModelStoreOwner()` with
|
lifecycle ≤ CREATED. Related contract: `rememberViewModelStoreOwner()` with
|
||||||
its default `savedStateRegistryOwner` THROWS at a RESUMED call site (same
|
its default `savedStateRegistryOwner` THROWS at a RESUMED call site (same
|
||||||
on Android) — scope shared VMs to the window owner (`viewModel { }` outside
|
on Android) - scope shared VMs to the window owner (`viewModel { }` outside
|
||||||
the entries, the `activityViewModels()` analog) instead.
|
the entries, the `activityViewModels()` analog) instead.
|
||||||
- nav3 entry decorators: saveable BEFORE viewmodel —
|
- nav3 entry decorators: saveable BEFORE viewmodel -
|
||||||
`listOf(rememberSaveableStateHolderNavEntryDecorator(),
|
`listOf(rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
rememberViewModelStoreNavEntryDecorator())`.
|
rememberViewModelStoreNavEntryDecorator())`.
|
||||||
- `Dispatchers.Main.immediate` must run inline on the main thread
|
- `Dispatchers.Main.immediate` must run inline on the main thread
|
||||||
(Sdl3MainDispatcher) — androidx.lifecycle's main-thread enforcement
|
(Sdl3MainDispatcher) - androidx.lifecycle's main-thread enforcement
|
||||||
round-trips through it; a queue-only Main deadlocks composition.
|
round-trips through it; a queue-only Main deadlocks composition.
|
||||||
- Regression probes: `demo --nav3test` (nav3 + ViewModels + lifecycle,
|
- Regression probes: `demo --nav3test` (nav3 + ViewModels + lifecycle,
|
||||||
composed late at RESUMED like the real sidebar flow), `--backtest`
|
composed late at RESUMED like the real sidebar flow), `--backtest`
|
||||||
@@ -673,4 +838,4 @@ Caveats that make these work here (all already wired — listed so nobody
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE.md](LICENSE.md).
|
MIT - see [LICENSE.md](LICENSE.md).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](LICENSE.md)
|
[](LICENSE.md)
|
||||||
|
|
||||||
# Compose Desktop Native
|
# Compose Desktop Native
|
||||||
@@ -18,6 +18,9 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
|
|||||||
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
|
- **The UI layers are vendored upstream.** `androidx.compose.ui`, `foundation`,
|
||||||
`animation`, and `material3` are copied from Compose Multiplatform verbatim
|
`animation`, and `material3` are copied from Compose Multiplatform verbatim
|
||||||
wherever they compile as is, with project code filling in only the native glue.
|
wherever they compile as is, with project code filling in only the native glue.
|
||||||
|
The same treatment extends to ecosystem libraries that stop short of
|
||||||
|
Kotlin/Native desktop - Koin, Coil 3 and Pulse MVI are vendored and rebuilt for
|
||||||
|
these targets, and you still declare their official coordinates.
|
||||||
- **Rendering is Skia everywhere.** macOS and Linux link the official Skiko
|
- **Rendering is Skia everywhere.** macOS and Linux link the official Skiko
|
||||||
klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships
|
klibs (Metal / OpenGL); Windows links the bitsycore Skiko fork, which ships
|
||||||
Skiko and Skia together in `skiko-windows-x64.dll`.
|
Skiko and Skia together in `skiko-windows-x64.dll`.
|
||||||
@@ -30,9 +33,9 @@ executable for macOS (arm64), Linux (x64/arm64), and Windows (mingwX64), using
|
|||||||
|
|
||||||
| Platform | Gradle target | Renderer |
|
| Platform | Gradle target | Renderer |
|
||||||
|----------|---------------|----------|
|
|----------|---------------|----------|
|
||||||
| macOS arm64 | `macosArm64` | Skia (Metal) — official Skiko |
|
| macOS arm64 | `macosArm64` | Skia (Metal) - official Skiko |
|
||||||
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) — official Skiko |
|
| Linux x64 / arm64 | `linuxX64` / `linuxArm64` | Skia (OpenGL) - official Skiko |
|
||||||
| Windows | `mingwX64` | Skia — bitsycore Skiko fork |
|
| Windows | `mingwX64` | Skia - bitsycore Skiko fork |
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
@@ -68,13 +71,30 @@ Each window gets its own `Lifecycle`, `ViewModelStore`, and `SavedStateRegistry`
|
|||||||
owners, driven by real SDL focus and visibility events, so ViewModels and
|
owners, driven by real SDL focus and visibility events, so ViewModels and
|
||||||
saved state behave as they do on Android.
|
saved state behave as they do on Android.
|
||||||
|
|
||||||
|
`Window()` takes the same attributes as Compose Desktop's - `undecorated`,
|
||||||
|
`transparent`, `resizable`, `enabled`, `focusable`, `alwaysOnTop`,
|
||||||
|
`onPreviewKeyEvent` and `onKeyEvent` - and re-applies them when they change.
|
||||||
|
(`transparent` is the one exception: SDL needs it at window-creation time, so it
|
||||||
|
is fixed for the window's life.)
|
||||||
|
|
||||||
|
When the Compose-level API doesn't reach far enough, drop to SDL directly:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val handles = window.rawSdlHandles() // SDL_Window* / SDL_Renderer* / GL / Metal
|
||||||
|
handles.window?.let { sdl3.SDL_FlashWindow(it.reinterpret(), SDL_FLASH_BRIEFLY) }
|
||||||
|
```
|
||||||
|
|
||||||
|
The pointers are a snapshot valid only while the window lives, and which ones
|
||||||
|
are non-null depends on the resolved renderer (`glContext` for Skia OpenGL,
|
||||||
|
`metalView` for Metal, `renderer` for CPU raster).
|
||||||
|
|
||||||
### Building in this repo
|
### Building in this repo
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation(project(":desktop-native-window")) // window shell + main loop
|
implementation(project(":compose:desktop:native:desktop-native-window")) // window shell + main loop
|
||||||
implementation(project(":material3")) // Material 3 widgets
|
implementation(project(":compose:material3:material3")) // Material 3 widgets
|
||||||
implementation(project(":material-symbols")) // icon-font composables (optional)
|
implementation(project(":utils:material-symbols")) // icon-font composables (optional)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -82,7 +102,7 @@ commonMain.dependencies {
|
|||||||
|
|
||||||
The klibs publish to [maven.bitsycore.com](https://maven.bitsycore.com/releases)
|
The klibs publish to [maven.bitsycore.com](https://maven.bitsycore.com/releases)
|
||||||
(no auth) and GitHub Packages (authenticated fallback) under per-area coordinates that mirror
|
(no auth) and GitHub Packages (authenticated fallback) under per-area coordinates that mirror
|
||||||
upstream — `com.bitsycore.compose.ui:ui`, `com.bitsycore.compose.foundation:foundation`,
|
upstream - `com.bitsycore.compose.ui:ui`, `com.bitsycore.compose.foundation:foundation`,
|
||||||
… (the `com.bitsycore` fork of each `org.jetbrains.compose.*`). Apply
|
… (the `com.bitsycore` fork of each `org.jetbrains.compose.*`). Apply
|
||||||
the bridge Gradle plugin once, declare the **official** Compose Multiplatform
|
the bridge Gradle plugin once, declare the **official** Compose Multiplatform
|
||||||
coordinates, and the plugin swaps in the port's klibs on native desktop targets
|
coordinates, and the plugin swaps in the port's klibs on native desktop targets
|
||||||
@@ -98,23 +118,29 @@ plugins {
|
|||||||
|
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
// The plugin exposes the exact Compose versions the port tracks, so you
|
// The plugin exposes the exact Compose versions the port tracks, so you
|
||||||
// never hand-match them (material3 is versioned separately upstream).
|
// never hand-match them (material3 and material3-adaptive each ride their
|
||||||
|
// own release train upstream).
|
||||||
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
|
implementation("org.jetbrains.compose.runtime:runtime:${composeDesktopNative.composeRuntime}")
|
||||||
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
|
implementation("org.jetbrains.compose.ui:ui:${composeDesktopNative.compose}")
|
||||||
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
|
implementation("org.jetbrains.compose.foundation:foundation:${composeDesktopNative.compose}")
|
||||||
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
|
implementation("org.jetbrains.compose.material3:material3:${composeDesktopNative.composeMaterial3}")
|
||||||
|
|
||||||
|
// material3-adaptive, which upstream publishes for apple targets only.
|
||||||
|
// Note adaptive api-exposes androidx.window:window-core - declare it
|
||||||
|
// DIRECTLY if your commonMain touches WindowSizeClass, since substituted
|
||||||
|
// modules hide their transitives from the common metadata classpath.
|
||||||
|
val vAdaptive = composeDesktopNative.composeMaterial3Adaptive
|
||||||
|
implementation("org.jetbrains.compose.material3.adaptive:adaptive:$vAdaptive")
|
||||||
|
implementation("org.jetbrains.compose.material3.adaptive:adaptive-layout:$vAdaptive")
|
||||||
|
implementation("androidx.window:window-core:1.5.0")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Two GitHub Packages repos are needed.** A consumer declares the port's repo
|
**One repository, no credentials.** `https://maven.bitsycore.com/releases`
|
||||||
(`bitsycore/compose-desktop-native`) **and** the skiko fork's (`bitsycore/skiko`).
|
carries both the port's artifacts and the bitsycore skiko fork
|
||||||
The Windows (mingwX64) target renders through the bitsycore skiko fork, and
|
(`com.bitsycore.skiko`), which the Windows (mingwX64) target renders through;
|
||||||
GitHub Packages binds each package name to a **single** repository — so
|
macOS/Linux pull the official skiko from Maven Central instead. The exact
|
||||||
`com.bitsycore.skiko` can't be mirrored into the port's repo, it stays in its
|
`repositories {}` snippet and version pinning:
|
||||||
own. Scope them with `includeGroup`/`excludeGroup("com.bitsycore.skiko")`
|
|
||||||
(macOS/Linux pull the official skiko from Maven Central instead; if you build no
|
|
||||||
mingwX64 you can omit the fork repo). The exact `repositories {}` snippet,
|
|
||||||
credentials, and version pinning:
|
|
||||||
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
|
[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
|
For a complete project that applies the bridge and builds one shared UI for
|
||||||
@@ -152,16 +178,123 @@ client certificates.
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
One Gradle module per upstream Compose artifact, mirroring the upstream
|
One Gradle module per upstream artifact, and the Gradle path mirrors the
|
||||||
`compose/` tree. The renderer lives in `:ui` and the `sdl3` cinterop in
|
directory (`:compose:ui:ui`, `:compose:foundation:foundation`, …). The renderer
|
||||||
`:sdl-core`; `:desktop-native-window` is the SDL integration layer. `:material-symbols`, `:components-resources`, and
|
lives in `:compose:ui:ui` and the `sdl3` cinterop in `:sdl:sdl-core`;
|
||||||
`:navigation3-ui` are project or vendored modules that the official Maven
|
`:compose:desktop:native:desktop-native-window` is the SDL integration layer.
|
||||||
artifacts do not cover for Kotlin/Native desktop.
|
|
||||||
|
|
||||||
Most of the androidx architecture stack (lifecycle, viewmodel, navigation3,
|
Most of the androidx architecture stack (lifecycle, viewmodel, navigation3,
|
||||||
savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on
|
savedstate, navigationevent) ships real Kotlin/Native desktop klibs and runs on
|
||||||
the port unmodified. The full module map, dependency graph, and the list of
|
the port unmodified. Where an ecosystem library stops short of Kotlin/Native
|
||||||
compatible artifacts are in [CLAUDE.md](CLAUDE.md).
|
desktop, this repo vendors it verbatim from upstream and rebuilds it for these
|
||||||
|
targets, so you keep writing against the official coordinates:
|
||||||
|
|
||||||
|
| Vendored | Why | Published as |
|
||||||
|
|----------|-----|--------------|
|
||||||
|
| Compose `ui` / `foundation` / `animation` / `material3` | the port itself | `com.bitsycore.compose.*` |
|
||||||
|
| `components-resources`, `navigation3-ui` | no mingwX64 / linux klibs | `com.bitsycore.compose.components`, `com.bitsycore.navigation3` |
|
||||||
|
| **Koin** viewmodel + compose modules | apple + android only upstream (`koin-core` itself is fine) | `com.bitsycore.koin` |
|
||||||
|
| **Coil 3** (whole stack) | no mingwX64 anywhere; no desktop native at all for its compose layer | `com.bitsycore.coil3` |
|
||||||
|
| **Pulse MVI** | no desktop-native artifact upstream | `com.bitsycore.compose.desktop.native.pulse` |
|
||||||
|
| **material3-adaptive** (all four) | ios + macosArm64 only upstream - no linux, no mingw | `com.bitsycore.compose.material3.adaptive` |
|
||||||
|
|
||||||
|
The bridge plugin substitutes each of these on native desktop configurations, so
|
||||||
|
app code declares `io.insert-koin:koin-compose`, `io.coil-kt.coil3:coil-compose`
|
||||||
|
and so on exactly as it would anywhere else. The full module map, dependency
|
||||||
|
graph, and the list of compatible artifacts are in [CLAUDE.md](CLAUDE.md).
|
||||||
|
|
||||||
|
## Pinned versions
|
||||||
|
|
||||||
|
Everything below is pinned in exactly two files -
|
||||||
|
[`gradle/libs.versions.toml`](gradle/libs.versions.toml) (Maven coordinates) and
|
||||||
|
[`scripts/compose-fork/compose.properties`](scripts/compose-fork/compose.properties)
|
||||||
|
(the upstream git refs the vendored sources are copied from) - plus
|
||||||
|
[`scripts/build-sdl/build-sdl.properties`](scripts/build-sdl/build-sdl.properties)
|
||||||
|
for SDL3. A release tracks one Compose Multiplatform version; the table is the
|
||||||
|
contract.
|
||||||
|
|
||||||
|
**Version policy.** Anything Compose Multiplatform integrates is pinned to what
|
||||||
|
CMP ships - for the androidx mirrors, the *Jetpack* version CMP's release notes
|
||||||
|
map its `org.jetbrains.androidx.*` artifact to, since this port consumes the
|
||||||
|
google coordinates. Anything CMP does not integrate is pinned to the latest
|
||||||
|
release. Deliberate exceptions are commented in the catalog; there is currently
|
||||||
|
one (`savedstate`, below).
|
||||||
|
|
||||||
|
### Toolchain
|
||||||
|
|
||||||
|
| | Version | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Kotlin | **2.4.20** | Newer than the 2.3.20 CMP 1.12.1 is built with. Kotlin/Native consumes older klibs fine, and the whole port builds clean on 2.4.20 with no klib API diff, so there is no reason to hold back. |
|
||||||
|
| Compose Multiplatform | **1.12.1** | The vendored sources are the `v1.12.1` tag of both upstream repos, and the JVM parity leg forces the same version - no dev-build skew. skiko is unchanged at 0.150.1, so the mingwX64 fork still matches its Skia base. |
|
||||||
|
| SDL3 | **release-3.4.16** | Built from source as a static lib per host (`scripts/build-sdl/build-all.py`), linked into the executable. |
|
||||||
|
| Skiko | **0.150.1** | macOS / Linux use the official `org.jetbrains.skiko`. |
|
||||||
|
| Skiko (Windows fork) | **0.150.1-mingw.2** | `com.bitsycore.skiko:skiko` - Skiko + Skia in `skiko-windows-x64.dll`. Same Skia base as the official build. Public repo, no auth. |
|
||||||
|
|
||||||
|
### Compose libraries
|
||||||
|
|
||||||
|
The port republishes each vendored artifact under a `com.bitsycore` group that
|
||||||
|
mirrors the upstream one, so the fork of any given coordinate is obvious. Apply
|
||||||
|
the bridge plugin and you keep declaring the **official** coordinate.
|
||||||
|
|
||||||
|
| Library | Official coordinate | Port coordinate |
|
||||||
|
|---|---|---|
|
||||||
|
| Runtime | `org.jetbrains.compose.runtime:runtime*:1.12.1` | *not forked - the official klibs serve every target* |
|
||||||
|
| UI | `org.jetbrains.compose.ui:ui*:1.12.1` | `com.bitsycore.compose.ui:ui*` |
|
||||||
|
| Foundation | `org.jetbrains.compose.foundation:foundation*:1.12.1` | `com.bitsycore.compose.foundation:foundation*` |
|
||||||
|
| Animation | `org.jetbrains.compose.animation:animation*:1.12.1` | `com.bitsycore.compose.animation:animation*` |
|
||||||
|
| Material Ripple | `org.jetbrains.compose.material:material-ripple:1.12.1` | `com.bitsycore.compose.material:material-ripple` |
|
||||||
|
| Material3 | `org.jetbrains.compose.material3:material3:1.12.0-alpha03` | `com.bitsycore.compose.material3:material3` |
|
||||||
|
| Material3 Adaptive | `org.jetbrains.compose.material3.adaptive:adaptive*:1.3.0-rc01` | `com.bitsycore.compose.material3.adaptive:adaptive*` |
|
||||||
|
| Resources | `org.jetbrains.compose.components:components-resources:1.12.1` | `com.bitsycore.compose.components:components-resources` |
|
||||||
|
| Navigation3 UI | `org.jetbrains.androidx.navigation3:navigation3-ui` | `com.bitsycore.navigation3:navigation3-ui` |
|
||||||
|
| Window shell | *no upstream equivalent* | `com.bitsycore.compose:desktop-native-window` |
|
||||||
|
| SDL layer | *no upstream equivalent* | `com.bitsycore.compose.sdl:sdl-core` |
|
||||||
|
|
||||||
|
Material3 rides its own release train upstream: `1.12.0-alpha03` **is** the
|
||||||
|
version Compose Multiplatform 1.12.1 ships (Jetpack Material3 1.5.0-alpha22).
|
||||||
|
material3-adaptive rides a third train again.
|
||||||
|
|
||||||
|
**material3-adaptive** (`adaptive`, `adaptive-layout`, `adaptive-navigation`,
|
||||||
|
`adaptive-navigation3`) is published upstream for ios and macosArm64 only - no
|
||||||
|
linux, no mingw. This port ships all four for every desktop-native target. It
|
||||||
|
needed no source changes at all: every platform actual the native legs want is
|
||||||
|
already in upstream's `nonAndroidMain` / `skikoMain` / `nativeMain`, and
|
||||||
|
`androidx.window:window-core` - the one dependency involved - has published
|
||||||
|
linux, mingw and macos klibs the whole time. The artifacts were missing only
|
||||||
|
because upstream never turned the targets on. `demo --screen=Adaptive` renders
|
||||||
|
a live `ListDetailPaneScaffold`.
|
||||||
|
|
||||||
|
### Ecosystem libraries
|
||||||
|
|
||||||
|
Vendored because upstream stops short of Kotlin/Native desktop. The bridge
|
||||||
|
substitutes them on native desktop targets only.
|
||||||
|
|
||||||
|
| Library | Official coordinate | Port coordinate | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Koin | `io.insert-koin:koin-compose*:4.2.2` | `com.bitsycore.koin:*` | apple + android only upstream |
|
||||||
|
| Koin core | `io.insert-koin:koin-core:4.2.2` | *not forked* | already ships mingwX64 + linux |
|
||||||
|
| Coil 3 | `io.coil-kt.coil3:coil*:3.6.3` | `com.bitsycore.coil3:*` | no mingwX64 anywhere; no desktop native at all for the compose layer |
|
||||||
|
| Pulse MVI | `com.bitsycore.lib:pulse*:0.3.7` | `com.bitsycore.compose.desktop.native.pulse:*` | no desktop-native artifact upstream - and Pulse is itself a `com.bitsycore` library, so the republish is namespaced under this project |
|
||||||
|
|
||||||
|
Koin's `koin-compose-viewmodel-navigation` is deliberately **not** provided: it
|
||||||
|
needs Navigation 2's `navigation-compose`, which has no mingwX64 or linux klibs
|
||||||
|
under either coordinate set. Use `koin-compose-navigation3`.
|
||||||
|
|
||||||
|
### AndroidX, used as-is
|
||||||
|
|
||||||
|
These publish real Kotlin/Native desktop klibs and run on the port unmodified -
|
||||||
|
nothing is vendored or substituted. The port standardises on the **google**
|
||||||
|
`androidx.*` coordinates; do not also pull the `org.jetbrains.androidx.*`
|
||||||
|
mirrors or you will have every class twice.
|
||||||
|
|
||||||
|
| Library | Coordinate |
|
||||||
|
|---|---|
|
||||||
|
| Lifecycle / ViewModel | `androidx.lifecycle:lifecycle-*:2.11.0` |
|
||||||
|
| SavedState | `androidx.savedstate:savedstate*:1.5.0` (CMP builds against 1.4.0; lifecycle 2.11.0 requires it only as a *minimum*, so 1.5.0 resolves cleanly and gives consumers the newer API) |
|
||||||
|
| Navigation3 runtime | `androidx.navigation3:navigation3-runtime:1.1.7` |
|
||||||
|
| Navigation Event | `androidx.navigationevent:navigationevent-compose:1.1.2` |
|
||||||
|
| Collection | `androidx.collection:collection:1.5.0` |
|
||||||
|
| Graphics Shapes | `androidx.graphics:graphics-shapes:1.1.0` |
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -177,6 +310,10 @@ command line tools; Linux needs gcc/g++ and the X11 / Wayland / audio dev
|
|||||||
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
|
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
|
||||||
the full build and verification workflow.
|
the full build and verification workflow.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
[Bitsycore's Discord](https://discord.gg/kGnraVAtDR)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
|
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
|
||||||
|
|||||||
+36
-20
@@ -9,7 +9,7 @@ For architecture, source-set layout, and vendoring rules, see
|
|||||||
## Native libraries
|
## Native libraries
|
||||||
|
|
||||||
SDL3 is built from source as a static library and linked straight into the
|
SDL3 is built from source as a static library and linked straight into the
|
||||||
executable — the windowing, input, and platform-integration layer. One script
|
executable - the windowing, input, and platform-integration layer. One script
|
||||||
does it on every OS. Output lands in the gitignored `libs/`; the version is
|
does it on every OS. Output lands in the gitignored `libs/`; the version is
|
||||||
pinned in `scripts/build-sdl/build-sdl.properties`.
|
pinned in `scripts/build-sdl/build-sdl.properties`.
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ absent). Run it once per machine, or after bumping the pinned version.
|
|||||||
The static lib is built **slim**: the port uses SDL only for video/window,
|
The static lib is built **slim**: the port uses SDL only for video/window,
|
||||||
events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render`
|
events, clipboard, file dialogs, GL/Metal contexts, the CPU-raster `SDL_Render`
|
||||||
blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py`
|
blit, filesystem, cursor, locale, theme, and text input/IME. `build-all.py`
|
||||||
disables every unused subsystem — audio, joystick, haptic, hidapi, sensor,
|
disables every unused subsystem - audio, joystick, haptic, hidapi, sensor,
|
||||||
power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus
|
power, camera, GPU, offscreen, virtual-joystick (all zero-reference), plus
|
||||||
tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of
|
tests/examples and the Windows D3D12 driver. If a consumer app ever needs one of
|
||||||
these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and
|
these (e.g. SDL audio), re-enable its `-DSDL_*` flag in `build-all.py` and
|
||||||
rebuild — a build-time change, no code edit.
|
rebuild - a build-time change, no code edit.
|
||||||
|
|
||||||
## Vendoring upstream Compose
|
## Vendoring upstream Compose
|
||||||
|
|
||||||
@@ -48,15 +48,24 @@ Reach for this after a fresh checkout or a ref bump. Details:
|
|||||||
Guardrails that keep the vendor tree honest:
|
Guardrails that keep the vendor tree honest:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/compose-fork/check-vendor-clean.py # src/vendor matches the pinned refs
|
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
|
python3 scripts/compose-fork/check-vendor-drift.py # hand-edited "manual vendors" match their pin
|
||||||
|
python3 scripts/compose-fork/audit-exclusions.py # every `!` exclusion is classified
|
||||||
```
|
```
|
||||||
|
|
||||||
`check-vendor-clean` fails if a hand-edit ever leaks into `src/vendor/`.
|
`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,
|
`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
|
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
|
the pin (needs reconciling) or is merely stale (safe to re-stamp).
|
||||||
every ref bump.
|
|
||||||
|
`audit-exclusions` covers the other half: a manifest `!` line takes a file OUT of
|
||||||
|
the verbatim sync, and the local replacement is either a derived copy
|
||||||
|
(`// VENDOR-BASE:`, drift-tracked) or a fresh reimplementation
|
||||||
|
(`// VENDOR-REIMPL:`, deliberately not tracked - only a SIGNATURE change matters).
|
||||||
|
It classifies each one, flags any local counterpart carrying NEITHER marker
|
||||||
|
(invisible to the drift check, silently rotting) and any copy that has become
|
||||||
|
identical to upstream (drop it and re-vendor), and exits non-zero on an
|
||||||
|
unclassified file. Run all three on every ref bump.
|
||||||
|
|
||||||
### Coverage against upstream
|
### Coverage against upstream
|
||||||
|
|
||||||
@@ -128,7 +137,7 @@ height]`). Its job is the mac-vs-Windows vertical-spacing question (PLAN.md §1b
|
|||||||
the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per
|
the SAME `NotoSans.ttf` bytes go through a different Skia `FontMgr` scaler per
|
||||||
host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different
|
host (CoreText / fontconfig-FreeType / DirectWrite), which can pick different
|
||||||
metric tables. Run it on native AND on the JVM parity app on the SAME host and
|
metric tables. Run it on native AND on the JVM parity app on the SAME host and
|
||||||
diff the lines — Windows-native must match skiko-JVM-Windows.
|
diff the lines - Windows-native must match skiko-JVM-Windows.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp
|
CDN_TEXT_METRICS=1 demo.kexe --screen=Buttons --screenshot=/tmp/b.bmp
|
||||||
@@ -145,6 +154,7 @@ demo.kexe --backtest # predictive-back / BackHandler
|
|||||||
demo.kexe --clicktest # pointer -> upstream clickable
|
demo.kexe --clicktest # pointer -> upstream clickable
|
||||||
demo.kexe --scrolltest # wheel -> scrollable
|
demo.kexe --scrolltest # wheel -> scrollable
|
||||||
demo.kexe --multiwintest # multi-window lifecycle
|
demo.kexe --multiwintest # multi-window lifecycle
|
||||||
|
demo.kexe --adaptivetest # material3-adaptive folds/unfolds on a LIVE window resize
|
||||||
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
|
demo.kexe --soaktest # memory soak (CDN_SOAK_SCREEN, CDN_SOAK_STATIC, CDN_SOAK_CYCLES)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -160,9 +170,9 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
|||||||
```
|
```
|
||||||
|
|
||||||
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
mingwX64 renders through Skia via the **bitsycore skiko fork**, consumed from
|
||||||
GitHub Packages as `com.bitsycore.skiko:skiko:0.150.1-mingw.1` (macOS/Linux use
|
the PUBLIC `https://maven.bitsycore.com/releases` (no credentials) as
|
||||||
official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
|
`com.bitsycore.skiko:skiko:0.150.1-mingw.2` (macOS/Linux use official Skiko). The runtime `skiko-windows-x64.dll` is auto-provisioned next
|
||||||
to the executable by the bridge plugin — no manual copy. The fork itself is
|
to the executable by the bridge plugin - no manual copy. The fork itself is
|
||||||
published by a separate GitHub Actions workflow in the fork repo, out of band
|
published by a separate GitHub Actions workflow in the fork repo, out of band
|
||||||
from this repo's release flow.
|
from this repo's release flow.
|
||||||
|
|
||||||
@@ -182,10 +192,11 @@ single file to edit; know which axis you are changing.
|
|||||||
|
|
||||||
| Version | Where | Notes |
|
| 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`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` — see `groupFor()` in the root build. | Set by the tag, not edited by hand. A non-publish build is `0.0.0-SNAPSHOT`. |
|
| 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`. Groups mirror upstream per area (`com.bitsycore.compose.<area>:<module>`, e.g. `com.bitsycore.compose.ui:ui`); project-only modules use `com.bitsycore.compose.sdl` and `:desktop-native-window` is `com.bitsycore.compose` - see `groupFor()` in the root build. | 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. |
|
| 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. |
|
| Vendored ecosystem libs | `KOIN_REF`, `COIL_REF`, `PULSE_REF` in the same file, plus `koin` / `coil` in `gradle/libs.versions.toml`. | Same rule: durable tags. Each repo gets its own sibling sparse clone (`../cmp-ref-koin`, `../cmp-ref-coil`, `../cmp-ref-pulse-mvi`). Re-sync, then `apiDump` - these modules are BCV-validated like the rest. |
|
||||||
| Skiko | `skiko` in `gradle/libs.versions.toml`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.1` from GitHub Packages), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
| JVM parity forcing | `compose` / `composeMaterial3` / `composeRuntime` in `gradle/libs.versions.toml` (read by `demo`, `apidemo`, `material-symbols` and the root build's forcing map). | Must be a version PUBLISHED to Maven Central. It may lag the vendored native ref (a documented skew) until the matching version is published; at v1.12.1 they are in lockstep. |
|
||||||
|
| Skiko | `skiko` (official) and `skikoMingw` (the fork) in `gradle/libs.versions.toml`; keep the fork's `-mingw.N` base equal to `skiko`. | macOS/Linux use official Skiko (`org.jetbrains.skiko`); mingwX64 uses the bitsycore fork (`com.bitsycore.skiko:skiko:0.150.1-mingw.2` from the public maven.bitsycore.com), published out of band by the fork repo's own workflow. Must expose the `org.jetbrains.skiko.node` `RenderNode` / `GraphicsContext` API the vendored compose-core uses (the fork keeps upstream's `org.jetbrains.skiko.*` package names; only the Maven coord is rebranded). Verify with a throwaway `skikoRendererMain` compile if unsure. |
|
||||||
| SDL3 | `scripts/build-sdl/build-sdl.properties`. | Rebuild `libs/` with `build-all.py` after any change. |
|
| SDL3 | `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. |
|
| 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. |
|
||||||
|
|
||||||
@@ -193,12 +204,17 @@ single file to edit; know which axis you are changing.
|
|||||||
|
|
||||||
Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
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`
|
1. Edit the relevant `*_REF` in `compose.properties` - `COMPOSE_CORE_REF` /
|
||||||
in `libs.versions.toml` if the coordinate version moved).
|
`COMPOSE_REF` for Compose, `KOIN_REF` / `COIL_REF` / `PULSE_REF` for the
|
||||||
|
ecosystem libs (and the matching entry in `libs.versions.toml` if the
|
||||||
|
coordinate version moved).
|
||||||
2. `scripts/compose-fork/sync.sh`
|
2. `scripts/compose-fork/sync.sh`
|
||||||
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
|
3. `python3 scripts/compose-fork/check-vendor-drift.py`. For any manual vendor
|
||||||
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
|
whose upstream base actually changed, reconcile by hand; otherwise re-stamp
|
||||||
its `// VENDOR-BASE:` header to the new ref.
|
its `// VENDOR-BASE:` header to the new ref. Then
|
||||||
|
`python3 scripts/compose-fork/audit-exclusions.py` - a newly-identical copy
|
||||||
|
should go back to verbatim vendoring, and an unclassified one needs a marker.
|
||||||
|
Finish with `./gradlew apiDump` and review the klib API diff.
|
||||||
4. Build and fix any fallout.
|
4. Build and fix any fallout.
|
||||||
5. `scripts/verify-mac.sh` (green, including the soak and parity gates).
|
5. `scripts/verify-mac.sh` (green, including the soak and parity gates).
|
||||||
6. If a matching Compose version is now published to Maven, bump
|
6. If a matching Compose version is now published to Maven, bump
|
||||||
@@ -216,12 +232,12 @@ Run this on each upstream bump; it is the flow that keeps the sync tax low.
|
|||||||
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
KotlinMultiplatform publications live there; a macOS-only publish leaves the
|
||||||
roots without mingwX64 variants (this bit v0.1.15).
|
roots without mingwX64 variants (this bit v0.1.15).
|
||||||
3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run
|
3. Refresh the klib API baselines: on a Windows host (JDK 21, matching CI) run
|
||||||
`./gradlew apiDump` and commit any diff. Not a hard gate yet — the API is
|
`./gradlew apiDump` and commit any diff. Not a hard gate yet - the API is
|
||||||
pre-stable and `apiCheck` isn't wired — but regenerating each release keeps
|
pre-stable and `apiCheck` isn't wired - but regenerating each release keeps
|
||||||
the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred
|
the baselines honest so the eventual gate is a no-op. `macosArm64` is inferred
|
||||||
from the linux/mingw ABIs (accurate for the target-independent Compose
|
from the linux/mingw ABIs (accurate for the target-independent Compose
|
||||||
surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps)
|
surface); `:sdl-core` (cinterop) and `:material-symbols` (generated icon maps)
|
||||||
are excluded via `apiValidation.ignoredProjects`. Best host: Windows — it
|
are excluded via `apiValidation.ignoredProjects`. Best host: Windows - it
|
||||||
builds the mingw slice (the fork's unique surface) for real and only infers
|
builds the mingw slice (the fork's unique surface) for real and only infers
|
||||||
macos; the macos ABI is target-independent for the tracked pure-Kotlin modules.
|
macos; the macos ABI is target-independent for the tracked pure-Kotlin modules.
|
||||||
4. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
|
4. `git tag vX.Y.Z && git push origin vX.Y.Z`. This triggers `.github/workflows/publish.yml`.
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ kotlin {
|
|||||||
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
||||||
|
|
||||||
// Common Material Symbols Utility
|
// Common Material Symbols Utility
|
||||||
implementation(project(":material-symbols"))
|
implementation(project(":utils:material-symbols"))
|
||||||
|
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.content.negotiation)
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
@@ -81,13 +81,17 @@ kotlin {
|
|||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":desktop-native-window"))
|
implementation(project(":compose:desktop:native:desktop-native-window"))
|
||||||
implementation(libs.ktor.client.curl)
|
implementation(libs.ktor.client.curl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
// decodeToImageBitmap / decodeToSvgPainter (window icon + in-memory
|
||||||
|
// image viewer). JVM-only: the native side goes through the port's
|
||||||
|
// own EncodedImageDecoder seam, so this stays out of commonMain.
|
||||||
|
implementation("org.jetbrains.compose.components:components-resources:${libs.versions.compose.get()}")
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
implementation(libs.kotlinx.coroutines.swing)
|
implementation(libs.kotlinx.coroutines.swing)
|
||||||
}
|
}
|
||||||
@@ -127,13 +131,13 @@ compose.desktop.native {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Fonts (shared pipeline — buildSrc ComposeFontBundling.kt)
|
// MARK: Fonts (shared pipeline - buildSrc ComposeFontBundling.kt)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
registerComposeFontBundling {
|
registerComposeFontBundling {
|
||||||
bundleNotoSans = true
|
bundleNotoSans = true
|
||||||
// The mono body font is loaded through the app's own seam (Fonts.kt), not via
|
// The mono body font is loaded through the app's own seam (Fonts.kt), not via
|
||||||
// FontFamily.Monospace call sites — auto-detection can't see it, bundle explicitly.
|
// FontFamily.Monospace call sites - auto-detection can't see it, bundle explicitly.
|
||||||
bundleNotoSansMono = true
|
bundleNotoSansMono = true
|
||||||
bundleMaterialSymbols = true
|
bundleMaterialSymbols = true
|
||||||
enableIconSubsetting = true
|
enableIconSubsetting = true
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import kotlin.io.encoding.Base64
|
|||||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
// macOS: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
|
// macOS: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
|
||||||
// certificate and key files directly — no certificate-store dance needed.
|
// certificate and key files directly - no certificate-store dance needed.
|
||||||
|
|
||||||
/** Point libcurl straight at the certificate / key files. */
|
/** Point libcurl straight at the certificate / key files. */
|
||||||
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
||||||
@@ -26,7 +26,7 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
|||||||
actual fun sweepTempClientCerts() {}
|
actual fun sweepTempClientCerts() {}
|
||||||
|
|
||||||
/** Continue the server's chain with intermediates / roots pulled from the
|
/** Continue the server's chain with intermediates / roots pulled from the
|
||||||
macOS Keychain — same UX as the Windows path does via the trust store.
|
macOS Keychain - same UX as the Windows path does via the trust store.
|
||||||
Falls back to the name-only placeholder if Apple's APIs can't resolve
|
Falls back to the name-only placeholder if Apple's APIs can't resolve
|
||||||
the chain (e.g. the server's leaf doesn't parse, or its issuer isn't
|
the chain (e.g. the server's leaf doesn't parse, or its issuer isn't
|
||||||
in any keychain). */
|
in any keychain). */
|
||||||
@@ -43,7 +43,7 @@ actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<Ch
|
|||||||
// ============
|
// ============
|
||||||
|
|
||||||
/** Build the chain by handing the leaf to SecTrust + evaluating against
|
/** Build the chain by handing the leaf to SecTrust + evaluating against
|
||||||
an SSL policy — Apple then walks Keychain, pulling intermediates and
|
an SSL policy - Apple then walks Keychain, pulling intermediates and
|
||||||
the root in. Returns the resolved chain with fromServer=true for the
|
the root in. Returns the resolved chain with fromServer=true for the
|
||||||
first inServerCount entries (those came down on the wire) and false
|
first inServerCount entries (those came down on the wire) and false
|
||||||
for anything the OS added. */
|
for anything the OS added. */
|
||||||
@@ -53,7 +53,7 @@ private fun osChainApple(
|
|||||||
inServerCount: Int,
|
inServerCount: Int,
|
||||||
inServerCerts: List<List<Pair<String, String>>>,
|
inServerCerts: List<List<Pair<String, String>>>,
|
||||||
): List<ChainCert> {
|
): List<ChainCert> {
|
||||||
// Parse every cert the server presented, not just the leaf — Apple's
|
// Parse every cert the server presented, not just the leaf - Apple's
|
||||||
// chain builder uses them as candidate intermediates when stitching to
|
// chain builder uses them as candidate intermediates when stitching to
|
||||||
// a root, instead of relying solely on AIA-fetch which is off by default.
|
// a root, instead of relying solely on AIA-fetch which is off by default.
|
||||||
val vServerCerts = inServerCerts.mapNotNull {
|
val vServerCerts = inServerCerts.mapNotNull {
|
||||||
@@ -82,7 +82,7 @@ private fun osChainApple(
|
|||||||
}
|
}
|
||||||
val vTrust = vTrustVar.value!!
|
val vTrust = vTrustVar.value!!
|
||||||
|
|
||||||
// Evaluate even if it fails — we still want the partial chain back so
|
// Evaluate even if it fails - we still want the partial chain back so
|
||||||
// the UI can show what it could resolve. SecTrustEvaluateWithError
|
// the UI can show what it could resolve. SecTrustEvaluateWithError
|
||||||
// stops at the highest cert it found.
|
// stops at the highest cert it found.
|
||||||
val vErr = nativeHeap.alloc<CFErrorRefVar>()
|
val vErr = nativeHeap.alloc<CFErrorRefVar>()
|
||||||
@@ -137,7 +137,7 @@ private fun createSecCertificate(inDer: ByteArray): SecCertificateRef? {
|
|||||||
return vCert
|
return vCert
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CFArray of SecCertificateRef — the input to SecTrustCreateWithCertificates. */
|
/** CFArray of SecCertificateRef - the input to SecTrustCreateWithCertificates. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef {
|
private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef {
|
||||||
val vPtrs = nativeHeap.allocArray<COpaquePointerVar>(inCerts.size)
|
val vPtrs = nativeHeap.allocArray<COpaquePointerVar>(inCerts.size)
|
||||||
@@ -147,7 +147,7 @@ private fun createCFArray(inCerts: List<SecCertificateRef>): CFArrayRef {
|
|||||||
return vArr
|
return vArr
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apple's "subject summary" — basically the CN, but falls back to the
|
/** Apple's "subject summary" - basically the CN, but falls back to the
|
||||||
organisation when no CN is present (matches what Keychain Access
|
organisation when no CN is present (matches what Keychain Access
|
||||||
displays in its cert list). */
|
displays in its cert list). */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
@@ -157,7 +157,7 @@ private fun secSubjectSummary(inCert: SecCertificateRef): String? {
|
|||||||
return vNs?.toString()
|
return vNs?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PEM string for a SecCertificate — base64 of the DER bytes wrapped
|
/** PEM string for a SecCertificate - base64 of the DER bytes wrapped
|
||||||
with -----BEGIN CERTIFICATE-----/-----END----- headers. */
|
with -----BEGIN CERTIFICATE-----/-----END----- headers. */
|
||||||
@OptIn(ExperimentalForeignApi::class, ExperimentalEncodingApi::class)
|
@OptIn(ExperimentalForeignApi::class, ExperimentalEncodingApi::class)
|
||||||
private fun secCertPem(inCert: SecCertificateRef): String? {
|
private fun secCertPem(inCert: SecCertificateRef): String? {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ internal fun App() {
|
|||||||
// Focus the saved active pack, but fall back to one that actually has tabs so
|
// Focus the saved active pack, but fall back to one that actually has tabs so
|
||||||
// the strip shows whenever any tab is open.
|
// the strip shows whenever any tab is open.
|
||||||
// The active pack/scope as a reference (a top-level pack, a sub-pack, or the
|
// The active pack/scope as a reference (a top-level pack, a sub-pack, or the
|
||||||
// loose root) — a reference (not an index) so sub-packs can be active.
|
// loose root) - a reference (not an index) so sub-packs can be active.
|
||||||
var vActivePackRef by remember {
|
var vActivePackRef by remember {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
vBoot.activePack.coerceIn(0, (vPacks.size - 1).coerceAtLeast(0)).let { vIdx ->
|
vBoot.activePack.coerceIn(0, (vPacks.size - 1).coerceAtLeast(0)).let { vIdx ->
|
||||||
@@ -138,7 +138,7 @@ internal fun App() {
|
|||||||
// Variables a request sees: session, then each enclosing pack (inner overrides).
|
// Variables a request sees: session, then each enclosing pack (inner overrides).
|
||||||
fun effective(inP: PackState): List<KeyVal> = vGlobalEnv.toList() + scopeChain(inP).flatMap { it.variables }
|
fun effective(inP: PackState): List<KeyVal> = vGlobalEnv.toList() + scopeChain(inP).flatMap { it.variables }
|
||||||
|
|
||||||
// …plus the request's own variables (innermost — they win over everything above).
|
// …plus the request's own variables (innermost - they win over everything above).
|
||||||
fun effectiveReqVars(inReq: ApiRequest, inP: PackState): List<KeyVal> = effective(inP) + inReq.variables
|
fun effectiveReqVars(inReq: ApiRequest, inP: PackState): List<KeyVal> = effective(inP) + inReq.variables
|
||||||
|
|
||||||
// Query params a request inherits: session, then each enclosing pack (inner wins by key).
|
// Query params a request inherits: session, then each enclosing pack (inner wins by key).
|
||||||
@@ -192,7 +192,7 @@ internal fun App() {
|
|||||||
// the tooltip. inChain for a request = scopeChain(owningPack) (its own pack
|
// the tooltip. inChain for a request = scopeChain(owningPack) (its own pack
|
||||||
// included); for a pack's settings = scopeChain(pack.parent) (ancestors only).
|
// included); for a pack's settings = scopeChain(pack.parent) (ancestors only).
|
||||||
// The full path of a scope for the source tooltip: "Methods" for a top-level
|
// The full path of a scope for the source tooltip: "Methods" for a top-level
|
||||||
// pack, "Methods / Nested" for a sub-pack (root excluded — it's never inherited).
|
// pack, "Methods / Nested" for a sub-pack (root excluded - it's never inherited).
|
||||||
fun scopePath(inP: PackState): String {
|
fun scopePath(inP: PackState): String {
|
||||||
val vParts = ArrayList<String>()
|
val vParts = ArrayList<String>()
|
||||||
var vCur: PackState? = inP
|
var vCur: PackState? = inP
|
||||||
@@ -243,7 +243,7 @@ internal fun App() {
|
|||||||
val vGE = vGlobalEnv.toList()
|
val vGE = vGlobalEnv.toList()
|
||||||
val vRootPack = vRoot.toPack()
|
val vRootPack = vRoot.toPack()
|
||||||
val vRootTabs = vRoot.openTabs.mapNotNull { vRs -> vRoot.requests.indexOf(vRs).takeIf { it >= 0 } }
|
val vRootTabs = vRoot.openTabs.mapNotNull { vRs -> vRoot.requests.indexOf(vRs).takeIf { it >= 0 } }
|
||||||
// Open-tab state — persisted in app state only, not in the session file.
|
// Open-tab state - persisted in app state only, not in the session file.
|
||||||
val vOpen = vPacks.map { vP -> vP.openTabs.mapNotNull { vRs -> vP.requests.indexOf(vRs).takeIf { it >= 0 } } }
|
val vOpen = vPacks.map { vP -> vP.openTabs.mapNotNull { vRs -> vP.requests.indexOf(vRs).takeIf { it >= 0 } } }
|
||||||
val vActiveReq = vActivePackRef?.let { it.requests.indexOf(it.active) } ?: -1
|
val vActiveReq = vActivePackRef?.let { it.requests.indexOf(it.active) } ?: -1
|
||||||
saveAppState(
|
saveAppState(
|
||||||
@@ -266,7 +266,7 @@ internal fun App() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
// A session opened from / saved to a file auto-saves back to it on every
|
// A session opened from / saved to a file auto-saves back to it on every
|
||||||
// change — once it has a file, it's always in sync (best-effort). The
|
// change - once it has a file, it's always in sync (best-effort). The
|
||||||
// Session has no open-tab fields, so the file never carries them.
|
// Session has no open-tab fields, so the file never carries them.
|
||||||
vSessionPath?.let {
|
vSessionPath?.let {
|
||||||
exportSession(
|
exportSession(
|
||||||
@@ -315,7 +315,7 @@ internal fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open a request in a specific pack's context (a linked copy shares the
|
// Open a request in a specific pack's context (a linked copy shares the
|
||||||
// source's ReqState objects, so the pack must be passed — packOf would
|
// source's ReqState objects, so the pack must be passed - packOf would
|
||||||
// resolve to the source, not the linked copy).
|
// resolve to the source, not the linked copy).
|
||||||
fun open(inRs: ReqState, inPack: PackState) {
|
fun open(inRs: ReqState, inPack: PackState) {
|
||||||
if (inRs !in inPack.openTabs) inPack.openTabs.add(inRs)
|
if (inRs !in inPack.openTabs) inPack.openTabs.add(inRs)
|
||||||
@@ -378,7 +378,7 @@ internal fun App() {
|
|||||||
val vTab = vFlat.getOrNull(inFrom) ?: return
|
val vTab = vFlat.getOrNull(inFrom) ?: return
|
||||||
val vRs = vTab.req ?: return
|
val vRs = vTab.req ?: return
|
||||||
val vP = vTab.pack ?: return
|
val vP = vTab.pack ?: return
|
||||||
// Flat index of this pack's first request tab — works for the root and any
|
// Flat index of this pack's first request tab - works for the root and any
|
||||||
// (nested) pack without hand-computing offsets.
|
// (nested) pack without hand-computing offsets.
|
||||||
val vReqStart = vFlat.indexOfFirst { it.pack === vP && it.req != null }
|
val vReqStart = vFlat.indexOfFirst { it.pack === vP && it.req != null }
|
||||||
if (vReqStart < 0) return
|
if (vReqStart < 0) return
|
||||||
@@ -400,7 +400,7 @@ internal fun App() {
|
|||||||
vP.requests.add(vRs); vP.openTabs.add(vRs); vP.active = vRs; vP.dirty = true; vReqMsg = null; vSideTab = 0
|
vP.requests.add(vRs); vP.openTabs.add(vRs); vP.active = vRs; vP.dirty = true; vReqMsg = null; vSideTab = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// A loose request at the session root (no pack — inherits session settings only).
|
// A loose request at the session root (no pack - inherits session settings only).
|
||||||
fun newLooseRequest() {
|
fun newLooseRequest() {
|
||||||
val vRs = ReqState(ApiRequest(name = "Request ${vRoot.requests.size + 1}"))
|
val vRs = ReqState(ApiRequest(name = "Request ${vRoot.requests.size + 1}"))
|
||||||
vRoot.requests.add(vRs); vRoot.openTabs.add(vRs); vRoot.active = vRs
|
vRoot.requests.add(vRs); vRoot.openTabs.add(vRs); vRoot.active = vRs
|
||||||
@@ -548,7 +548,7 @@ internal fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A linked copy mirrors inP's requests read-only but gets its own (copied)
|
// A linked copy mirrors inP's requests read-only but gets its own (copied)
|
||||||
// variables / headers / cert — for running the same calls against another env.
|
// variables / headers / cert - for running the same calls against another env.
|
||||||
fun createLinkedPack(inP: PackState) {
|
fun createLinkedPack(inP: PackState) {
|
||||||
val vSource = inP.linkedSource ?: inP // link to the real source, never to another link
|
val vSource = inP.linkedSource ?: inP // link to the real source, never to another link
|
||||||
val vAt = (vPacks.indexOf(inP) + 1).coerceIn(0, vPacks.size)
|
val vAt = (vPacks.indexOf(inP) + 1).coerceIn(0, vPacks.size)
|
||||||
@@ -582,7 +582,7 @@ internal fun App() {
|
|||||||
|
|
||||||
// Move inRs out of inFrom into inTo at inIndex. inIndex counts inTo's rows
|
// Move inRs out of inFrom into inTo at inIndex. inIndex counts inTo's rows
|
||||||
// *excluding* the dragged request (so for a same-pack reorder it is already an
|
// *excluding* the dragged request (so for a same-pack reorder it is already an
|
||||||
// index into the post-removal list — no shift needed; resolveReqDrop produces
|
// index into the post-removal list - no shift needed; resolveReqDrop produces
|
||||||
// exactly this). A request that was open / active follows to the new pack so
|
// exactly this). A request that was open / active follows to the new pack so
|
||||||
// its tab survives the move.
|
// its tab survives the move.
|
||||||
fun moveRequest(inRs: ReqState, inFrom: PackState, inTo: PackState, inIndex: Int) {
|
fun moveRequest(inRs: ReqState, inFrom: PackState, inTo: PackState, inIndex: Int) {
|
||||||
@@ -746,7 +746,7 @@ internal fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit the resolved request / pack drop, then clear the drag. A press that
|
// Commit the resolved request / pack drop, then clear the drag. A press that
|
||||||
// never passed the slop (engaged == false) is a click, not a drop — skip it.
|
// never passed the slop (engaged == false) is a click, not a drop - skip it.
|
||||||
fun reqDropEnd() {
|
fun reqDropEnd() {
|
||||||
val vRs = vTreeDrag.dragReq
|
val vRs = vTreeDrag.dragReq
|
||||||
val vFrom = vTreeDrag.dragReqOwner
|
val vFrom = vTreeDrag.dragReqOwner
|
||||||
@@ -915,7 +915,13 @@ internal fun App() {
|
|||||||
val vExtended = if (vDark) vPalette.scheme.darkExtended else vPalette.scheme.lightExtended
|
val vExtended = if (vDark) vPalette.scheme.darkExtended else vPalette.scheme.lightExtended
|
||||||
val vC = appColorsFromScheme(vScheme)
|
val vC = appColorsFromScheme(vScheme)
|
||||||
|
|
||||||
MaterialTheme(colorScheme = vScheme) {
|
// PARITY: pin the default text family to the bundled NotoSans on BOTH stacks.
|
||||||
|
// Left alone, native resolves FontFamily.Default to the bundled font while the
|
||||||
|
// JVM leg resolves it to a system face, so every un-familied Text was being
|
||||||
|
// compared against a different typeface (see Fonts.kt).
|
||||||
|
val vTypography = MaterialTheme.typography.withDefaultFontFamily(defaultFontFamily)
|
||||||
|
|
||||||
|
MaterialTheme(colorScheme = vScheme, typography = vTypography) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalAppColors provides vC,
|
LocalAppColors provides vC,
|
||||||
LocalVolticExtended provides vExtended,
|
LocalVolticExtended provides vExtended,
|
||||||
@@ -933,7 +939,7 @@ internal fun App() {
|
|||||||
// ============
|
// ============
|
||||||
// Sidebar (Pack panel)
|
// Sidebar (Pack panel)
|
||||||
Column(modifier = Modifier.fillMaxSize().background(vC.panel)) {
|
Column(modifier = Modifier.fillMaxSize().background(vC.panel)) {
|
||||||
// Sticky header — pack switcher + section tabs stay pinned while the list scrolls.
|
// Sticky header - pack switcher + section tabs stay pinned while the list scrolls.
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -1039,7 +1045,7 @@ internal fun App() {
|
|||||||
}
|
}
|
||||||
if (vPacks.isEmpty() && vRoot.requests.isEmpty()) {
|
if (vPacks.isEmpty() && vRoot.requests.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Nothing here yet — use Add (+) for a request or pack, or Open above.",
|
"Nothing here yet - use Add (+) for a request or pack, or Open above.",
|
||||||
color = vC.dim,
|
color = vC.dim,
|
||||||
fontSize = 12.sp
|
fontSize = 12.sp
|
||||||
)
|
)
|
||||||
@@ -1140,7 +1146,7 @@ internal fun App() {
|
|||||||
},
|
},
|
||||||
second = {
|
second = {
|
||||||
// ============
|
// ============
|
||||||
// Main — unified tab strip over the editor (request) or pack env.
|
// Main - unified tab strip over the editor (request) or pack env.
|
||||||
val vTabs = stripTabs()
|
val vTabs = stripTabs()
|
||||||
val vReqActive = vP?.active
|
val vReqActive = vP?.active
|
||||||
val vEnvShown = vEnvActive && vP != null && vP.envOpen
|
val vEnvShown = vEnvActive && vP != null && vP.envOpen
|
||||||
@@ -1161,12 +1167,12 @@ internal fun App() {
|
|||||||
"Load default session"
|
"Load default session"
|
||||||
) { loadDefaultSession() }
|
) { loadDefaultSession() }
|
||||||
}
|
}
|
||||||
} else Text("Nothing open — click a request, a pack, or Session settings.", color = vC.dim)
|
} else Text("Nothing open - click a request, a pack, or Session settings.", color = vC.dim)
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Column(modifier = Modifier.fillMaxSize().background(vC.bg)) {
|
Column(modifier = Modifier.fillMaxSize().background(vC.bg)) {
|
||||||
// Panel 1 — unified tab strip (session + pack-settings + request tabs).
|
// Panel 1 - unified tab strip (session + pack-settings + request tabs).
|
||||||
RequestTabStrip(
|
RequestTabStrip(
|
||||||
inTabs = vTabs,
|
inTabs = vTabs,
|
||||||
inActiveKey = when {
|
inActiveKey = when {
|
||||||
@@ -1263,7 +1269,7 @@ internal fun App() {
|
|||||||
inOnCert = { vP.cert = it; vP.dirty = true; persist() },
|
inOnCert = { vP.cert = it; vP.dirty = true; persist() },
|
||||||
inCertHelp = "Used by every request in this pack unless the request sets its own. Overrides the session cert.",
|
inCertHelp = "Used by every request in this pack unless the request sets its own. Overrides the session cert.",
|
||||||
inCertHeading = "Pack client certificate",
|
inCertHeading = "Pack client certificate",
|
||||||
// What this pack inherits from above (session + ancestor packs — itself excluded).
|
// What this pack inherits from above (session + ancestor packs - itself excluded).
|
||||||
inInheritedVars = sourcedVars(scopeChain(vP.parent)),
|
inInheritedVars = sourcedVars(scopeChain(vP.parent)),
|
||||||
inInheritedParams = sourcedParams(scopeChain(vP.parent)),
|
inInheritedParams = sourcedParams(scopeChain(vP.parent)),
|
||||||
inInheritedHeaders = sourcedHeaders(scopeChain(vP.parent)),
|
inInheritedHeaders = sourcedHeaders(scopeChain(vP.parent)),
|
||||||
@@ -1286,13 +1292,13 @@ internal fun App() {
|
|||||||
size = 14.dp
|
size = 14.dp
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Linked copy — read-only. Runs with this pack's Var/Header/Cert; edit the request in “${vP.linkedSource?.name ?: "source"}”.",
|
"Linked copy - read-only. Runs with this pack's Var/Header/Cert; edit the request in “${vP.linkedSource?.name ?: "source"}”.",
|
||||||
color = vC.dim,
|
color = vC.dim,
|
||||||
fontSize = 11.sp
|
fontSize = 11.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Panel 2 — unified method · url · send.
|
// Panel 2 - unified method · url · send.
|
||||||
UrlBar(
|
UrlBar(
|
||||||
inReq = vReq,
|
inReq = vReq,
|
||||||
inLoading = vReqActive.loading,
|
inLoading = vReqActive.loading,
|
||||||
@@ -1438,7 +1444,7 @@ internal fun App() {
|
|||||||
Text("Replace current session?", color = vC.text, fontSize = 16.sp)
|
Text("Replace current session?", color = vC.text, fontSize = 16.sp)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"This session hasn't been saved to a file — continuing will discard it. Save it first if you want to keep it.",
|
"This session hasn't been saved to a file - continuing will discard it. Save it first if you want to keep it.",
|
||||||
color = vC.dim,
|
color = vC.dim,
|
||||||
fontSize = 13.sp
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
@@ -1549,7 +1555,7 @@ internal fun App() {
|
|||||||
Text("Remove pack", color = vC.text, fontSize = 16.sp)
|
Text("Remove pack", color = vC.text, fontSize = 16.sp)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"\"${vRmPack.name}\" will be removed from the session. Unsaved changes are lost — export it first to keep them.",
|
"\"${vRmPack.name}\" will be removed from the session. Unsaved changes are lost - export it first to keep them.",
|
||||||
color = vC.dim,
|
color = vC.dim,
|
||||||
fontSize = 13.sp
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ internal fun imageFileName(inContentType: String?): String = when {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun statusColor(inStatus: Int): Color = when (inStatus) {
|
internal fun statusColor(inStatus: Int): Color = when (inStatus) {
|
||||||
in 200..299 -> Color(0xFF36B37E) // success — green
|
in 200..299 -> Color(0xFF36B37E) // success - green
|
||||||
in 300..399 -> Color(0xFF4C9AFF) // redirect — blue
|
in 300..399 -> Color(0xFF4C9AFF) // redirect - blue
|
||||||
in 400..499 -> Color(0xFFFF991F) // client error — orange (warning)
|
in 400..499 -> Color(0xFFFF991F) // client error - orange (warning)
|
||||||
in 500..599 -> Color(0xFFFF5630) // server error — red
|
in 500..599 -> Color(0xFFFF5630) // server error - red
|
||||||
else -> Color(0xFFFF991F) // unknown / pending — orange
|
else -> Color(0xFFFF991F) // unknown / pending - orange
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ internal class HistoryEntry(
|
|||||||
val request: ApiRequest
|
val request: ApiRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
/** One request plus its live session state — response, in-flight job (for
|
/** One request plus its live session state - response, in-flight job (for
|
||||||
cancel) and which sub-tabs show. Stable identity lets the sidebar and the
|
cancel) and which sub-tabs show. Stable identity lets the sidebar and the
|
||||||
open-tab strip reference the same request without index juggling. */
|
open-tab strip reference the same request without index juggling. */
|
||||||
internal class ReqState(inInitial: ApiRequest) {
|
internal class ReqState(inInitial: ApiRequest) {
|
||||||
@@ -56,11 +56,11 @@ internal class PackState(
|
|||||||
// A linked copy mirrors its source's requests read-only; non-linked packs own
|
// A linked copy mirrors its source's requests read-only; non-linked packs own
|
||||||
// their requests. `requests` resolves to whichever applies.
|
// their requests. `requests` resolves to whichever applies.
|
||||||
var linkedSource by mutableStateOf<PackState?>(null)
|
var linkedSource by mutableStateOf<PackState?>(null)
|
||||||
private val fOwnRequests = mutableStateListOf<ReqState>().apply {
|
private val mOwnRequests = mutableStateListOf<ReqState>().apply {
|
||||||
inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty
|
inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty
|
||||||
}
|
}
|
||||||
val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState>
|
val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState>
|
||||||
get() = linkedSource?.requests ?: fOwnRequests
|
get() = linkedSource?.requests ?: mOwnRequests
|
||||||
val isLinked: Boolean get() = linkedSource != null
|
val isLinked: Boolean get() = linkedSource != null
|
||||||
var parent: PackState? =
|
var parent: PackState? =
|
||||||
null // enclosing pack (null = top-level / root)
|
null // enclosing pack (null = top-level / root)
|
||||||
@@ -82,7 +82,7 @@ internal class PackState(
|
|||||||
inOpenTabs.forEach { vIdx -> requests.getOrNull(vIdx)?.let { add(it) } }
|
inOpenTabs.forEach { vIdx -> requests.getOrNull(vIdx)?.let { add(it) } }
|
||||||
}
|
}
|
||||||
var active by mutableStateOf(requests.getOrNull(inActive)?.takeIf { it in openTabs } ?: openTabs.firstOrNull())
|
var active by mutableStateOf(requests.getOrNull(inActive)?.takeIf { it in openTabs } ?: openTabs.firstOrNull())
|
||||||
var expanded by mutableStateOf(true) // sidebar fold state (transient — not part of the pack file)
|
var expanded by mutableStateOf(true) // sidebar fold state (transient - not part of the pack file)
|
||||||
var envOpen by mutableStateOf(false) // whether this pack's env tab is open in the strip (transient)
|
var envOpen by mutableStateOf(false) // whether this pack's env tab is open in the strip (transient)
|
||||||
|
|
||||||
fun toPack(): Pack = Pack(
|
fun toPack(): Pack = Pack(
|
||||||
@@ -112,7 +112,7 @@ internal const val kSessionTabKey = "session-settings"
|
|||||||
internal class StripTab(val pack: PackState?, val req: ReqState?, val isSession: Boolean = false)
|
internal class StripTab(val pack: PackState?, val req: ReqState?, val isSession: Boolean = false)
|
||||||
|
|
||||||
/** Identity of a strip tab. A linked pack shares the source's ReqState objects,
|
/** Identity of a strip tab. A linked pack shares the source's ReqState objects,
|
||||||
so the key must include the pack — otherwise the source's and linked's tabs
|
so the key must include the pack - otherwise the source's and linked's tabs
|
||||||
for the same request collide (double selection, duplicate key()). Data class →
|
for the same request collide (double selection, duplicate key()). Data class →
|
||||||
structural equality, so compare keys with == (not ===). */
|
structural equality, so compare keys with == (not ===). */
|
||||||
internal data class TabKey(val pack: PackState?, val req: ReqState?)
|
internal data class TabKey(val pack: PackState?, val req: ReqState?)
|
||||||
@@ -138,7 +138,7 @@ internal class TreeDrag {
|
|||||||
var dy by mutableStateOf(0f) // draw-only follow offset for the grabbed element
|
var dy by mutableStateOf(0f) // draw-only follow offset for the grabbed element
|
||||||
var pressRel by mutableStateOf(0) // relY at capture (cursor = grabbed-element top + relY)
|
var pressRel by mutableStateOf(0) // relY at capture (cursor = grabbed-element top + relY)
|
||||||
|
|
||||||
// A press doesn't become a drag until the pointer moves past kDragSlop — so a
|
// A press doesn't become a drag until the pointer moves past kDragSlop - so a
|
||||||
// plain click (which carries a pixel or two of jitter) still selects / opens.
|
// plain click (which carries a pixel or two of jitter) still selects / opens.
|
||||||
var engaged by mutableStateOf(false)
|
var engaged by mutableStateOf(false)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import kotlin.io.encoding.Base64
|
|||||||
// MARK: Minimal X.509 / DER parser
|
// MARK: Minimal X.509 / DER parser
|
||||||
// ==================
|
// ==================
|
||||||
// Some TLS backends (notably Schannel on Windows) expose only Subject / Issuer /
|
// Some TLS backends (notably Schannel on Windows) expose only Subject / Issuer /
|
||||||
// the PEM through CURLINFO_CERTINFO — not the parsed fields (validity, serial,
|
// the PEM through CURLINFO_CERTINFO - not the parsed fields (validity, serial,
|
||||||
// SAN, algorithms). Since every backend gives us the PEM, we parse it ourselves
|
// SAN, algorithms). Since every backend gives us the PEM, we parse it ourselves
|
||||||
// to fill in the detail. Just enough ASN.1 to walk a Certificate; anything odd
|
// to fill in the detail. Just enough ASN.1 to walk a Certificate; anything odd
|
||||||
// is swallowed and the field is simply omitted.
|
// is swallowed and the field is simply omitted.
|
||||||
@@ -94,7 +94,7 @@ private fun pemToDer(inPem: String): ByteArray? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The friendly name (or dotted OID) of the first child OID of a SEQUENCE — used
|
/** The friendly name (or dotted OID) of the first child OID of a SEQUENCE - used
|
||||||
for signatureAlgorithm and subjectPublicKeyInfo's AlgorithmIdentifier. */
|
for signatureAlgorithm and subjectPublicKeyInfo's AlgorithmIdentifier. */
|
||||||
private fun firstOidName(inB: ByteArray, inSeq: IntArray): String? {
|
private fun firstOidName(inB: ByteArray, inSeq: IntArray): String? {
|
||||||
val vR = DerReader(inB, inSeq[1], inSeq[2])
|
val vR = DerReader(inB, inSeq[1], inSeq[2])
|
||||||
@@ -139,7 +139,7 @@ private fun parseAsn1Time(inB: ByteArray, inTlv: IntArray): String? {
|
|||||||
if (inTlv[0] == 0x18) // GeneralizedTime
|
if (inTlv[0] == 0x18) // GeneralizedTime
|
||||||
{
|
{
|
||||||
vYear = vS.substring(0, 4); vRest = vS.substring(4)
|
vYear = vS.substring(0, 4); vRest = vS.substring(4)
|
||||||
} else // UTCTime — RFC 5280 pivot at 50
|
} else // UTCTime - RFC 5280 pivot at 50
|
||||||
{
|
{
|
||||||
val vYy = vS.substring(0, 2).toInt()
|
val vYy = vS.substring(0, 2).toInt()
|
||||||
vYear = (if (vYy >= 50) "19" else "20") + vS.substring(0, 2)
|
vYear = (if (vYy >= 50) "19" else "20") + vS.substring(0, 2)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -11,10 +12,57 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
// back to the default proportional font.
|
// back to the default proportional font.
|
||||||
const val kMonoFamily = "noto-mono"
|
const val kMonoFamily = "noto-mono"
|
||||||
|
|
||||||
/** The raw family name string — used by wrappedRowCount, which measures via the
|
/** Family name the bundled proportional font registers under. */
|
||||||
|
const val kDefaultFamily = "noto-sans"
|
||||||
|
|
||||||
|
/** The raw family name string - used by wrappedRowCount, which measures via the
|
||||||
platform text pipeline (accepts a name string). Null when not bundled. */
|
platform text pipeline (accepts a name string). Null when not bundled. */
|
||||||
expect val monoFontFamilyName: String?
|
expect val monoFontFamilyName: String?
|
||||||
|
|
||||||
/** The material3 Text / BasicTextField-shaped FontFamily for the mono family.
|
/** The material3 Text / BasicTextField-shaped FontFamily for the mono family.
|
||||||
Null when not bundled. */
|
Null when not bundled. */
|
||||||
expect val monoFontFamily: FontFamily?
|
expect val monoFontFamily: FontFamily?
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Default proportional font (seam)
|
||||||
|
// ==================
|
||||||
|
// PARITY: the native side resolves FontFamily.Default to the BUNDLED NotoSans
|
||||||
|
// (SkiaFonts.defaultTypeface is the fallback for every unresolved family), while
|
||||||
|
// stock Compose Desktop on the JVM resolves it to a SYSTEM font (Segoe UI on
|
||||||
|
// Windows, Helvetica on macOS). Different typefaces mean different ascent /
|
||||||
|
// descent / line-gap, so identical layout code puts glyphs at a different height
|
||||||
|
// inside an identically-sized text box - which is what made the SourceTag pill
|
||||||
|
// look vertically centred on JVM and off-centre on native.
|
||||||
|
//
|
||||||
|
// The two stacks must rasterise the SAME face for the comparison to mean
|
||||||
|
// anything, so the app pins its default family explicitly rather than inheriting
|
||||||
|
// whatever the platform picks. Null = the bundled font isn't available, in which
|
||||||
|
// case we fall back to the platform default and the skew returns.
|
||||||
|
|
||||||
|
/** The bundled proportional font, applied app-wide as the default text family.
|
||||||
|
Null when NotoSans isn't shipped (data.kres on native, classpath font/ on jvm). */
|
||||||
|
expect val defaultFontFamily: FontFamily?
|
||||||
|
|
||||||
|
/** Re-stamps every style in a Material 3 [Typography] with [inFamily]. M3 seeds
|
||||||
|
LocalTextStyle from the typography, so this reaches every `Text` that doesn't name
|
||||||
|
a family of its own. A null family leaves the typography untouched. */
|
||||||
|
fun Typography.withDefaultFontFamily(inFamily: FontFamily?): Typography {
|
||||||
|
if (inFamily == null) return this
|
||||||
|
return copy(
|
||||||
|
displayLarge = displayLarge.copy(fontFamily = inFamily),
|
||||||
|
displayMedium = displayMedium.copy(fontFamily = inFamily),
|
||||||
|
displaySmall = displaySmall.copy(fontFamily = inFamily),
|
||||||
|
headlineLarge = headlineLarge.copy(fontFamily = inFamily),
|
||||||
|
headlineMedium = headlineMedium.copy(fontFamily = inFamily),
|
||||||
|
headlineSmall = headlineSmall.copy(fontFamily = inFamily),
|
||||||
|
titleLarge = titleLarge.copy(fontFamily = inFamily),
|
||||||
|
titleMedium = titleMedium.copy(fontFamily = inFamily),
|
||||||
|
titleSmall = titleSmall.copy(fontFamily = inFamily),
|
||||||
|
bodyLarge = bodyLarge.copy(fontFamily = inFamily),
|
||||||
|
bodyMedium = bodyMedium.copy(fontFamily = inFamily),
|
||||||
|
bodySmall = bodySmall.copy(fontFamily = inFamily),
|
||||||
|
labelLarge = labelLarge.copy(fontFamily = inFamily),
|
||||||
|
labelMedium = labelMedium.copy(fontFamily = inFamily),
|
||||||
|
labelSmall = labelSmall.copy(fontFamily = inFamily),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
// respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like
|
// respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like
|
||||||
// typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas)
|
// typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas)
|
||||||
// still reformats instead of failing.
|
// still reformats instead of failing.
|
||||||
private val fPrettyJson = Json {
|
private val mPrettyJson = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
prettyPrintIndent = "\t"
|
prettyPrintIndent = "\t"
|
||||||
isLenient = true
|
isLenient = true
|
||||||
@@ -23,7 +23,7 @@ private val fPrettyJson = Json {
|
|||||||
fun formatBody(inText: String, inFormat: BodyFormat): String =
|
fun formatBody(inText: String, inFormat: BodyFormat): String =
|
||||||
when (inFormat) {
|
when (inFormat) {
|
||||||
BodyFormat.JSON -> runCatching {
|
BodyFormat.JSON -> runCatching {
|
||||||
fPrettyJson.encodeToString(JsonElement.serializer(), fPrettyJson.parseToJsonElement(inText))
|
mPrettyJson.encodeToString(JsonElement.serializer(), mPrettyJson.parseToJsonElement(inText))
|
||||||
}.getOrDefault(inText)
|
}.getOrDefault(inText)
|
||||||
|
|
||||||
BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText)
|
BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText)
|
||||||
@@ -32,7 +32,7 @@ fun formatBody(inText: String, inFormat: BodyFormat): String =
|
|||||||
|
|
||||||
// Minimal XML re-indenter: a newline between adjacent tags, one tab per
|
// Minimal XML re-indenter: a newline between adjacent tags, one tab per
|
||||||
// nesting level (rendered at the editor's tab size). Declarations (<?…?>), comments / doctype (<!…>), self-closing tags and
|
// nesting level (rendered at the editor's tab size). Declarations (<?…?>), comments / doctype (<!…>), self-closing tags and
|
||||||
// single-line <a>text</a> elements don't change depth. Best-effort — content
|
// single-line <a>text</a> elements don't change depth. Best-effort - content
|
||||||
// with a literal '>' inside attributes or CDATA may not round-trip, hence the
|
// with a literal '>' inside attributes or CDATA may not round-trip, hence the
|
||||||
// runCatching guard at the call site.
|
// runCatching guard at the call site.
|
||||||
private fun formatXml(inText: String): String {
|
private fun formatXml(inText: String): String {
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import okio.buffer
|
|||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: HttpRunner — executes an ApiRequest through Ktor
|
// MARK: HttpRunner - executes an ApiRequest through Ktor
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Holds one Ktor HttpClient for the app's lifetime. One engine on every desktop
|
/** Holds one Ktor HttpClient for the app's lifetime. One engine on every desktop
|
||||||
target: Ktor's Curl engine (bundled libcurl — Schannel on Windows, OpenSSL on
|
target: Ktor's Curl engine (bundled libcurl - Schannel on Windows, OpenSSL on
|
||||||
macOS/Linux). Same TLS stack as the client-cert path in CurlMtls.kt.
|
macOS/Linux). Same TLS stack as the client-cert path in CurlMtls.kt.
|
||||||
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 = createApiHttpClient()
|
private val mClient = 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
|
||||||
@@ -32,11 +32,11 @@ class HttpRunner {
|
|||||||
|
|
||||||
suspend fun run(inReq: ApiRequest): ApiResponse {
|
suspend fun run(inReq: ApiRequest): ApiResponse {
|
||||||
// Client-certificate requests bypass Ktor (no engine exposes a cert API)
|
// Client-certificate requests bypass Ktor (no engine exposes a cert API)
|
||||||
// and go straight through libcurl — same bundled TLS stack.
|
// and go straight through libcurl - same bundled TLS stack.
|
||||||
if (inReq.hasClientCert) return curlSendWithClientCert(inReq)
|
if (inReq.hasClientCert) return curlSendWithClientCert(inReq)
|
||||||
val vMark = TimeSource.Monotonic.markNow()
|
val vMark = TimeSource.Monotonic.markNow()
|
||||||
return try {
|
return try {
|
||||||
val vResp: HttpResponse = fClient.request(inReq.url.trim()) {
|
val vResp: HttpResponse = mClient.request(inReq.url.trim()) {
|
||||||
method = HttpMethod.parse(inReq.method.name)
|
method = HttpMethod.parse(inReq.method.name)
|
||||||
inReq.params
|
inReq.params
|
||||||
.filter { it.enabled && it.key.isNotBlank() }
|
.filter { it.enabled && it.key.isNotBlank() }
|
||||||
@@ -81,7 +81,7 @@ class HttpRunner {
|
|||||||
.sortedBy { it.first.lowercase() },
|
.sortedBy { it.first.lowercase() },
|
||||||
body = when {
|
body = when {
|
||||||
vIsImage -> ""
|
vIsImage -> ""
|
||||||
vBinary -> "(${vContentType ?: "binary"} · ${vBody.size} bytes — not shown; use Save as…)"
|
vBinary -> "(${vContentType ?: "binary"} · ${vBody.size} bytes - not shown; use Save as…)"
|
||||||
else -> vBody.decodeToString()
|
else -> vBody.decodeToString()
|
||||||
},
|
},
|
||||||
bytes = vBody,
|
bytes = vBody,
|
||||||
@@ -97,13 +97,13 @@ class HttpRunner {
|
|||||||
httpVersion = vResp.version.toString(),
|
httpVersion = vResp.version.toString(),
|
||||||
)
|
)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
// The caller cancelled (Cancel button) — let it propagate so the
|
// The caller cancelled (Cancel button) - let it propagate so the
|
||||||
// request is dropped rather than reported as a failed response.
|
// request is dropped rather than reported as a failed response.
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
status = 0,
|
status = 0,
|
||||||
statusText = "—",
|
statusText = "-",
|
||||||
timeMs = vMark.elapsedNow().inWholeMilliseconds,
|
timeMs = vMark.elapsedNow().inWholeMilliseconds,
|
||||||
sizeBytes = 0,
|
sizeBytes = 0,
|
||||||
headers = emptyList(),
|
headers = emptyList(),
|
||||||
@@ -113,7 +113,7 @@ class HttpRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() = fClient.close()
|
fun close() = mClient.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Heuristic for a non-text body: textual content types (text, json, xml, html,
|
/** Heuristic for a non-text body: textual content types (text, json, xml, html,
|
||||||
@@ -129,7 +129,7 @@ internal fun isBinaryBody(inContentType: String?, inBytes: ByteArray): Boolean {
|
|||||||
return inBytes.take(1024).any { it.toInt() == 0 } // no type: sniff for NUL bytes
|
return inBytes.take(1024).any { it.toInt() == 0 } // no type: sniff for NUL bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when the bytes are gzip — either the response says so or they carry the
|
/** True when the bytes are gzip - either the response says so or they carry the
|
||||||
gzip magic number (1f 8b). */
|
gzip magic number (1f 8b). */
|
||||||
private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
|
private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
|
||||||
inEncoding?.contains("gzip", ignoreCase = true) == true ||
|
inEncoding?.contains("gzip", ignoreCase = true) == true ||
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
|||||||
/** A request tab pairing an inherited (read-only, source-tagged) list with the
|
/** A request tab pairing an inherited (read-only, source-tagged) list with the
|
||||||
request's own editable list: Override copies an inherited entry down, and own
|
request's own editable list: Override copies an inherited entry down, and own
|
||||||
rows that shadow an inherited one get an OverrideMark. Used by Query / Var /
|
rows that shadow an inherited one get an OverrideMark. Used by Query / Var /
|
||||||
Headers — the only differences are the key case-sensitivity and the labels. */
|
Headers - the only differences are the key case-sensitivity and the labels. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun InheritedEditableTab(
|
internal fun InheritedEditableTab(
|
||||||
inInherited: List<InheritedKv>,
|
inInherited: List<InheritedKv>,
|
||||||
@@ -131,7 +131,7 @@ internal fun SourceTag(inLabel: String, inPath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The tiny marker shown on an own value that shadows an inherited one — hover for
|
/** The tiny marker shown on an own value that shadows an inherited one - hover for
|
||||||
"Overrides <key> from <path>". */
|
"Overrides <key> from <path>". */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun OverrideMark(inKey: String, inPath: String) {
|
internal fun OverrideMark(inKey: String, inPath: String) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** A "pack" is a savable collection of requests plus its environment variables
|
/** A "pack" is a savable collection of requests plus its environment variables
|
||||||
— the export/import unit. Variables are referenced as {{name}} in any URL,
|
- the export/import unit. Variables are referenced as {{name}} in any URL,
|
||||||
query value, header or body and substituted just before the request is sent
|
query value, header or body and substituted just before the request is sent
|
||||||
(see resolveVars in Tools.kt). Packs may be empty; the rich starter set lives
|
(see resolveVars in Tools.kt). Packs may be empty; the rich starter set lives
|
||||||
in defaultSession(). */
|
in defaultSession(). */
|
||||||
@@ -27,7 +27,7 @@ data class Pack(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** A client-certificate (mutual TLS) configuration. Lives on a request today and
|
/** A client-certificate (mutual TLS) configuration. Lives on a request today and
|
||||||
— with the pack tree — on the session / packs too (inherited downward). Maps
|
- with the pack tree - on the session / packs too (inherited downward). Maps
|
||||||
to libcurl's CURLOPT_SSLCERT / SSLCERTTYPE / SSLKEY / SSLKEYTYPE / KEYPASSWD. */
|
to libcurl's CURLOPT_SSLCERT / SSLCERTTYPE / SSLKEY / SSLKEYTYPE / KEYPASSWD. */
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CertConfig(
|
data class CertConfig(
|
||||||
@@ -39,7 +39,7 @@ data class CertConfig(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** The starter SESSION loaded on first launch (and on demand from the session
|
/** The starter SESSION loaded on first launch (and on demand from the session
|
||||||
menu) — a guided tour of every feature against httpbin.org: the inheritance
|
menu) - a guided tour of every feature against httpbin.org: the inheritance
|
||||||
ladder (session → pack → sub-pack → request, for variables / query params /
|
ladder (session → pack → sub-pack → request, for variables / query params /
|
||||||
headers / client cert, innermost wins), loose root requests, a nested sub-pack,
|
headers / client cert, innermost wins), loose root requests, a nested sub-pack,
|
||||||
per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or
|
per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or
|
||||||
@@ -47,7 +47,7 @@ per-request overrides, a client-cert pack, and a linked-copy pack. Each /get or
|
|||||||
inherited result is visible in the response. Loaded as an unsaved session. */
|
inherited result is visible in the response. Loaded as an unsaved session. */
|
||||||
fun defaultSession(): Session = Session(
|
fun defaultSession(): Session = Session(
|
||||||
activePack = 0,
|
activePack = 0,
|
||||||
// ── Session level — the base of every inheritance ladder ──
|
// ── Session level - the base of every inheritance ladder ──
|
||||||
globalEnv = listOf(
|
globalEnv = listOf(
|
||||||
KeyVal("baseUrl", "https://httpbin.org"),
|
KeyVal("baseUrl", "https://httpbin.org"),
|
||||||
KeyVal("token", "session-token-abc"),
|
KeyVal("token", "session-token-abc"),
|
||||||
@@ -64,7 +64,7 @@ fun defaultSession(): Session = Session(
|
|||||||
), // every request gets ?trace=session (echoed in /get args)
|
), // every request gets ?trace=session (echoed in /get args)
|
||||||
// No session-level cert: it would route every request through mTLS and fail
|
// No session-level cert: it would route every request through mTLS and fail
|
||||||
// without a real cert file. Client certs are shown on the "Secure (mTLS)" pack.
|
// without a real cert file. Client certs are shown on the "Secure (mTLS)" pack.
|
||||||
// ── Loose requests at the session root (in no pack — inherit session only) ──
|
// ── Loose requests at the session root (in no pack - inherit session only) ──
|
||||||
root = Pack(
|
root = Pack(
|
||||||
isRoot = true, name = "", requests = listOf(
|
isRoot = true, name = "", requests = listOf(
|
||||||
ApiRequest(name = "Ping (loose)", method = ReqMethod.GET, url = "{{baseUrl}}/get"),
|
ApiRequest(name = "Ping (loose)", method = ReqMethod.GET, url = "{{baseUrl}}/get"),
|
||||||
@@ -72,7 +72,7 @@ fun defaultSession(): Session = Session(
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
packs = listOf(
|
packs = listOf(
|
||||||
// ── Methods — pack header/param/var inherited by its requests, plus a
|
// ── Methods - pack header/param/var inherited by its requests, plus a
|
||||||
// nested sub-pack and per-request overrides of each kind. ──
|
// nested sub-pack and per-request overrides of each kind. ──
|
||||||
SavedPack(
|
SavedPack(
|
||||||
pack = Pack(
|
pack = Pack(
|
||||||
@@ -92,15 +92,15 @@ fun defaultSession(): Session = Session(
|
|||||||
), // overrides the session's apiVer for this pack
|
), // overrides the session's apiVer for this pack
|
||||||
requests = listOf(
|
requests = listOf(
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "GET — echoes inherited", method = ReqMethod.GET, url = "{{baseUrl}}/get",
|
name = "GET - echoes inherited", method = ReqMethod.GET, url = "{{baseUrl}}/get",
|
||||||
params = listOf(KeyVal("q", "compose"))
|
params = listOf(KeyVal("q", "compose"))
|
||||||
), // own q + inherited trace=session + source=methods
|
), // own q + inherited trace=session + source=methods
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "GET — overrides query param", method = ReqMethod.GET, url = "{{baseUrl}}/get",
|
name = "GET - overrides query param", method = ReqMethod.GET, url = "{{baseUrl}}/get",
|
||||||
params = listOf(KeyVal("trace", "request"))
|
params = listOf(KeyVal("trace", "request"))
|
||||||
), // request's trace wins over the session's
|
), // request's trace wins over the session's
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "POST — overrides header",
|
name = "POST - overrides header",
|
||||||
method = ReqMethod.POST,
|
method = ReqMethod.POST,
|
||||||
url = "{{baseUrl}}/post",
|
url = "{{baseUrl}}/post",
|
||||||
headers = listOf(KeyVal("Accept", "text/plain")), // request's Accept wins over the pack's
|
headers = listOf(KeyVal("Accept", "text/plain")), // request's Accept wins over the pack's
|
||||||
@@ -109,17 +109,17 @@ fun defaultSession(): Session = Session(
|
|||||||
body = "{\n \"user\": \"{{user}}\",\n \"api\": \"{{apiVer}}\"\n}"
|
body = "{\n \"user\": \"{{user}}\",\n \"api\": \"{{apiVer}}\"\n}"
|
||||||
),
|
),
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "POST — overrides {{user}} var", method = ReqMethod.POST, url = "{{baseUrl}}/anything",
|
name = "POST - overrides {{user}} var", method = ReqMethod.POST, url = "{{baseUrl}}/anything",
|
||||||
variables = listOf(
|
variables = listOf(
|
||||||
KeyVal(
|
KeyVal(
|
||||||
"user",
|
"user",
|
||||||
"request-user"
|
"request-user"
|
||||||
)
|
)
|
||||||
), // request var beats session/pack — see body echo
|
), // request var beats session/pack - see body echo
|
||||||
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"who\": \"{{user}}\" }"
|
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"who\": \"{{user}}\" }"
|
||||||
),
|
),
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "PUT — replace",
|
name = "PUT - replace",
|
||||||
method = ReqMethod.PUT,
|
method = ReqMethod.PUT,
|
||||||
url = "{{baseUrl}}/put",
|
url = "{{baseUrl}}/put",
|
||||||
bodyType = BodyType.TEXT,
|
bodyType = BodyType.TEXT,
|
||||||
@@ -132,7 +132,7 @@ fun defaultSession(): Session = Session(
|
|||||||
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"hello\": \"world\" }"
|
bodyType = BodyType.TEXT, bodyFormat = BodyFormat.JSON, body = "{ \"hello\": \"world\" }"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Nested sub-pack — inherits Session → Methods → here (apiVer becomes v3,
|
// Nested sub-pack - inherits Session → Methods → here (apiVer becomes v3,
|
||||||
// plus its own X-Nested header; trace / source / Accept still flow down).
|
// plus its own X-Nested header; trace / source / Accept still flow down).
|
||||||
subPacks = listOf(
|
subPacks = listOf(
|
||||||
Pack(
|
Pack(
|
||||||
@@ -141,7 +141,7 @@ fun defaultSession(): Session = Session(
|
|||||||
headers = listOf(KeyVal("X-Nested", "true")),
|
headers = listOf(KeyVal("X-Nested", "true")),
|
||||||
requests = listOf(
|
requests = listOf(
|
||||||
ApiRequest(
|
ApiRequest(
|
||||||
name = "GET — deep inheritance",
|
name = "GET - deep inheritance",
|
||||||
method = ReqMethod.GET,
|
method = ReqMethod.GET,
|
||||||
url = "{{baseUrl}}/anything/{{apiVer}}"
|
url = "{{baseUrl}}/anything/{{apiVer}}"
|
||||||
),
|
),
|
||||||
@@ -150,7 +150,7 @@ fun defaultSession(): Session = Session(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// ── Auth & status — uses the inherited {{token}} / {{user}} / {{password}} ──
|
// ── Auth & status - uses the inherited {{token}} / {{user}} / {{password}} ──
|
||||||
SavedPack(
|
SavedPack(
|
||||||
pack = Pack(
|
pack = Pack(
|
||||||
name = "Auth & status", color = 2, requests = listOf(
|
name = "Auth & status", color = 2, requests = listOf(
|
||||||
@@ -170,7 +170,7 @@ fun defaultSession(): Session = Session(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// ── Formats — response viewer (JSON / XML / HTML / image) ──
|
// ── Formats - response viewer (JSON / XML / HTML / image) ──
|
||||||
SavedPack(
|
SavedPack(
|
||||||
pack = Pack(
|
pack = Pack(
|
||||||
name = "Formats", color = 3, requests = listOf(
|
name = "Formats", color = 3, requests = listOf(
|
||||||
@@ -183,7 +183,7 @@ fun defaultSession(): Session = Session(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
// ── Secure (mTLS) — a PACK-LEVEL client cert, inherited by its requests.
|
// ── Secure (mTLS) - a PACK-LEVEL client cert, inherited by its requests.
|
||||||
// Open a request's Cert tab to see the inherited cert (source pill +
|
// Open a request's Cert tab to see the inherited cert (source pill +
|
||||||
// Override). Set real cert paths to actually send; the example paths
|
// Override). Set real cert paths to actually send; the example paths
|
||||||
// won't load. The second request overrides with its own cert. ──
|
// won't load. The second request overrides with its own cert. ──
|
||||||
@@ -303,7 +303,7 @@ fun ApiRequest.bodyContentType(): String? = when (bodyType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Response (runtime only — not part of a pack)
|
// MARK: Response (runtime only - not part of a pack)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
class ApiResponse(
|
class ApiResponse(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import okio.Path.Companion.toPath
|
|||||||
// MARK: Pack persistence (okio + kotlinx.serialization)
|
// MARK: Pack persistence (okio + kotlinx.serialization)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
private val fJson = Json {
|
private val mJson = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
prettyPrintIndent = " "
|
prettyPrintIndent = " "
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -20,7 +20,7 @@ private val fJson = Json {
|
|||||||
error message. */
|
error message. */
|
||||||
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
||||||
systemFileSystem.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inPack))
|
writeUtf8(mJson.encodeToString(inPack))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -30,7 +30,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 = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Pack>(vText))
|
Result.success(mJson.decodeFromString<Pack>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,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 {
|
||||||
systemFileSystem.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inSession))
|
writeUtf8(mJson.encodeToString(inSession))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -48,7 +48,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 = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Session>(vText))
|
Result.success(mJson.decodeFromString<Session>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
|
|||||||
/** Re-indent a JSON response for display; returns the input unchanged if it
|
/** Re-indent a JSON response for display; returns the input unchanged if it
|
||||||
isn't valid JSON (so plain-text / HTML responses still show). */
|
isn't valid JSON (so plain-text / HTML responses still show). */
|
||||||
fun prettyJsonOrRaw(inText: String): String = try {
|
fun prettyJsonOrRaw(inText: String): String = try {
|
||||||
fJson.encodeToString(JsonElement.serializer(), fJson.parseToJsonElement(inText))
|
mJson.encodeToString(JsonElement.serializer(), mJson.parseToJsonElement(inText))
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
inText
|
inText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ data class SavedPack(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** A whole session, the export/import unit for the working set: every open pack
|
/** A whole session, the export/import unit for the working set: every open pack
|
||||||
(self-contained — full content embedded) plus the shared global env and which
|
(self-contained - full content embedded) plus the shared global env and which
|
||||||
pack was active. Only one session is open at a time. */
|
pack was active. Only one session is open at a time. */
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Session(
|
data class Session(
|
||||||
@@ -64,7 +64,7 @@ data class Session(
|
|||||||
// MARK: Load / save (app-data dir via okio)
|
// MARK: Load / save (app-data dir via okio)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
private val fStateJson = Json {
|
private val mStateJson = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
@@ -88,26 +88,26 @@ fun loadAppState(): AppState {
|
|||||||
val vPath = stateFilePath() ?: return AppState()
|
val vPath = stateFilePath() ?: return AppState()
|
||||||
return try {
|
return try {
|
||||||
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
||||||
fStateJson.decodeFromString<AppState>(vText)
|
mStateJson.decodeFromString<AppState>(vText)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
AppState()
|
AppState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write the app state. Best-effort — failures are swallowed (it's a cache of
|
/** Write the app state. Best-effort - failures are swallowed (it's a cache of
|
||||||
the session, not the user's exported .json packs). */
|
the session, not the user's exported .json packs). */
|
||||||
fun saveAppState(inState: AppState) {
|
fun saveAppState(inState: AppState) {
|
||||||
val vPath = stateFilePath() ?: return
|
val vPath = stateFilePath() ?: return
|
||||||
try {
|
try {
|
||||||
systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
systemFileSystem.write(vPath.toPath()) { writeUtf8(mStateJson.encodeToString(inState)) }
|
||||||
} catch (_: Throwable) {
|
} catch (_: 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Open the per-user app-data folder where the state file lives in the OS file
|
/** Open the per-user app-data folder where the state file lives in the OS file
|
||||||
manager. Revealing the state file opens its containing folder, which SDL has
|
manager. Revealing the state file opens its containing folder, which SDL has
|
||||||
already created even when nothing's been saved yet. Fire-and-forget — the
|
already created even when nothing's been saved yet. Fire-and-forget - the
|
||||||
reveal launches off the main thread (see revealInFileManager). */
|
reveal launches off the main thread (see revealInFileManager). */
|
||||||
fun openSettingsFolder() {
|
fun openSettingsFolder() {
|
||||||
val vPath = stateFilePath() ?: return
|
val vPath = stateFilePath() ?: return
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ internal fun OptionsMenu(
|
|||||||
Text("Light mode", color = if (!inDark) c.accent else c.text, fontSize = 13.sp)
|
Text("Light mode", color = if (!inDark) c.accent else c.text, fontSize = 13.sp)
|
||||||
}, onClick = { if (inDark) inOnToggleTheme(); vOpen = false })
|
}, onClick = { if (inDark) inOnToggleTheme(); vOpen = false })
|
||||||
HorizontalDivider(color = c.border)
|
HorizontalDivider(color = c.border)
|
||||||
// Palette picker — a swatch of each theme's primary + a check on the active one.
|
// Palette picker - a swatch of each theme's primary + a check on the active one.
|
||||||
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
|
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
|
||||||
Text("Theme", color = c.dim, fontSize = 11.sp)
|
Text("Theme", color = c.dim, fontSize = 11.sp)
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ internal fun OptionsMenu(
|
|||||||
// MARK: Method picker (dropdown)
|
// MARK: Method picker (dropdown)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Panel 2 — one unified bar: a method dropdown (coloured label + unfold arrows),
|
/** Panel 2 - one unified bar: a method dropdown (coloured label + unfold arrows),
|
||||||
a borderless URL field that melts into the bar, and Send (or Cancel). */
|
a borderless URL field that melts into the bar, and Send (or Cancel). */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun UrlBar(
|
internal fun UrlBar(
|
||||||
@@ -126,7 +126,7 @@ internal fun UrlBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Borderless URL field — no box, so it reads as part of the bar.
|
// Borderless URL field - no box, so it reads as part of the bar.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f).alpha(if (inReadOnly) 0.55f else 1f).onKeyEvent { ev ->
|
modifier = Modifier.weight(1f).alpha(if (inReadOnly) 0.55f else 1f).onKeyEvent { ev ->
|
||||||
if (ev.type == KeyEventType.KeyDown && (ev.key == Key.Enter || ev.key == Key.NumPadEnter)) {
|
if (ev.type == KeyEventType.KeyDown && (ev.key == Key.Enter || ev.key == Key.NumPadEnter)) {
|
||||||
@@ -154,8 +154,8 @@ internal fun UrlBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inspect the server's TLS certificate chain (handshake-only probe).
|
// Inspect the server's TLS certificate chain (handshake-only probe).
|
||||||
// During the probe the lock glyph swaps to a spinner IN PLACE — same
|
// During the probe the lock glyph swaps to a spinner IN PLACE - same
|
||||||
// box, padding and size — so the bar doesn't shift.
|
// box, padding and size - so the bar doesn't shift.
|
||||||
val vLockHoverSrc = remember { MutableInteractionSource() }
|
val vLockHoverSrc = remember { MutableInteractionSource() }
|
||||||
val vLockHover by vLockHoverSrc.collectIsHoveredAsState()
|
val vLockHover by vLockHoverSrc.collectIsHoveredAsState()
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
@@ -185,7 +185,7 @@ internal fun UrlBar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send and Cancel are the same Material Button (same MinHeight + padding)
|
// Send and Cancel are the same Material Button (same MinHeight + padding)
|
||||||
// so the bar never changes height when toggling — Cancel is just red.
|
// so the bar never changes height when toggling - Cancel is just red.
|
||||||
if (inLoading) Button(
|
if (inLoading) Button(
|
||||||
onClick = inOnCancel,
|
onClick = inOnCancel,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import com.compose.sdl.icons.MaterialSymbols
|
|||||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Panel 3 — request builder (Query / Headers / Body)
|
// MARK: Panel 3 - request builder (Query / Headers / Body)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Tabs to edit query params, headers and the body, a Preview toggle top-right,
|
/** Tabs to edit query params, headers and the body, a Preview toggle top-right,
|
||||||
@@ -63,7 +63,7 @@ internal fun RequestBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tabs, with the status message and a Preview toggle (icon only) on the
|
// Tabs, with the status message and a Preview toggle (icon only) on the
|
||||||
// same line — the toggle no longer needs its own row.
|
// same line - the toggle no longer needs its own row.
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 10.dp, top = 6.dp),
|
modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 10.dp, top = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -98,7 +98,7 @@ internal fun RequestBuilder(
|
|||||||
}
|
}
|
||||||
HorizontalDivider(color = c.border)
|
HorizontalDivider(color = c.border)
|
||||||
|
|
||||||
// Tab content — scrolls, except a Text body which fills the whole panel
|
// Tab content - scrolls, except a Text body which fills the whole panel
|
||||||
// (so it stays one editable surface you can click anywhere in). Greyed when
|
// (so it stays one editable surface you can click anywhere in). Greyed when
|
||||||
// read-only (a linked-copy request).
|
// read-only (a linked-copy request).
|
||||||
val vScroll = rememberScrollState()
|
val vScroll = rememberScrollState()
|
||||||
@@ -117,7 +117,7 @@ internal fun RequestBuilder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body-type selector — only on the Body tab, pinned at the bottom. For a
|
// Body-type selector - only on the Body tab, pinned at the bottom. For a
|
||||||
// Text body the format/type picker sits beside it; that type drives both the
|
// Text body the format/type picker sits beside it; that type drives both the
|
||||||
// syntax colours and the sent Content-Type.
|
// syntax colours and the sent Content-Type.
|
||||||
if (inRs.reqTab == 3) {
|
if (inRs.reqTab == 3) {
|
||||||
@@ -170,7 +170,7 @@ internal fun FormatButton(inOnClick: () -> Unit) {
|
|||||||
HoverIconBtn(MaterialSymbols.AutoFixHigh, "Format (pretty-print)", inOnClick)
|
HoverIconBtn(MaterialSymbols.AutoFixHigh, "Format (pretty-print)", inOnClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Icon button with the toolbar's standard hover treatment — accent-tinted
|
/** Icon button with the toolbar's standard hover treatment - accent-tinted
|
||||||
background + accent icon on hover (matching the TLS chain button). inActive
|
background + accent icon on hover (matching the TLS chain button). inActive
|
||||||
keeps it lit, for toggles such as the preview eye. */
|
keeps it lit, for toggles such as the preview eye. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -212,7 +212,7 @@ internal fun HoverIconBtn(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Tab-size picker (2 / 4 / 8), shown left of the format button. Sets the global
|
/** Tab-size picker (2 / 4 / 8), shown left of the format button. Sets the global
|
||||||
editor tab width via TextLayoutConfig — how wide a typed '\t' renders AND how
|
editor tab width via TextLayoutConfig - how wide a typed '\t' renders AND how
|
||||||
deep the formatter indents. Snapshot-backed, so the change is live. */
|
deep the formatter indents. Snapshot-backed, so the change is live. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TabSizeSelector() {
|
internal fun TabSizeSelector() {
|
||||||
@@ -258,7 +258,7 @@ internal fun TabSizeSelector() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FILE body — pick a file; its path is stored in body and sent as raw bytes. */
|
/** FILE body - pick a file; its path is stored in body and sent as raw bytes. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) -> Unit) {
|
internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) -> Unit) {
|
||||||
val c = LocalAppColors.current
|
val c = LocalAppColors.current
|
||||||
@@ -271,7 +271,7 @@ internal fun FileBody(inReq: ApiRequest, inEdit: ((ApiRequest) -> ApiRequest) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Client-certificate (mTLS) editor — certificate + optional separate key +
|
/** Client-certificate (mTLS) editor - certificate + optional separate key +
|
||||||
passphrase. PEM/DER are used directly by OpenSSL on macOS/Linux; on Windows
|
passphrase. PEM/DER are used directly by OpenSSL on macOS/Linux; on Windows
|
||||||
they're imported into the certificate store for the request then removed
|
they're imported into the certificate store for the request then removed
|
||||||
(see CurlMtls). PKCS#12 bundles its own private key. */
|
(see CurlMtls). PKCS#12 bundles its own private key. */
|
||||||
@@ -331,7 +331,7 @@ internal fun CertConfigEditor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
inCert.keyPath.ifBlank { "Optional — only if the key is in a separate file." },
|
inCert.keyPath.ifBlank { "Optional - only if the key is in a separate file." },
|
||||||
color = if (inCert.keyPath.isNotBlank()) c.text else c.dim, fontSize = 12.sp,
|
color = if (inCert.keyPath.isNotBlank()) c.text else c.dim, fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ internal fun RequestCertTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scope settings editor (Variables / Query / Headers / Cert sub-tabs) — shared by
|
/** Scope settings editor (Variables / Query / Headers / Cert sub-tabs) - shared by
|
||||||
the session settings tab and each pack's settings tab. Each sub-tab shows what
|
the session settings tab and each pack's settings tab. Each sub-tab shows what
|
||||||
the scope inherits from above (source-tagged, with Override) over its own
|
the scope inherits from above (source-tagged, with Override) over its own
|
||||||
editable list, mirroring the request panels. */
|
editable list, mirroring the request panels. */
|
||||||
@@ -415,7 +415,7 @@ internal fun ScopeSettings(
|
|||||||
inParams: List<KeyVal>, inOnParams: (List<KeyVal>) -> Unit, inParamHelp: String,
|
inParams: List<KeyVal>, inOnParams: (List<KeyVal>) -> Unit, inParamHelp: String,
|
||||||
inHeaders: List<KeyVal>, inOnHeaders: (List<KeyVal>) -> Unit, inHeaderHelp: String,
|
inHeaders: List<KeyVal>, inOnHeaders: (List<KeyVal>) -> Unit, inHeaderHelp: String,
|
||||||
inCert: CertConfig?, inOnCert: (CertConfig?) -> Unit, inCertHelp: String, inCertHeading: String,
|
inCert: CertConfig?, inOnCert: (CertConfig?) -> Unit, inCertHelp: String, inCertHeading: String,
|
||||||
// What this scope inherits from above (empty for the session — it's the top).
|
// What this scope inherits from above (empty for the session - it's the top).
|
||||||
inInheritedVars: List<InheritedKv> = emptyList(),
|
inInheritedVars: List<InheritedKv> = emptyList(),
|
||||||
inInheritedParams: List<InheritedKv> = emptyList(),
|
inInheritedParams: List<InheritedKv> = emptyList(),
|
||||||
inInheritedHeaders: List<InheritedKv> = emptyList(),
|
inInheritedHeaders: List<InheritedKv> = emptyList(),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
|||||||
|
|
||||||
/** Strip of open requests above the editor. Each tab is clickable to select
|
/** Strip of open requests above the editor. Each tab is clickable to select
|
||||||
and drag-reorderable. While dragging, the grabbed tab lifts (zIndex),
|
and drag-reorderable. While dragging, the grabbed tab lifts (zIndex),
|
||||||
fades, and follows the cursor via a graphicsLayer translation — its
|
fades, and follows the cursor via a graphicsLayer translation - its
|
||||||
neighbours don't shuffle; the reorder commits on drop, to the slot whose
|
neighbours don't shuffle; the reorder commits on drop, to the slot whose
|
||||||
neighbours' centres the cursor passed. key(tab) keeps each tab's
|
neighbours' centres the cursor passed. key(tab) keeps each tab's
|
||||||
LayoutNode stable so the drag session survives the reorder. */
|
LayoutNode stable so the drag session survives the reorder. */
|
||||||
@@ -57,7 +57,7 @@ internal fun RequestTabStrip(
|
|||||||
var vDragTarget by remember { mutableStateOf(-1) }
|
var vDragTarget by remember { mutableStateOf(-1) }
|
||||||
|
|
||||||
// One shared context menu, anchored to the tab that opened it (m3 DropdownMenu
|
// One shared context menu, anchored to the tab that opened it (m3 DropdownMenu
|
||||||
// is parent-anchored, not cursor-anchored — the old x/y capture is gone).
|
// is parent-anchored, not cursor-anchored - the old x/y capture is gone).
|
||||||
var vMenu by remember { mutableStateOf(false) }
|
var vMenu by remember { mutableStateOf(false) }
|
||||||
var vMenuTab by remember { mutableStateOf<StripTab?>(null) }
|
var vMenuTab by remember { mutableStateOf<StripTab?>(null) }
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ internal fun RequestTabStrip(
|
|||||||
.onGloballyPositioned { vLeft[vKey] = it.x }
|
.onGloballyPositioned { vLeft[vKey] = it.x }
|
||||||
.onSizeChanged { vWidth[vKey] = it.width }
|
.onSizeChanged { vWidth[vKey] = it.width }
|
||||||
// NOTE: `alpha` and `translationX` MUST be on the same graphicsLayer.
|
// NOTE: `alpha` and `translationX` MUST be on the same graphicsLayer.
|
||||||
// `Modifier.alpha` expands to `graphicsLayer(alpha, clip = true)` — clip is
|
// `Modifier.alpha` expands to `graphicsLayer(alpha, clip = true)` - clip is
|
||||||
// applied in the ALPHA layer's local coord space (before any child layer's
|
// applied in the ALPHA layer's local coord space (before any child layer's
|
||||||
// translation), so `.alpha().graphicsLayer(translationX = X)` clips the drag
|
// translation), so `.alpha().graphicsLayer(translationX = X)` clips the drag
|
||||||
// ghost inside its original bounds and the ghost disappears as it slides. One
|
// ghost inside its original bounds and the ghost disappears as it slides. One
|
||||||
@@ -103,7 +103,7 @@ internal fun RequestTabStrip(
|
|||||||
.pointerInput(vKey) {
|
.pointerInput(vKey) {
|
||||||
// Accumulate the per-frame delta rather than reading change.position.
|
// Accumulate the per-frame delta rather than reading change.position.
|
||||||
// The tab wears a graphicsLayer(translationX = vDragDx) while dragging,
|
// The tab wears a graphicsLayer(translationX = vDragDx) while dragging,
|
||||||
// which transforms the modifier's local pointer frame — so position.x
|
// which transforms the modifier's local pointer frame - so position.x
|
||||||
// would compensate for the translation each frame and the tab would
|
// would compensate for the translation each frame and the tab would
|
||||||
// trail the mouse by roughly half its offset. dragAmount is the
|
// trail the mouse by roughly half its offset. dragAmount is the
|
||||||
// root-frame delta and doesn't feed back through the layer transform.
|
// root-frame delta and doesn't feed back through the layer transform.
|
||||||
@@ -143,7 +143,7 @@ internal fun RequestTabStrip(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Middle-click closes the tab; right-click opens the tab menu. Compose
|
// Middle-click closes the tab; right-click opens the tab menu. Compose
|
||||||
// has no first-class match for either — inline pointerInput checks the
|
// has no first-class match for either - inline pointerInput checks the
|
||||||
// pressed PointerButton at each Press.
|
// pressed PointerButton at each Press.
|
||||||
.pointerInput(vTab) {
|
.pointerInput(vTab) {
|
||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ internal fun PackColorPicker(inSelected: Int, inOnPick: (Int) -> Unit) {
|
|||||||
// MARK: Add-pack menu (header '+')
|
// MARK: Add-pack menu (header '+')
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** The header '+' — a small menu to add a pack to the open session, either blank
|
/** The header '+' - a small menu to add a pack to the open session, either blank
|
||||||
or imported from a .json file. Importing a pack always lands it in the session. */
|
or imported from a .json file. Importing a pack always lands it in the session. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPackMenu(inOnNewRequest: () -> Unit, inOnNew: () -> Unit, inOnImport: () -> Unit) {
|
internal fun AddPackMenu(inOnNewRequest: () -> Unit, inOnNew: () -> Unit, inOnImport: () -> Unit) {
|
||||||
@@ -131,7 +131,7 @@ internal fun ellipsizeMiddle(inText: String, inMax: Int = 46): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Header dropdown for the one open session. Shows its file name with the full
|
/** Header dropdown for the one open session. Shows its file name with the full
|
||||||
path underneath when it has a file (so you can see it's saved — file-backed
|
path underneath when it has a file (so you can see it's saved - file-backed
|
||||||
sessions auto-save), or "Untitled session" with a dot when it has no file yet.
|
sessions auto-save), or "Untitled session" with a dot when it has no file yet.
|
||||||
Menu: Save / Save as… / Rename / Reveal / Open… / New + a recent list. */
|
Menu: Save / Save as… / Rename / Reveal / Open… / New + a recent list. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -240,7 +240,7 @@ internal fun SessionMenu(
|
|||||||
HorizontalDivider(color = c.border)
|
HorizontalDivider(color = c.border)
|
||||||
// Aligned to the icon column (the DropdownMenuItem's 16dp pad), so
|
// Aligned to the icon column (the DropdownMenuItem's 16dp pad), so
|
||||||
// it lines up with the recent items' file icon. Padding goes on a
|
// it lines up with the recent items' file icon. Padding goes on a
|
||||||
// wrapping Box — a leaf Text's own start padding isn't applied to its
|
// wrapping Box - a leaf Text's own start padding isn't applied to its
|
||||||
// draw position by this layout engine.
|
// draw position by this layout engine.
|
||||||
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
|
Box(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 2.dp)) {
|
||||||
Text("Recent", color = c.dim, fontSize = 11.sp)
|
Text("Recent", color = c.dim, fontSize = 11.sp)
|
||||||
@@ -263,7 +263,7 @@ internal fun SessionMenu(
|
|||||||
Text(fileLeaf(vPath), color = c.text, fontSize = 12.sp)
|
Text(fileLeaf(vPath), color = c.text, fontSize = 12.sp)
|
||||||
Text(ellipsizeMiddle(vPath, 32), color = c.dim, fontSize = 10.sp, softWrap = false)
|
Text(ellipsizeMiddle(vPath, 32), color = c.dim, fontSize = 10.sp, softWrap = false)
|
||||||
}
|
}
|
||||||
// Reveal this session's folder — its own hover + tooltip so it
|
// Reveal this session's folder - its own hover + tooltip so it
|
||||||
// reads as a separate action from "open the session".
|
// reads as a separate action from "open the session".
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||||
@@ -332,7 +332,7 @@ internal class PackOps(
|
|||||||
|
|
||||||
/** Renders a pack then its sub-packs recursively, each indented by its depth.
|
/** Renders a pack then its sub-packs recursively, each indented by its depth.
|
||||||
inSiblings is the list this pack belongs to (top-level vPacks, or a parent's
|
inSiblings is the list this pack belongs to (top-level vPacks, or a parent's
|
||||||
subPacks) — used to position the pack-reorder drop bars by index. */
|
subPacks) - used to position the pack-reorder drop bars by index. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PackTree(
|
internal fun PackTree(
|
||||||
inPack: PackState,
|
inPack: PackState,
|
||||||
@@ -519,7 +519,7 @@ internal fun PackSection(
|
|||||||
Text(inPack.name, color = c.text, fontSize = 14.sp, modifier = Modifier.weight(1f))
|
Text(inPack.name, color = c.text, fontSize = 14.sp, modifier = Modifier.weight(1f))
|
||||||
Text("${inPack.requests.size}", color = c.dim, fontSize = 11.sp)
|
Text("${inPack.requests.size}", color = c.dim, fontSize = 11.sp)
|
||||||
}
|
}
|
||||||
// + (new request) and ⋮ (pack menu) — both reveal on hover only, so the
|
// + (new request) and ⋮ (pack menu) - both reveal on hover only, so the
|
||||||
// header width never shifts (alpha, not conditional layout). A linked
|
// header width never shifts (alpha, not conditional layout). A linked
|
||||||
// copy mirrors its source's requests, so it has no "new request".
|
// copy mirrors its source's requests, so it has no "new request".
|
||||||
Row(
|
Row(
|
||||||
@@ -573,7 +573,7 @@ internal fun PackSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Pack body — request list (expanded, or always for the loose root).
|
// Pack body - request list (expanded, or always for the loose root).
|
||||||
// Rows drag within and across packs via the shared controller; the drop
|
// Rows drag within and across packs via the shared controller; the drop
|
||||||
// bar shows here whenever this pack is the resolved target.
|
// bar shows here whenever this pack is the resolved target.
|
||||||
if (inPack.expanded || inHeaderless) {
|
if (inPack.expanded || inHeaderless) {
|
||||||
@@ -584,7 +584,7 @@ internal fun PackSection(
|
|||||||
val vBarBefore = if (vIsTarget) vOthers.getOrNull(inDrag.dropIndex) else null
|
val vBarBefore = if (vIsTarget) vOthers.getOrNull(inDrag.dropIndex) else null
|
||||||
val vBarAtEnd = vIsTarget && inDrag.dropIndex >= vOthers.size
|
val vBarAtEnd = vIsTarget && inDrag.dropIndex >= vOthers.size
|
||||||
// The loose root has no header, so register the body box as its drop
|
// The loose root has no header, so register the body box as its drop
|
||||||
// anchor — that gives an empty root a region to target while dragging.
|
// anchor - that gives an empty root a region to target while dragging.
|
||||||
var vBodyMod = Modifier.fillMaxWidth().padding(start = 10.dp, top = 2.dp, bottom = 4.dp)
|
var vBodyMod = Modifier.fillMaxWidth().padding(start = 10.dp, top = 2.dp, bottom = 4.dp)
|
||||||
if (inHeaderless) vBodyMod = vBodyMod
|
if (inHeaderless) vBodyMod = vBodyMod
|
||||||
.onGloballyPositioned { inDrag.headTop[inPack] = it.y }
|
.onGloballyPositioned { inDrag.headTop[inPack] = it.y }
|
||||||
@@ -605,7 +605,7 @@ internal fun PackSection(
|
|||||||
key(vRs) {
|
key(vRs) {
|
||||||
val vDragged = vRs === inDrag.dragReq && vMoving
|
val vDragged = vRs === inDrag.dragReq && vMoving
|
||||||
var vMod: Modifier = Modifier
|
var vMod: Modifier = Modifier
|
||||||
// Linked copies mirror the source's requests — read-only, so
|
// Linked copies mirror the source's requests - read-only, so
|
||||||
// their rows neither register geometry (would clobber the
|
// their rows neither register geometry (would clobber the
|
||||||
// source's) nor drag.
|
// source's) nor drag.
|
||||||
if (!inPack.isLinked) vMod = vMod
|
if (!inPack.isLinked) vMod = vMod
|
||||||
@@ -616,7 +616,7 @@ internal fun PackSection(
|
|||||||
if (vDragged) vMod =
|
if (vDragged) vMod =
|
||||||
vMod.zIndex(1f).graphicsLayer(alpha = 0.65f, translationX = 0f, translationY = inDrag.dy)
|
vMod.zIndex(1f).graphicsLayer(alpha = 0.65f, translationX = 0f, translationY = inDrag.dy)
|
||||||
if (!inPack.isLinked) vMod = vMod.pointerInput(vRs) {
|
if (!inPack.isLinked) vMod = vMod.pointerInput(vRs) {
|
||||||
// Same graphicsLayer feedback concern as the pack drag —
|
// Same graphicsLayer feedback concern as the pack drag -
|
||||||
// accumulate dragAmount instead of reading change.position.
|
// accumulate dragAmount instead of reading change.position.
|
||||||
detectDragGestures(
|
detectDragGestures(
|
||||||
onDragStart = { offset ->
|
onDragStart = { offset ->
|
||||||
@@ -706,7 +706,7 @@ internal fun RequestRow(
|
|||||||
) {
|
) {
|
||||||
MethodTag(vReq.method)
|
MethodTag(vReq.method)
|
||||||
Text(vReq.name, color = c.text, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
Text(vReq.name, color = c.text, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
||||||
// Overflow (vertical-dots) menu — always laid out so the row width
|
// Overflow (vertical-dots) menu - always laid out so the row width
|
||||||
// never changes; only its opacity toggles on hover / while open. Delete
|
// never changes; only its opacity toggles on hover / while open. Delete
|
||||||
// lives inside this menu (no separate button on the row).
|
// lives inside this menu (no separate button on the row).
|
||||||
Box(modifier = Modifier.alpha(if (vHover || vMenu) 1f else 0f)) {
|
Box(modifier = Modifier.alpha(if (vHover || vMenu) 1f else 0f)) {
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import androidx.compose.ui.text.buildAnnotatedString
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: BodyFormat — viewer/highlighter selection
|
// MARK: BodyFormat - viewer/highlighter selection
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Format of a body payload as shown in the viewer, and (for a TEXT request body)
|
/** Format of a body payload as shown in the viewer, and (for a TEXT request body)
|
||||||
the type the user picks on the right of the Body tab — it drives both syntax
|
the type the user picks on the right of the Body tab - it drives both syntax
|
||||||
highlighting and the sent Content-Type. RAW disables highlighting and falls back
|
highlighting and the sent Content-Type. RAW disables highlighting and falls back
|
||||||
to the selectable BasicTextField; other values pick a tokeniser. */
|
to the selectable BasicTextField; other values pick a tokeniser. */
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -68,7 +68,7 @@ data class SyntaxPalette(
|
|||||||
val punct: Color,
|
val punct: Color,
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
/** Light-on-dark — VS Code Dark+. */
|
/** Light-on-dark - VS Code Dark+. */
|
||||||
val Dark = SyntaxPalette(
|
val Dark = SyntaxPalette(
|
||||||
key = Color(0xFF9CDCFE),
|
key = Color(0xFF9CDCFE),
|
||||||
string = Color(0xFFCE9178),
|
string = Color(0xFFCE9178),
|
||||||
@@ -79,7 +79,7 @@ data class SyntaxPalette(
|
|||||||
punct = Color(0xFFCCCCCC),
|
punct = Color(0xFFCCCCCC),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Dark-on-light — VS Code Light+. Higher contrast on white. */
|
/** Dark-on-light - VS Code Light+. Higher contrast on white. */
|
||||||
val Light = SyntaxPalette(
|
val Light = SyntaxPalette(
|
||||||
key = Color(0xFF0451A5),
|
key = Color(0xFF0451A5),
|
||||||
string = Color(0xFFA31515),
|
string = Color(0xFFA31515),
|
||||||
@@ -113,13 +113,13 @@ fun highlight(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: JSON tokeniser — single-pass, error-tolerant
|
// MARK: JSON tokeniser - single-pass, error-tolerant
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Tokeniser for JSON. Walks the string once and emits style spans for
|
/** Tokeniser for JSON. Walks the string once and emits style spans for
|
||||||
strings, numbers, booleans, null, punctuation. Distinguishes "keys"
|
strings, numbers, booleans, null, punctuation. Distinguishes "keys"
|
||||||
(strings followed by ':') from string values by peeking after each
|
(strings followed by ':') from string values by peeking after each
|
||||||
string. Error-tolerant — malformed JSON still highlights as best
|
string. Error-tolerant - malformed JSON still highlights as best
|
||||||
effort rather than throwing. */
|
effort rather than throwing. */
|
||||||
private fun highlightJson(inText: String, inP: SyntaxPalette): AnnotatedString = buildAnnotatedString {
|
private fun highlightJson(inText: String, inP: SyntaxPalette): AnnotatedString = buildAnnotatedString {
|
||||||
val vN = inText.length
|
val vN = inText.length
|
||||||
@@ -224,7 +224,7 @@ private fun highlightXml(inText: String, inP: SyntaxPalette): AnnotatedString =
|
|||||||
append(inText.substring(vTagStart, vJ))
|
append(inText.substring(vTagStart, vJ))
|
||||||
pop()
|
pop()
|
||||||
}
|
}
|
||||||
// Attributes — name="value" pairs.
|
// Attributes - name="value" pairs.
|
||||||
while (vJ < vGt - 1) {
|
while (vJ < vGt - 1) {
|
||||||
val vC2 = inText[vJ]
|
val vC2 = inText[vJ]
|
||||||
when {
|
when {
|
||||||
@@ -271,7 +271,7 @@ private fun highlightXml(inText: String, inP: SyntaxPalette): AnnotatedString =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: YAML tokeniser — line-based
|
// MARK: YAML tokeniser - line-based
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Tokeniser for YAML. Walks line by line because YAML's grammar is
|
/** Tokeniser for YAML. Walks line by line because YAML's grammar is
|
||||||
@@ -320,7 +320,7 @@ private fun highlightYaml(inText: String, inP: SyntaxPalette): AnnotatedString =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Render the value portion of a YAML line — applies string / number /
|
/** Render the value portion of a YAML line - applies string / number /
|
||||||
keyword / list-marker colours where applicable. */
|
keyword / list-marker colours where applicable. */
|
||||||
private fun AnnotatedString.Builder.appendYamlValue(inSeg: String, inP: SyntaxPalette) {
|
private fun AnnotatedString.Builder.appendYamlValue(inSeg: String, inP: SyntaxPalette) {
|
||||||
val vTrimmed = inSeg.trimStart()
|
val vTrimmed = inSeg.trimStart()
|
||||||
@@ -378,7 +378,7 @@ private fun findUnquotedHash(inLine: String): Int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Locate the colon that separates a YAML key from its value at the
|
/** Locate the colon that separates a YAML key from its value at the
|
||||||
current indent level — the FIRST unquoted ':' that's followed by
|
current indent level - the FIRST unquoted ':' that's followed by
|
||||||
either whitespace or end-of-line. Returns -1 if the line has no key. */
|
either whitespace or end-of-line. Returns -1 if the line has no key. */
|
||||||
private fun indexOfKeyColon(inLine: String): Int {
|
private fun indexOfKeyColon(inLine: String): Int {
|
||||||
var vInSingle = false
|
var vInSingle = false
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Theme — the app's legacy colour slots, derived from the active palette
|
// MARK: Theme - the app's legacy colour slots, derived from the active palette
|
||||||
// ==================
|
// ==================
|
||||||
// AppColors is the app's own colour vocabulary (used by the custom widgets). It
|
// AppColors is the app's own colour vocabulary (used by the custom widgets). It
|
||||||
// is now DERIVED from the selected Voltic M3 ColorScheme via appColorsFromScheme
|
// is now DERIVED from the selected Voltic M3 ColorScheme via appColorsFromScheme
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ internal fun CertCard(inIndex: Int, inCert: ChainCert) {
|
|||||||
vSubject?.let { CertLine("Subject", it) }
|
vSubject?.let { CertLine("Subject", it) }
|
||||||
if (vSelfSigned) CertLine("Issuer", "Self-signed", kSelfSignedColor)
|
if (vSelfSigned) CertLine("Issuer", "Self-signed", kSelfSignedColor)
|
||||||
else vIssuer?.let { CertLine("Issuer", it) }
|
else vIssuer?.let { CertLine("Issuer", it) }
|
||||||
// Hosts this cert is valid for — the field that actually has to match the URL.
|
// Hosts this cert is valid for - the field that actually has to match the URL.
|
||||||
(certField(vFields, "X509v3 Subject Alternative Name")
|
(certField(vFields, "X509v3 Subject Alternative Name")
|
||||||
?: certField(vFields, "Subject Alternative Name")
|
?: certField(vFields, "Subject Alternative Name")
|
||||||
?: vParsed?.sans?.takeIf { it.isNotEmpty() }?.joinToString(", "))
|
?: vParsed?.sans?.takeIf { it.isNotEmpty() }?.joinToString(", "))
|
||||||
@@ -141,7 +141,7 @@ internal fun CertCard(inIndex: Int, inCert: ChainCert) {
|
|||||||
|
|
||||||
internal val kSelfSignedColor = Color(0xFF3FB950L)
|
internal val kSelfSignedColor = Color(0xFF3FB950L)
|
||||||
|
|
||||||
/** Copy button — rounded, hover overlay, real click that copies inText and shows
|
/** Copy button - rounded, hover overlay, real click that copies inText and shows
|
||||||
a green check. Icon-only (per cert) flashes a small floating "Copied" bubble
|
a green check. Icon-only (per cert) flashes a small floating "Copied" bubble
|
||||||
for 2s; the labelled variant (Copy chain) flips its label to "Copied" instead.
|
for 2s; the labelled variant (Copy chain) flips its label to "Copied" instead.
|
||||||
Uses a non-catching Popup so it never dismisses the dialog or eats clicks. */
|
Uses a non-catching Popup so it never dismisses the dialog or eats clicks. */
|
||||||
@@ -218,7 +218,7 @@ format the platform backends emit:
|
|||||||
"CN=R3, O=Let's Encrypt, C=US" (Windows CertGetNameStringA)
|
"CN=R3, O=Let's Encrypt, C=US" (Windows CertGetNameStringA)
|
||||||
"/C=US/O=Let's Encrypt/CN=R3" (OpenSSL X509_NAME_oneline)
|
"/C=US/O=Let's Encrypt/CN=R3" (OpenSSL X509_NAME_oneline)
|
||||||
"CN = R3, O = Let's Encrypt, C = US" (OpenSSL X509_NAME_print_ex
|
"CN = R3, O = Let's Encrypt, C = US" (OpenSSL X509_NAME_print_ex
|
||||||
default, with spaces — what
|
default, with spaces - what
|
||||||
libcurl on macOS emits)
|
libcurl on macOS emits)
|
||||||
Null when there's no CN. */
|
Null when there's no CN. */
|
||||||
internal fun cnOf(inDn: String): String? {
|
internal fun cnOf(inDn: String): String? {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ package apidemo
|
|||||||
// MARK: TLS chain / client-certificate seam
|
// MARK: TLS chain / client-certificate seam
|
||||||
// ==================
|
// ==================
|
||||||
// The real implementations drive the bundled libcurl directly (CurlMtls.kt,
|
// The real implementations drive the bundled libcurl directly (CurlMtls.kt,
|
||||||
// nativeMain) — Ktor's engines expose no client-cert / CERTINFO API. The jvm
|
// 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
|
// parity app has no bundled libcurl, so its actuals report the feature as
|
||||||
// native-only instead.
|
// native-only instead.
|
||||||
|
|
||||||
/** One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
/** One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
||||||
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
||||||
presented it. Derived certs — an issuer resolved from the OS store, or a
|
presented it. Derived certs - an issuer resolved from the OS store, or a
|
||||||
name-only placeholder — have fromServer = false and are drawn dotted. */
|
name-only placeholder - have fromServer = false and are drawn dotted. */
|
||||||
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
|
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
|
/** The server's TLS certificate chain. error is set instead when it couldn't be
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ fun substituteVars(inText: String, inVars: List<KeyVal>): String {
|
|||||||
return kVarRegex.replace(inText) { vMatch -> vMap[vMatch.groupValues[1]] ?: vMatch.value }
|
return kVarRegex.replace(inText) { vMatch -> vMap[vMatch.groupValues[1]] ?: vMatch.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply variable substitution across every field that actually gets sent — URL,
|
/** Apply variable substitution across every field that actually gets sent - URL,
|
||||||
query params, headers and body — returning a fully-resolved request. The
|
query params, headers and body - returning a fully-resolved request. The
|
||||||
original (template) request is left untouched. */
|
original (template) request is left untouched. */
|
||||||
fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest {
|
fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest {
|
||||||
val vActive = inVars.any { it.enabled && it.key.isNotBlank() }
|
val vActive = inVars.any { it.enabled && it.key.isNotBlank() }
|
||||||
@@ -54,7 +54,7 @@ fun resolveVars(inReq: ApiRequest, inVars: List<KeyVal>): ApiRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The distinct {{names}} referenced anywhere in the request that have no enabled
|
/** The distinct {{names}} referenced anywhere in the request that have no enabled
|
||||||
variable to fill them — surfaced as a warning in the editor. */
|
variable to fill them - surfaced as a warning in the editor. */
|
||||||
fun unresolvedVars(inReq: ApiRequest, inVars: List<KeyVal>): List<String> {
|
fun unresolvedVars(inReq: ApiRequest, inVars: List<KeyVal>): List<String> {
|
||||||
val vDefined = inVars
|
val vDefined = inVars
|
||||||
.filter { it.enabled && it.key.isNotBlank() }
|
.filter { it.enabled && it.key.isNotBlank() }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ width so request names line up. */
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun MethodTag(inMethod: ReqMethod) {
|
internal fun MethodTag(inMethod: ReqMethod) {
|
||||||
val vCol = methodColor(inMethod)
|
val vCol = methodColor(inMethod)
|
||||||
// No background — just the coloured method name, fixed width so the request
|
// No background - just the coloured method name, fixed width so the request
|
||||||
// names still line up in the sidebar list.
|
// names still line up in the sidebar list.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.width(46.dp).padding(vertical = 2.dp),
|
modifier = Modifier.width(46.dp).padding(vertical = 2.dp),
|
||||||
@@ -53,7 +53,7 @@ internal fun RowDropBar() {
|
|||||||
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(c.accent, RoundedCornerShape(1.dp)))
|
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(c.accent, RoundedCornerShape(1.dp)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tab indices listed in inDots get a small accent dot after their label — used
|
/** Tab indices listed in inDots get a small accent dot after their label - used
|
||||||
to flag a tab that holds content (e.g. a non-empty request body). */
|
to flag a tab that holds content (e.g. a non-empty request body). */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TabBar(inTabs: List<String>, inSelected: Int, inDots: Set<Int> = emptySet(), inOnSelect: (Int) -> Unit) {
|
internal fun TabBar(inTabs: List<String>, inSelected: Int, inDots: Set<Int> = emptySet(), inOnSelect: (Int) -> Unit) {
|
||||||
@@ -90,7 +90,7 @@ internal fun TogglePill(inLabel: String, inSelected: Boolean, inOnClick: () -> U
|
|||||||
) { Text(inLabel, color = if (inSelected) c.onAccent else c.dim, fontSize = 12.sp) }
|
) { Text(inLabel, color = if (inSelected) c.onAccent else c.dim, fontSize = 12.sp) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Compact single-line (or fixed-height multi-line) input — a BasicTextField in
|
/** Compact single-line (or fixed-height multi-line) input - a BasicTextField in
|
||||||
a slim bordered box, much shorter than the 56 dp Material OutlinedTextField. */
|
a slim bordered box, much shorter than the 56 dp Material OutlinedTextField. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ThinField(
|
internal fun ThinField(
|
||||||
@@ -177,7 +177,7 @@ internal fun OutlinedAction(inIcon: Int, inLabel: String, inOnClick: () -> Unit)
|
|||||||
|
|
||||||
/** Filled red icon+label button for destructive / stop actions (delete, cancel, quit).
|
/** Filled red icon+label button for destructive / stop actions (delete, cancel, quit).
|
||||||
Uses the m3 Button so its size + ripple + interaction match every other
|
Uses the m3 Button so its size + ripple + interaction match every other
|
||||||
Button in the app — previously handrolled Box + background + clickable + padding
|
Button in the app - previously handrolled Box + background + clickable + padding
|
||||||
gave it a different height / horizontal padding, and the Discard button sat
|
gave it a different height / horizontal padding, and the Discard button sat
|
||||||
next to a Button ("Save first…") looking noticeably smaller in dialogs. */
|
next to a Button ("Save first…") looking noticeably smaller in dialogs. */
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import kotlinx.coroutines.launch
|
|||||||
// MARK: Response
|
// MARK: Response
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Panel 4 — Request / Response viewer. Each tab stacks a HEADERS section over a
|
/** Panel 4 - Request / Response viewer. Each tab stacks a HEADERS section over a
|
||||||
BODY section. "Request" shows the resolved request that would be sent (the
|
BODY section. "Request" shows the resolved request that would be sent (the
|
||||||
Preview); "Response" shows the result (image-aware). A copy / save toolbar
|
Preview); "Response" shows the result (image-aware). A copy / save toolbar
|
||||||
acts on whichever view is showing. */
|
acts on whichever view is showing. */
|
||||||
@@ -61,7 +61,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
val vPreview = inRs.preview
|
val vPreview = inRs.preview
|
||||||
val vShowRequest = vPreview || inRs.viewTab == 0
|
val vShowRequest = vPreview || inRs.viewTab == 0
|
||||||
|
|
||||||
// Pretty-print once per received body (not every recomposition) — the whole
|
// Pretty-print once per received body (not every recomposition) - the whole
|
||||||
// body is shown; the renderer line-culls + wrap-caches so even a 20k-line
|
// body is shown; the renderer line-culls + wrap-caches so even a 20k-line
|
||||||
// payload stays cheap. (Previously capped at 20000 chars, which silently cut
|
// payload stays cheap. (Previously capped at 20000 chars, which silently cut
|
||||||
// long responses off around line ~1100.)
|
// long responses off around line ~1100.)
|
||||||
@@ -89,7 +89,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Text("not sent", color = VolticTheme.extended.warning, fontSize = 11.sp)
|
Text("not sent", color = VolticTheme.extended.warning, fontSize = 11.sp)
|
||||||
} else {
|
} else {
|
||||||
// Request tab — adds the coloured method only once the
|
// Request tab - adds the coloured method only once the
|
||||||
// request has actually been sent (sentReq != null).
|
// request has actually been sent (sentReq != null).
|
||||||
val vSent = inRs.sentReq
|
val vSent = inRs.sentReq
|
||||||
ViewerTab(
|
ViewerTab(
|
||||||
@@ -99,7 +99,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
inSelected = inRs.viewTab == 0,
|
inSelected = inRs.viewTab == 0,
|
||||||
inOnClick = { inRs.viewTab = 0 },
|
inOnClick = { inRs.viewTab = 0 },
|
||||||
)
|
)
|
||||||
// Response tab — adds the coloured status only when a
|
// Response tab - adds the coloured status only when a
|
||||||
// response (or error) is in. No placeholder during the
|
// response (or error) is in. No placeholder during the
|
||||||
// pre-send / loading state.
|
// pre-send / loading state.
|
||||||
val (vRespAccent, vRespColor) = when {
|
val (vRespAccent, vRespColor) = when {
|
||||||
@@ -115,7 +115,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
inOnClick = { inRs.viewTab = 1 },
|
inOnClick = { inRs.viewTab = 1 },
|
||||||
)
|
)
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
// Spinner only — the Cancel control already lives next to
|
// Spinner only - the Cancel control already lives next to
|
||||||
// Send in the URL bar; no need for a duplicate here.
|
// Send in the URL bar; no need for a duplicate here.
|
||||||
if (inRs.viewTab == 1 && vLoading) {
|
if (inRs.viewTab == 1 && vLoading) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(14.dp), color = c.accent, strokeWidth = 2.dp)
|
CircularProgressIndicator(modifier = Modifier.size(14.dp), color = c.accent, strokeWidth = 2.dp)
|
||||||
@@ -134,7 +134,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
val vSentHeaders =
|
val vSentHeaders =
|
||||||
if (!vPreview) inRs.response?.requestHeaders?.takeIf { it.isNotEmpty() } else null
|
if (!vPreview) inRs.response?.requestHeaders?.takeIf { it.isNotEmpty() } else null
|
||||||
// Synthesize wire-level headers (Host, Content-Length, User-Agent)
|
// Synthesize wire-level headers (Host, Content-Length, User-Agent)
|
||||||
// the engine adds without telling Ktor — same set httpie shows.
|
// the engine adds without telling Ktor - same set httpie shows.
|
||||||
val vRawHeaders =
|
val vRawHeaders =
|
||||||
if (vSentHeaders != null) vSentHeaders else parseHeaderLines(requestHeadersText(vR))
|
if (vSentHeaders != null) vSentHeaders else parseHeaderLines(requestHeadersText(vR))
|
||||||
val vHeaders = synthesizeRequestHeaders(vR, vRawHeaders)
|
val vHeaders = synthesizeRequestHeaders(vR, vRawHeaders)
|
||||||
@@ -204,7 +204,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
inAutoLabel = if (inRs.respFormatOverride == null) vAuto.label else null,
|
inAutoLabel = if (inRs.respFormatOverride == null) vAuto.label else null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Wrap toggle — soft-wrap long lines (default) vs. no wrap +
|
// Wrap toggle - soft-wrap long lines (default) vs. no wrap +
|
||||||
// horizontal scroll (one source line per row). Shown for any text
|
// horizontal scroll (one source line per row). Shown for any text
|
||||||
// body (request preview or response), hidden for an image.
|
// body (request preview or response), hidden for an image.
|
||||||
val vHasTextBody = (vRespHasContent && !vRespImage) || vReqHasContent
|
val vHasTextBody = (vRespHasContent && !vRespImage) || vReqHasContent
|
||||||
@@ -257,7 +257,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: HttpFlowView — flat httpie-style request/response layout
|
// MARK: HttpFlowView - flat httpie-style request/response layout
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Renders the body of a Request or Response tab in a httpie-flavoured
|
/** Renders the body of a Request or Response tab in a httpie-flavoured
|
||||||
@@ -280,12 +280,12 @@ internal fun HttpFlowView(
|
|||||||
) {
|
) {
|
||||||
val c = LocalAppColors.current
|
val c = LocalAppColors.current
|
||||||
val vState = rememberLazyListState()
|
val vState = rememberLazyListState()
|
||||||
// Editor-style selection state — anchor + caret in absolute character offsets
|
// Editor-style selection state - anchor + caret in absolute character offsets
|
||||||
// into inBody. Resets when the body changes so a stale range never highlights
|
// into inBody. Resets when the body changes so a stale range never highlights
|
||||||
// a shorter/different response. Lives outside any SelectionContainer so it
|
// a shorter/different response. Lives outside any SelectionContainer so it
|
||||||
// survives scroll and spans off-screen chunks (see the BodySel comment).
|
// survives scroll and spans off-screen chunks (see the BodySel comment).
|
||||||
var vSel: BodySel? by remember(inBody) { mutableStateOf(null) }
|
var vSel: BodySel? by remember(inBody) { mutableStateOf(null) }
|
||||||
// Selection background from the app theme — same colour that
|
// Selection background from the app theme - same colour that
|
||||||
// LocalTextSelectionColors publishes to Compose text selection elsewhere.
|
// LocalTextSelectionColors publishes to Compose text selection elsewhere.
|
||||||
val vSelColor = LocalTextSelectionColors.current.backgroundColor
|
val vSelColor = LocalTextSelectionColors.current.backgroundColor
|
||||||
val vFocus = remember { FocusRequester() }
|
val vFocus = remember { FocusRequester() }
|
||||||
@@ -297,12 +297,12 @@ internal fun HttpFlowView(
|
|||||||
// auto-scroll effect that scrolls the LazyColumn while the pointer sits in
|
// auto-scroll effect that scrolls the LazyColumn while the pointer sits in
|
||||||
// the top / bottom edge zone.
|
// the top / bottom edge zone.
|
||||||
var vDragPos: Offset? by remember { mutableStateOf(null) }
|
var vDragPos: Offset? by remember { mutableStateOf(null) }
|
||||||
// The body is highlighted ONCE, then sliced into BLOCKS of lines — one BasicText
|
// The body is highlighted ONCE, then sliced into BLOCKS of lines - one BasicText
|
||||||
// per block, NOT one per line. Far fewer nodes / selectables / paragraph set-ups
|
// per block, NOT one per line. Far fewer nodes / selectables / paragraph set-ups
|
||||||
// churn through the viewport while scrolling (the smoothness win), and the
|
// churn through the viewport while scrolling (the smoothness win), and the
|
||||||
// O(total-spans) AnnotatedString.subSequence runs once per block at build time
|
// O(total-spans) AnnotatedString.subSequence runs once per block at build time
|
||||||
// (memoised) instead of per visible line every frame. LazyColumn still virtualizes,
|
// (memoised) instead of per visible line every frame. LazyColumn still virtualizes,
|
||||||
// so only the ~2-3 on-screen blocks are composed/measured — 12k lines stay cheap.
|
// so only the ~2-3 on-screen blocks are composed/measured - 12k lines stay cheap.
|
||||||
val vDark = isDarkBg(c.bg)
|
val vDark = isDarkBg(c.bg)
|
||||||
val vChunks = remember(inBody, inBodyFormat, vDark) {
|
val vChunks = remember(inBody, inBodyFormat, vDark) {
|
||||||
if (inBody == null) emptyList()
|
if (inBody == null) emptyList()
|
||||||
@@ -315,16 +315,16 @@ internal fun HttpFlowView(
|
|||||||
// Per-visible-chunk TextLayoutResult, keyed by chunk index. Populated as each
|
// Per-visible-chunk TextLayoutResult, keyed by chunk index. Populated as each
|
||||||
// BodyChunkRow's BasicText lays out, dropped implicitly when the chunks list
|
// BodyChunkRow's BasicText lays out, dropped implicitly when the chunks list
|
||||||
// changes (new response) via the remember key. Off-screen chunks aren't in
|
// changes (new response) via the remember key. Off-screen chunks aren't in
|
||||||
// this map — we can't hit-test them via mouse either since they're not painted.
|
// this map - we can't hit-test them via mouse either since they're not painted.
|
||||||
val vChunkLayouts = remember(vChunks) { mutableStateMapOf<Int, TextLayoutResult>() }
|
val vChunkLayouts = remember(vChunks) { mutableStateMapOf<Int, TextLayoutResult>() }
|
||||||
val vTotalLines = remember(inBody) { if (inBody == null) 0 else inBody.count { it == '\n' } + 1 }
|
val vTotalLines = remember(inBody) { if (inBody == null) 0 else inBody.count { it == '\n' } + 1 }
|
||||||
// No-wrap mode pans horizontally; every block shares this one scroll state so the
|
// No-wrap mode pans horizontally; every block shares this one scroll state so the
|
||||||
// whole body pans together (the gutter stays pinned — not scrolled).
|
// whole body pans together (the gutter stays pinned - not scrolled).
|
||||||
val vHScroll = rememberScrollState()
|
val vHScroll = rememberScrollState()
|
||||||
val vGutterWidth = (vTotalLines.coerceAtLeast(1).toString().length * 7 + 4).dp
|
val vGutterWidth = (vTotalLines.coerceAtLeast(1).toString().length * 7 + 4).dp
|
||||||
// Per-line styles hoisted once (not allocated per line per frame). The rows use
|
// Per-line styles hoisted once (not allocated per line per frame). The rows use
|
||||||
// BasicText with these fixed styles instead of material3 Text — no per-node
|
// BasicText with these fixed styles instead of material3 Text - no per-node
|
||||||
// LocalTextStyle merge / LocalContentColor read — which is the main scroll-
|
// LocalTextStyle merge / LocalContentColor read - which is the main scroll-
|
||||||
// smoothness win when thousands of line items churn through the viewport.
|
// smoothness win when thousands of line items churn through the viewport.
|
||||||
val vBodyStyle = remember(c.text) { TextStyle(color = c.text, fontSize = 12.sp, fontFamily = monoFontFamily) }
|
val vBodyStyle = remember(c.text) { TextStyle(color = c.text, fontSize = 12.sp, fontFamily = monoFontFamily) }
|
||||||
val vNumStyle = remember(c.dim) {
|
val vNumStyle = remember(c.dim) {
|
||||||
@@ -339,7 +339,7 @@ internal fun HttpFlowView(
|
|||||||
// Body-relative x offset (px) of the block's BasicText inside the outer Box.
|
// Body-relative x offset (px) of the block's BasicText inside the outer Box.
|
||||||
// Mirrors BodyChunkRow's layout: 4dp Row start padding + gutter width + 6dp spacer.
|
// Mirrors BodyChunkRow's layout: 4dp Row start padding + gutter width + 6dp spacer.
|
||||||
val vTextStartPx = with(density) { (4.dp + vGutterWidth + 6.dp).toPx() }
|
val vTextStartPx = with(density) { (4.dp + vGutterWidth + 6.dp).toPx() }
|
||||||
// LazyColumn item index of chunk index 0 — status row + optional header
|
// LazyColumn item index of chunk index 0 - status row + optional header
|
||||||
// table + divider. Used to scroll a specific chunk into view when the caret
|
// table + divider. Used to scroll a specific chunk into view when the caret
|
||||||
// moves off-screen via keyboard. Only stable while we're on the "have body"
|
// moves off-screen via keyboard. Only stable while we're on the "have body"
|
||||||
// branch, but that's the only branch that renders chunks in the first place.
|
// branch, but that's the only branch that renders chunks in the first place.
|
||||||
@@ -358,7 +358,7 @@ internal fun HttpFlowView(
|
|||||||
if (!vVisible) vScope.launch { vState.scrollToItem(vChunkItemOffset + vCi) }
|
if (!vVisible) vScope.launch { vState.scrollToItem(vChunkItemOffset + vCi) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit-test — turns a pointer position (outer Box local frame) into a global
|
// Hit-test - turns a pointer position (outer Box local frame) into a global
|
||||||
// character offset into inBody, or null if the pointer isn't over any chunk.
|
// character offset into inBody, or null if the pointer isn't over any chunk.
|
||||||
// Clamps past-viewport pointers to the nearest visible chunk boundary so a
|
// Clamps past-viewport pointers to the nearest visible chunk boundary so a
|
||||||
// drag that overshoots still keeps extending toward that end.
|
// drag that overshoots still keeps extending toward that end.
|
||||||
@@ -387,7 +387,7 @@ internal fun HttpFlowView(
|
|||||||
// Drag auto-scroll: while a drag is in progress (vDragPos != null), tick
|
// Drag auto-scroll: while a drag is in progress (vDragPos != null), tick
|
||||||
// every frame and scroll the LazyColumn when the pointer sits in a 40dp
|
// every frame and scroll the LazyColumn when the pointer sits in a 40dp
|
||||||
// edge zone at the top / bottom. Speed ramps linearly with depth into the
|
// edge zone at the top / bottom. Speed ramps linearly with depth into the
|
||||||
// zone up to 24dp per frame — roughly a page per second at 60fps at max
|
// zone up to 24dp per frame - roughly a page per second at 60fps at max
|
||||||
// depth. After each scroll we re-hit-test the current pointer position so
|
// depth. After each scroll we re-hit-test the current pointer position so
|
||||||
// the selection keeps extending onto the newly revealed chunks.
|
// the selection keeps extending onto the newly revealed chunks.
|
||||||
val vEdgePx = with(density) { 40.dp.toPx() }
|
val vEdgePx = with(density) { 40.dp.toPx() }
|
||||||
@@ -412,7 +412,7 @@ internal fun HttpFlowView(
|
|||||||
|
|
||||||
// contentAlignment pins the narrow scrollbar to the right edge; the
|
// contentAlignment pins the narrow scrollbar to the right edge; the
|
||||||
// fillMaxSize content fills the rest (this project's Box has no BoxScope, so
|
// fillMaxSize content fills the rest (this project's Box has no BoxScope, so
|
||||||
// there's no Modifier.align — alignment is via contentAlignment).
|
// there's no Modifier.align - alignment is via contentAlignment).
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -421,7 +421,7 @@ internal fun HttpFlowView(
|
|||||||
// Mouse: press starts a selection, drag extends it, Shift+press extends
|
// Mouse: press starts a selection, drag extends it, Shift+press extends
|
||||||
// from the existing anchor. We drive this by hand instead of going
|
// from the existing anchor. We drive this by hand instead of going
|
||||||
// through SelectionContainer because SC only sees composed selectables
|
// through SelectionContainer because SC only sees composed selectables
|
||||||
// (visible chunks) — Ctrl+A and Shift+Arrow can't reach off-screen text
|
// (visible chunks) - Ctrl+A and Shift+Arrow can't reach off-screen text
|
||||||
// through it. Header items above the chunks stay non-selectable, which
|
// through it. Header items above the chunks stay non-selectable, which
|
||||||
// matches the previous DisableSelection wrappers.
|
// matches the previous DisableSelection wrappers.
|
||||||
.pointerInput(vChunks) {
|
.pointerInput(vChunks) {
|
||||||
@@ -437,7 +437,7 @@ internal fun HttpFlowView(
|
|||||||
val vDown = awaitPointerEvent()
|
val vDown = awaitPointerEvent()
|
||||||
if (vDown.type != PointerEventType.Press) continue
|
if (vDown.type != PointerEventType.Press) continue
|
||||||
val vChange = vDown.changes.firstOrNull() ?: continue
|
val vChange = vDown.changes.firstOrNull() ?: continue
|
||||||
// Skip presses a child already handled — most importantly the
|
// Skip presses a child already handled - most importantly the
|
||||||
// VerticalScrollbar sibling in this Box. Without this, dragging
|
// VerticalScrollbar sibling in this Box. Without this, dragging
|
||||||
// the scrollbar would also start (and continuously extend) a
|
// the scrollbar would also start (and continuously extend) a
|
||||||
// body selection because my hit-test still finds a chunk under
|
// body selection because my hit-test still finds a chunk under
|
||||||
@@ -584,7 +584,7 @@ internal fun HttpFlowView(
|
|||||||
if (inShowSecureLock) {
|
if (inShowSecureLock) {
|
||||||
MaterialSymbolsOutlined(
|
MaterialSymbolsOutlined(
|
||||||
icon = MaterialSymbols.Lock,
|
icon = MaterialSymbols.Lock,
|
||||||
tint = Color(0xFF36B37E), // green — TLS verified by OS
|
tint = Color(0xFF36B37E), // green - TLS verified by OS
|
||||||
size = 14.dp,
|
size = 14.dp,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
@@ -592,7 +592,7 @@ internal fun HttpFlowView(
|
|||||||
Text(inStatusLine, color = c.text, fontSize = 13.sp)
|
Text(inStatusLine, color = c.text, fontSize = 13.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Headers as a key/value table — only when not collapsed.
|
// Headers as a key/value table - only when not collapsed.
|
||||||
if (!inHeadersCollapsed && inHeaders.isNotEmpty()) {
|
if (!inHeadersCollapsed && inHeaders.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
HeaderTable(inHeaders, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
|
HeaderTable(inHeaders, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
|
||||||
@@ -611,7 +611,7 @@ internal fun HttpFlowView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (vChunks.isNotEmpty()) {
|
} else if (vChunks.isNotEmpty()) {
|
||||||
// One BasicText per BLOCK of lines (not per line) — far fewer
|
// One BasicText per BLOCK of lines (not per line) - far fewer
|
||||||
// nodes to churn while scrolling. Chunk key lets the pointer
|
// nodes to churn while scrolling. Chunk key lets the pointer
|
||||||
// hit-test map a visible item back to its chunk index.
|
// hit-test map a visible item back to its chunk index.
|
||||||
items(vChunks.size, key = { "chunk_$it" }, contentType = { "chunk" }) { vCi ->
|
items(vChunks.size, key = { "chunk_$it" }, contentType = { "chunk" }) { vCi ->
|
||||||
@@ -658,7 +658,7 @@ where they've moved to. When they differ, the selected range is [ min..max ].
|
|||||||
When equal, it's a zero-width caret still useful as a starting point for
|
When equal, it's a zero-width caret still useful as a starting point for
|
||||||
subsequent Shift+Arrow / Shift+Click extensions.
|
subsequent Shift+Arrow / Shift+Click extensions.
|
||||||
|
|
||||||
Note this lives OUTSIDE the SelectionContainer machinery — that machinery only
|
Note this lives OUTSIDE the SelectionContainer machinery - that machinery only
|
||||||
tracks selectables composed by the LazyColumn (visible chunks), which is why
|
tracks selectables composed by the LazyColumn (visible chunks), which is why
|
||||||
Ctrl+A and drag-select cannot reach off-screen text through it. This state is
|
Ctrl+A and drag-select cannot reach off-screen text through it. This state is
|
||||||
plain integer offsets so it survives scroll and covers the whole body. */
|
plain integer offsets so it survives scroll and covers the whole body. */
|
||||||
@@ -676,7 +676,7 @@ private fun lineStartAt(inText: String, inOffset: Int): Int {
|
|||||||
return if (vPrev < 0) 0 else vPrev + 1
|
return if (vPrev < 0) 0 else vPrev + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Character offset of the end of the line containing [inOffset] — the position of
|
/** Character offset of the end of the line containing [inOffset] - the position of
|
||||||
its '\n', or the length of [inText] if this is the last line. */
|
its '\n', or the length of [inText] if this is the last line. */
|
||||||
private fun lineEndAt(inText: String, inOffset: Int): Int {
|
private fun lineEndAt(inText: String, inOffset: Int): Int {
|
||||||
val vClamped = inOffset.coerceIn(0, inText.length)
|
val vClamped = inOffset.coerceIn(0, inText.length)
|
||||||
@@ -684,7 +684,7 @@ private fun lineEndAt(inText: String, inOffset: Int): Int {
|
|||||||
return if (vNext < 0) inText.length else vNext
|
return if (vNext < 0) inText.length else vNext
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move a caret one visual "row" down while trying to keep its column — i.e. the
|
/** Move a caret one visual "row" down while trying to keep its column - i.e. the
|
||||||
number of chars past the current line's start. If the target line is shorter,
|
number of chars past the current line's start. If the target line is shorter,
|
||||||
the caret snaps to its end. Off-by-one at EOF collapses to the body length. */
|
the caret snaps to its end. Off-by-one at EOF collapses to the body length. */
|
||||||
private fun moveDown(inText: String, inOffset: Int): Int {
|
private fun moveDown(inText: String, inOffset: Int): Int {
|
||||||
@@ -728,7 +728,7 @@ private fun wordRangeAt(inText: String, inOffset: Int): IntRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The index of the chunk in [inChunks] whose character range contains
|
/** The index of the chunk in [inChunks] whose character range contains
|
||||||
[inOffset], or null when the body is empty. Uses a linear scan — the chunk
|
[inOffset], or null when the body is empty. Uses a linear scan - the chunk
|
||||||
count is O(body / kLinesPerChunk) which stays modest even for huge bodies. */
|
count is O(body / kLinesPerChunk) which stays modest even for huge bodies. */
|
||||||
private fun chunkContaining(inChunks: List<BodyChunk>, inOffset: Int): Int? {
|
private fun chunkContaining(inChunks: List<BodyChunk>, inOffset: Int): Int? {
|
||||||
if (inChunks.isEmpty()) return null
|
if (inChunks.isEmpty()) return null
|
||||||
@@ -791,7 +791,7 @@ private fun BodyChunkRow(
|
|||||||
// Precompute this block's slice of the selection in LOCAL offsets. `null`
|
// Precompute this block's slice of the selection in LOCAL offsets. `null`
|
||||||
// when the selection is empty or doesn't touch this block. Also computes the
|
// when the selection is empty or doesn't touch this block. Also computes the
|
||||||
// caret's local offset when the caret itself falls inside this block (drawn
|
// caret's local offset when the caret itself falls inside this block (drawn
|
||||||
// even when the range is empty — that's the read-only viewer's cursor).
|
// even when the range is empty - that's the read-only viewer's cursor).
|
||||||
val vChunkStart = inChunk.startCharOffset
|
val vChunkStart = inChunk.startCharOffset
|
||||||
val vChunkEnd = vChunkStart + inChunk.body.length
|
val vChunkEnd = vChunkStart + inChunk.body.length
|
||||||
val vLocalRange: IntRange? = if (inSel == null || inSel.isEmpty) null else {
|
val vLocalRange: IntRange? = if (inSel == null || inSel.isEmpty) null else {
|
||||||
@@ -886,7 +886,7 @@ private class BodyChunk(
|
|||||||
|
|
||||||
/** Slice a highlighted body into blocks of [inLinesPerChunk] lines. The O(total-spans)
|
/** Slice a highlighted body into blocks of [inLinesPerChunk] lines. The O(total-spans)
|
||||||
subSequence runs once per block HERE (build time, memoised) instead of once per
|
subSequence runs once per block HERE (build time, memoised) instead of once per
|
||||||
visible line every frame — the difference between smooth and janky on a big body. */
|
visible line every frame - the difference between smooth and janky on a big body. */
|
||||||
private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerChunk: Int): List<BodyChunk> {
|
private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerChunk: Int): List<BodyChunk> {
|
||||||
val vStarts = lineStartOffsets(inText)
|
val vStarts = lineStartOffsets(inText)
|
||||||
val vN = vStarts.size
|
val vN = vStarts.size
|
||||||
@@ -908,7 +908,7 @@ private fun buildBodyChunks(inText: String, inAnn: AnnotatedString, inLinesPerCh
|
|||||||
|
|
||||||
/** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1),
|
/** Offsets where each source line starts (line i spans [starts[ i], starts[i+1]-1),
|
||||||
the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n)
|
the -1 dropping the '\n'; the last line runs to the string end). One cheap O(n)
|
||||||
scan, memoised — replaces the old gutter pre-pass that measured wrapping on
|
scan, memoised - replaces the old gutter pre-pass that measured wrapping on
|
||||||
every one of N lines on each width change (the other half of the freeze). */
|
every one of N lines on each width change (the other half of the freeze). */
|
||||||
private fun lineStartOffsets(inText: String): IntArray {
|
private fun lineStartOffsets(inText: String): IntArray {
|
||||||
val vStarts = ArrayList<Int>(64)
|
val vStarts = ArrayList<Int>(64)
|
||||||
@@ -943,7 +943,7 @@ internal fun HeaderTable(inHeaders: List<Pair<String, String>>, modifier: Modifi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Body view with line numbers in the gutter — mimics the code panel
|
/** Body view with line numbers in the gutter - mimics the code panel
|
||||||
in httpie. Numbers are dim; body text uses the regular colour. */
|
in httpie. Numbers are dim; body text uses the regular colour. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BodyView(
|
internal fun BodyView(
|
||||||
@@ -963,7 +963,7 @@ internal fun BodyView(
|
|||||||
val vGutterWidth = (vDigits * 7 + 4).dp
|
val vGutterWidth = (vDigits * 7 + 4).dp
|
||||||
// Body wrap width (physical px, from onSizeChanged), reported by the body
|
// Body wrap width (physical px, from onSizeChanged), reported by the body
|
||||||
// Box once it's laid out (0 on the first frame). The gutter numbering
|
// Box once it's laid out (0 on the first frame). The gutter numbering
|
||||||
// depends on it — see vNumbers. Wrap measurement expects the font
|
// depends on it - see vNumbers. Wrap measurement expects the font
|
||||||
// size + max-width in the SAME unit (both physical px on Retina), so we
|
// size + max-width in the SAME unit (both physical px on Retina), so we
|
||||||
// scale 12.sp through the current density here, matching what the paragraph
|
// scale 12.sp through the current density here, matching what the paragraph
|
||||||
// engine does when it builds the body's real paragraph.
|
// engine does when it builds the body's real paragraph.
|
||||||
@@ -972,16 +972,16 @@ internal fun BodyView(
|
|||||||
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
|
// Horizontal scroll for no-wrap mode (read-only body only); dormant when wrapping.
|
||||||
val vHScroll = rememberScrollState()
|
val vHScroll = rememberScrollState()
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
// Numbers are reference-only — half-alpha so they stay legible without
|
// Numbers are reference-only - half-alpha so they stay legible without
|
||||||
// competing with the body. Rendered as ONE '\n'-joined multi-line Text
|
// competing with the body. Rendered as ONE '\n'-joined multi-line Text
|
||||||
// (not one Text per line): a 1000-line gutter is then a single node the
|
// (not one Text per line): a 1000-line gutter is then a single node the
|
||||||
// renderer line-culls + wrap-caches exactly like the body, instead of
|
// renderer line-culls + wrap-caches exactly like the body, instead of
|
||||||
// 1000 leaf nodes — which lagged and tripped SDL's ~16384px draw-
|
// 1000 leaf nodes - which lagged and tripped SDL's ~16384px draw-
|
||||||
// coordinate limit (line numbers vanished past ~955).
|
// coordinate limit (line numbers vanished past ~955).
|
||||||
//
|
//
|
||||||
// When the body soft-wraps, a source line can span several visual rows.
|
// When the body soft-wraps, a source line can span several visual rows.
|
||||||
// To keep the numbers aligned we emit each line's number once (on its
|
// To keep the numbers aligned we emit each line's number once (on its
|
||||||
// first row) followed by one blank row per wrapped continuation — by
|
// first row) followed by one blank row per wrapped continuation - by
|
||||||
// re-running the SAME wrap the body uses (same measurer, font, width),
|
// re-running the SAME wrap the body uses (same measurer, font, width),
|
||||||
// so the gutter ends up with exactly as many rows as the body. Without
|
// so the gutter ends up with exactly as many rows as the body. Without
|
||||||
// this, a line wrapping into 3 rows would read 10/11/12 instead of
|
// this, a line wrapping into 3 rows would read 10/11/12 instead of
|
||||||
@@ -997,7 +997,7 @@ internal fun BodyView(
|
|||||||
append(vIdx + 1)
|
append(vIdx + 1)
|
||||||
if (vWrapRows) {
|
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.
|
||||||
@@ -1033,7 +1033,7 @@ internal fun BodyView(
|
|||||||
}
|
}
|
||||||
if (inOnChange != null) {
|
if (inOnChange != null) {
|
||||||
// EDITABLE request body: BasicTextField with a colour-only
|
// EDITABLE request body: BasicTextField with a colour-only
|
||||||
// syntax-highlight visualTransform — cursor / selection map to
|
// syntax-highlight visualTransform - cursor / selection map to
|
||||||
// the plain text. RAW = no highlight. Remembered per (format,
|
// the plain text. RAW = no highlight. Remembered per (format,
|
||||||
// theme) so typing and cursor-blink reuse it.
|
// theme) so typing and cursor-blink reuse it.
|
||||||
val vDark = isDarkBg(c.bg)
|
val vDark = isDarkBg(c.bg)
|
||||||
@@ -1079,7 +1079,7 @@ internal fun BodyView(
|
|||||||
}
|
}
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
// No-wrap mode: one source line per row, long lines overflow
|
// No-wrap mode: one source line per row, long lines overflow
|
||||||
// and the body pans horizontally (the gutter stays pinned —
|
// and the body pans horizontally (the gutter stays pinned -
|
||||||
// it's outside this scroll). The scroll must sit on a PARENT
|
// it's outside this scroll). The scroll must sit on a PARENT
|
||||||
// of the Text: a node's own scroll offset shifts its children,
|
// of the Text: a node's own scroll offset shifts its children,
|
||||||
// not itself. Wrap mode: plain soft-wrapping Text.
|
// not itself. Wrap mode: plain soft-wrapping Text.
|
||||||
@@ -1138,12 +1138,12 @@ internal fun ViewerTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Helpers — header parsing / formatting
|
// MARK: Helpers - header parsing / formatting
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** "HTTP/1.1 200 OK" status line for a Response, with the protocol
|
/** "HTTP/1.1 200 OK" status line for a Response, with the protocol
|
||||||
token dimmed (it's structural) and the status code + reason in the
|
token dimmed (it's structural) and the status code + reason in the
|
||||||
status colour. Just "HTTP/1.1" pre-response — no placeholder. Triple-
|
status colour. Just "HTTP/1.1" pre-response - no placeholder. Triple-
|
||||||
spaced to match the request-line formatting. */
|
spaced to match the request-line formatting. */
|
||||||
internal fun formatStatusLine(inResp: ApiResponse?, inColors: AppColors): AnnotatedString {
|
internal fun formatStatusLine(inResp: ApiResponse?, inColors: AppColors): AnnotatedString {
|
||||||
if (inResp == null) return buildAnnotatedString {
|
if (inResp == null) return buildAnnotatedString {
|
||||||
@@ -1197,7 +1197,7 @@ internal fun formatRequestLine(inReq: ApiRequest, inHttpVersion: String, inColor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Pull the path (+ query) out of a URL, dropping scheme + host. The
|
/** Pull the path (+ query) out of a URL, dropping scheme + host. The
|
||||||
request line in HTTP is "METHOD path HTTP/x.y" — the host lives on a
|
request line in HTTP is "METHOD path HTTP/x.y" - the host lives on a
|
||||||
separate Host: header line, not in the path. Falls back to the input
|
separate Host: header line, not in the path. Falls back to the input
|
||||||
string when no "://" is present (relative URL). */
|
string when no "://" is present (relative URL). */
|
||||||
internal fun urlPathOnly(inUrl: String): String {
|
internal fun urlPathOnly(inUrl: String): String {
|
||||||
@@ -1208,7 +1208,7 @@ internal fun urlPathOnly(inUrl: String): String {
|
|||||||
return if (vSlash < 0) "/" else vAfterScheme.substring(vSlash)
|
return if (vSlash < 0) "/" else vAfterScheme.substring(vSlash)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extract the host (+ port) from a URL — used to synthesize a Host
|
/** Extract the host (+ port) from a URL - used to synthesize a Host
|
||||||
header for display when the engine didn't surface one. */
|
header for display when the engine didn't surface one. */
|
||||||
internal fun urlHost(inUrl: String): String? {
|
internal fun urlHost(inUrl: String): String? {
|
||||||
val vIdx = inUrl.indexOf("://")
|
val vIdx = inUrl.indexOf("://")
|
||||||
@@ -1219,7 +1219,7 @@ internal fun urlHost(inUrl: String): String? {
|
|||||||
return vAuthority.ifEmpty { null }
|
return vAuthority.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The user-agent string our Darwin engine sends is opaque to Ktor —
|
/** The user-agent string our Darwin engine sends is opaque to Ktor -
|
||||||
NSURLSession picks the default. Match what httpie does: identify
|
NSURLSession picks the default. Match what httpie does: identify
|
||||||
ourselves so the wire log isn't missing the field entirely. */
|
ourselves so the wire log isn't missing the field entirely. */
|
||||||
internal const val kUserAgent: String = "compose-apidemo/1.0"
|
internal const val kUserAgent: String = "compose-apidemo/1.0"
|
||||||
@@ -1248,7 +1248,7 @@ internal fun synthesizeRequestHeaders(
|
|||||||
|
|
||||||
/** Body length in bytes for the headers synthesis. JSON / TEXT use the
|
/** Body length in bytes for the headers synthesis. JSON / TEXT use the
|
||||||
raw UTF-8 byte count; FORM serialises and counts; FILE skips
|
raw UTF-8 byte count; FORM serialises and counts; FILE skips
|
||||||
(loading the file just for the count would be wasteful — the engine
|
(loading the file just for the count would be wasteful - the engine
|
||||||
sets the field anyway). */
|
sets the field anyway). */
|
||||||
internal fun computedBodyLength(inReq: ApiRequest): Int? = when (inReq.bodyType) {
|
internal fun computedBodyLength(inReq: ApiRequest): Int? = when (inReq.bodyType) {
|
||||||
BodyType.TEXT -> inReq.body.encodeToByteArray().size
|
BodyType.TEXT -> inReq.body.encodeToByteArray().size
|
||||||
@@ -1267,12 +1267,12 @@ internal fun formatTimingSize(inResp: ApiResponse): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: ViewerOverflowMenu — 3-dot menu in the bottom-right of the viewer
|
// MARK: ViewerOverflowMenu - 3-dot menu in the bottom-right of the viewer
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Replaces the inline Copy / Save chips with a single MoreHoriz menu.
|
/** Replaces the inline Copy / Save chips with a single MoreHoriz menu.
|
||||||
Copy actions target whichever tab is showing (request or response);
|
Copy actions target whichever tab is showing (request or response);
|
||||||
Clear is global — wipes the response, sentReq, preview state, and any
|
Clear is global - wipes the response, sentReq, preview state, and any
|
||||||
memory-backed image resource for the current request. */
|
memory-backed image resource for the current request. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ViewerOverflowMenu(
|
internal fun ViewerOverflowMenu(
|
||||||
@@ -1370,7 +1370,7 @@ internal fun ViewerOverflowMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Perceptual luminance check — true when the colour reads as "dark"
|
/** Perceptual luminance check - true when the colour reads as "dark"
|
||||||
(background gets a light foreground). Standard Rec. 709 weights. */
|
(background gets a light foreground). Standard Rec. 709 weights. */
|
||||||
internal fun isDarkBg(inColor: Color): Boolean {
|
internal fun isDarkBg(inColor: Color): Boolean {
|
||||||
val vY = 0.299f * inColor.red + 0.587f * inColor.green + 0.114f * inColor.blue
|
val vY = 0.299f * inColor.red + 0.587f * inColor.green + 0.114f * inColor.blue
|
||||||
@@ -1378,7 +1378,7 @@ internal fun isDarkBg(inColor: Color): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: BodyFormatSelector — small dropdown for RAW / JSON / XML / YAML / HTML
|
// MARK: BodyFormatSelector - small dropdown for RAW / JSON / XML / YAML / HTML
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Format / "type" picker. inBordered renders it as a full dropdown matching
|
/** Format / "type" picker. inBordered renders it as a full dropdown matching
|
||||||
@@ -1423,8 +1423,8 @@ internal fun BodyFormatSelector(
|
|||||||
|
|
||||||
/** TLS validation indicator: only true when the URL is https AND we got
|
/** TLS validation indicator: only true when the URL is https AND we got
|
||||||
a real response back (i.e. the OS engine completed the TLS handshake
|
a real response back (i.e. the OS engine completed the TLS handshake
|
||||||
without an error). The engines we ship — NSURLSession on macOS,
|
without an error). The engines we ship - NSURLSession on macOS,
|
||||||
WinHttp on Windows, libcurl on Linux — all reject an untrusted
|
WinHttp on Windows, libcurl on Linux - all reject an untrusted
|
||||||
certificate by default, so a non-error response is implicit proof
|
certificate by default, so a non-error response is implicit proof
|
||||||
that the OS validated the chain. */
|
that the OS validated the chain. */
|
||||||
internal fun isTlsValidated(inUrl: String, inResp: ApiResponse?): Boolean {
|
internal fun isTlsValidated(inUrl: String, inResp: ApiResponse?): Boolean {
|
||||||
@@ -1469,7 +1469,7 @@ internal fun ViewerEmpty(inIcon: Int, inText: String, inModifier: Modifier = Mod
|
|||||||
internal fun headersText(inHeaders: List<Pair<String, String>>): String =
|
internal fun headersText(inHeaders: List<Pair<String, String>>): String =
|
||||||
inHeaders.joinToString("\n") { (vK, vV) -> "$vK: $vV" }.ifEmpty { "(no headers)" }
|
inHeaders.joinToString("\n") { (vK, vV) -> "$vK: $vV" }.ifEmpty { "(no headers)" }
|
||||||
|
|
||||||
/** The headers that *would* be sent — explicit enabled headers plus the inferred
|
/** The headers that *would* be sent - explicit enabled headers plus the inferred
|
||||||
Content-Type for the body type (unless one is already set). Used for Preview;
|
Content-Type for the body type (unless one is already set). Used for Preview;
|
||||||
a sent request shows the real headers via headersText(response.requestHeaders). */
|
a sent request shows the real headers via headersText(response.requestHeaders). */
|
||||||
internal fun requestHeadersText(inReq: ApiRequest): String {
|
internal fun requestHeadersText(inReq: ApiRequest): String {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Voltic themes — selectable Material 3 palettes
|
// MARK: Voltic themes - selectable Material 3 palettes
|
||||||
// ==================
|
// ==================
|
||||||
// The three "voltic" brand palettes (Kotlin purple, orange, and a stock-M3
|
// The three "voltic" brand palettes (Kotlin purple, orange, and a stock-M3
|
||||||
// purple), each a full light + dark M3 ColorScheme plus an EXTENDED group that
|
// purple), each a full light + dark M3 ColorScheme plus an EXTENDED group that
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import okio.FileSystem
|
|||||||
// MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop)
|
// 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
|
// 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.
|
// directly - every SDL-backed API the app uses goes through these expects.
|
||||||
// Signatures mirror the native originals so the native actuals are pure
|
// Signatures mirror the native originals so the native actuals are pure
|
||||||
// delegation (same pattern as :demo's demo.shim package).
|
// delegation (same pattern as :demo's demo.shim package).
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ expect fun appDataDir(inOrg: String, inApp: String): String?
|
|||||||
/** Open the OS file manager showing the given path. Fire-and-forget. */
|
/** Open the OS file manager showing the given path. Fire-and-forget. */
|
||||||
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
|
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
|
||||||
|
|
||||||
/** "Explorer" / "Finder" / "Files" — for menu labels. */
|
/** "Explorer" / "Finder" / "Files" - for menu labels. */
|
||||||
expect fun fileManagerName(): String
|
expect fun fileManagerName(): String
|
||||||
|
|
||||||
/** Async save-file dialog; the callback gets the chosen absolute path or null. */
|
/** Async save-file dialog; the callback gets the chosen absolute path or null. */
|
||||||
@@ -54,7 +54,7 @@ inFontPx (already density-scaled). Used by the body gutter so its line
|
|||||||
numbers stay aligned with the wrapped body text. */
|
numbers stay aligned with the wrapped body text. */
|
||||||
expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int
|
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
|
/** Editor tab width in spaces - how wide a typed '\t' renders. On the native
|
||||||
stack this drives the project text pipeline (TextLayoutConfig); the jvm
|
stack this drives the project text pipeline (TextLayoutConfig); the jvm
|
||||||
parity app only stores the preference. */
|
parity app only stores the preference. */
|
||||||
expect var editorTabWidth: Int
|
expect var editorTabWidth: Int
|
||||||
@@ -63,10 +63,10 @@ expect var editorTabWidth: Int
|
|||||||
// Infrastructure
|
// Infrastructure
|
||||||
|
|
||||||
/** okio's FileSystem.SYSTEM is declared per-platform, not in its common
|
/** okio's FileSystem.SYSTEM is declared per-platform, not in its common
|
||||||
metadata — surface it through a seam. */
|
metadata - surface it through a seam. */
|
||||||
internal expect val systemFileSystem: FileSystem
|
internal expect val systemFileSystem: FileSystem
|
||||||
|
|
||||||
/** The app's Ktor client: Curl on native (bundled libcurl — same TLS stack as
|
/** The app's Ktor client: Curl on native (bundled libcurl - same TLS stack as
|
||||||
the mTLS path), CIO on the jvm parity target. */
|
the mTLS path), CIO on the jvm parity target. */
|
||||||
expect fun createApiHttpClient(): HttpClient
|
expect fun createApiHttpClient(): HttpClient
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME
|
// 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
|
// implementation runs on the jvm parity target - the port modules are
|
||||||
// native-only, so apidemo's commonMain cannot reference them directly.
|
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||||
// Keep in sync with the original when it changes.
|
// Keep in sync with the original when it changes.
|
||||||
package apidemo.compat
|
package apidemo.compat
|
||||||
@@ -41,7 +41,7 @@ import kotlin.math.roundToInt
|
|||||||
/** A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
|
/** A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
|
||||||
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
|
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
|
||||||
it a ScrollbarAdapter built from the same ScrollState. This project's Box has
|
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
|
no BoxScope (so no Modifier.align) - pin the bar with the Box's
|
||||||
contentAlignment; the fillMaxSize content fills the rest:
|
contentAlignment; the fillMaxSize content fills the rest:
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||||
@@ -100,7 +100,7 @@ interface ScrollbarAdapter {
|
|||||||
|
|
||||||
/** Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the
|
/** 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
|
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
|
unknowable - we estimate it from the average visible item size × total item
|
||||||
count (the same approach as Compose Desktop). Accurate enough for a thumb
|
count (the same approach as Compose Desktop). Accurate enough for a thumb
|
||||||
whose length/position track the scroll position; the estimate self-corrects
|
whose length/position track the scroll position; the estimate self-corrects
|
||||||
as items of different sizes scroll through the viewport. */
|
as items of different sizes scroll through the viewport. */
|
||||||
@@ -131,7 +131,7 @@ private class LazyListScrollbarAdapter(private val state: LazyListState) : Scrol
|
|||||||
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
|
// 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
|
// list's pixel geometry is only ESTIMATED (average visible item size), and
|
||||||
// that estimate drifts as differently-sized items scroll through the
|
// that estimate drifts as differently-sized items scroll through the
|
||||||
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
|
// viewport - so index-snapping every frame makes the thumb jump. scrollBy
|
||||||
// sidesteps the estimate entirely. This is what Compose Desktop's own
|
// sidesteps the estimate entirely. This is what Compose Desktop's own
|
||||||
// LazyListScrollbarAdapter does, and for the same reason.
|
// LazyListScrollbarAdapter does, and for the same reason.
|
||||||
if (abs(vDistance) <= viewportSize) {
|
if (abs(vDistance) <= viewportSize) {
|
||||||
@@ -184,7 +184,7 @@ private fun Scrollbar(
|
|||||||
val vHovered by vHoverSource.collectIsHoveredAsState()
|
val vHovered by vHoverSource.collectIsHoveredAsState()
|
||||||
var vDraggingThumb by remember { mutableStateOf(false) }
|
var vDraggingThumb by remember { mutableStateOf(false) }
|
||||||
// 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
|
||||||
// density.toPx() on Retina, which visibly bloated the thumb (filled the
|
// density.toPx() on Retina, which visibly bloated the thumb (filled the
|
||||||
// whole track) and made drag/track-click positions land 2× too far down.
|
// whole track) and made drag/track-click positions land 2× too far down.
|
||||||
@@ -230,13 +230,13 @@ private fun Scrollbar(
|
|||||||
Box(modifier = vThumbMod)
|
Box(modifier = vThumbMod)
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// 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. The press is CONSUMED:
|
// coordinates have a stable (non-moving) origin. The press is CONSUMED:
|
||||||
// a scrollbar owns the pointer input it handles, so content sharing this
|
// a scrollbar owns the pointer input it handles, so content sharing this
|
||||||
// region (the scrollable beneath, or a parent that also reads pointer
|
// region (the scrollable beneath, or a parent that also reads pointer
|
||||||
// input) never reacts to the same press. Geometry is read LIVE from the
|
// input) never reacts to the same press. Geometry is read LIVE from the
|
||||||
// adapter at press time — not from the captured render metrics above,
|
// adapter at press time - not from the captured render metrics above,
|
||||||
// which go stale between pointerInput relaunches — so a grab after a
|
// which go stale between pointerInput relaunches - so a grab after a
|
||||||
// wheel-scroll still hit-tests against the thumb's current position.
|
// wheel-scroll still hit-tests against the thumb's current position.
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -246,7 +246,7 @@ private fun Scrollbar(
|
|||||||
val vDown = awaitFirstDown(requireUnconsumed = false)
|
val vDown = awaitFirstDown(requireUnconsumed = false)
|
||||||
vDown.consume()
|
vDown.consume()
|
||||||
|
|
||||||
// Live geometry — stable for the drag's duration (content /
|
// Live geometry - stable for the drag's duration (content /
|
||||||
// viewport / track sizes don't change mid-drag).
|
// viewport / track sizes don't change mid-drag).
|
||||||
val vContentPx = adapter.contentSize
|
val vContentPx = adapter.contentSize
|
||||||
val vViewportPx = adapter.viewportSize
|
val vViewportPx = adapter.viewportSize
|
||||||
@@ -262,7 +262,7 @@ private fun Scrollbar(
|
|||||||
|
|
||||||
// Map a track-relative thumb-top to a scroll offset. The
|
// Map a track-relative thumb-top to a scroll offset. The
|
||||||
// target uses the LIVE max scroll (read fresh each move),
|
// target uses the LIVE max scroll (read fresh each move),
|
||||||
// not the press-time value — the adapter compares it against
|
// not the press-time value - the adapter compares it against
|
||||||
// its live scrollOffset, and mixing a stale scale with a live
|
// its live scrollOffset, and mixing a stale scale with a live
|
||||||
// one makes the delta run away (grab at the bottom, drag up,
|
// one makes the delta run away (grab at the bottom, drag up,
|
||||||
// and the list overshoots to the middle).
|
// and the list overshoots to the middle).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME
|
// 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
|
// implementation runs on the jvm parity target - the port modules are
|
||||||
// native-only, so apidemo's commonMain cannot reference them directly.
|
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||||
// Keep in sync with the original when it changes.
|
// Keep in sync with the original when it changes.
|
||||||
package apidemo.compat
|
package apidemo.compat
|
||||||
@@ -66,7 +66,7 @@ fun HorizontalSplitPane(
|
|||||||
|
|
||||||
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
||||||
|
|
||||||
// Solid divider that fills its own slot — what you see is exactly what
|
// 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
|
// 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
|
// size change → no layout shift, and no flicker when the pointer briefly
|
||||||
// leaves the slot mid-drag).
|
// leaves the slot mid-drag).
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.platform.Font
|
import androidx.compose.ui.text.platform.Font
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Monospace body font — JVM actuals (classpath font/ staged by
|
// MARK: Monospace body font - JVM actuals (classpath font/ staged by
|
||||||
// jvmProcessResources)
|
// jvmProcessResources)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
actual val monoFontFamily: FontFamily? by lazy {
|
actual val monoFontFamily: FontFamily? by lazy {
|
||||||
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
|
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
|
||||||
if (vBytes == null) {
|
if (vBytes == null) {
|
||||||
println("apidemo: NotoSansMono.ttf not on the classpath — body uses the default font")
|
println("apidemo: NotoSansMono.ttf not on the classpath - body uses the default font")
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
FontFamily(Font(identity = kMonoFamily, data = vBytes))
|
FontFamily(Font(identity = kMonoFamily, data = vBytes))
|
||||||
@@ -21,3 +21,15 @@ actual val monoFontFamily: FontFamily? by lazy {
|
|||||||
actual val monoFontFamilyName: String? by lazy {
|
actual val monoFontFamilyName: String? by lazy {
|
||||||
if (monoFontFamily != null) kMonoFamily else null
|
if (monoFontFamily != null) kMonoFamily else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The JVM leg would otherwise inherit a SYSTEM font here; load the same bundled
|
||||||
|
NotoSans the native build uses so the two stacks are comparable. */
|
||||||
|
actual val defaultFontFamily: FontFamily? by lazy {
|
||||||
|
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSans.ttf")?.use { it.readBytes() }
|
||||||
|
if (vBytes == null) {
|
||||||
|
println("apidemo: NotoSans.ttf not on the classpath - using the platform default font")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
FontFamily(Font(identity = kDefaultFamily, data = vBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.ImageComposeScene
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
import androidx.compose.ui.platform.InfiniteAnimationPolicy
|
||||||
import androidx.compose.ui.res.useResource
|
import androidx.compose.ui.res.useResource
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Window
|
import androidx.compose.ui.window.Window
|
||||||
import androidx.compose.ui.window.application
|
import androidx.compose.ui.window.application
|
||||||
import androidx.compose.ui.window.rememberWindowState
|
import androidx.compose.ui.window.rememberWindowState
|
||||||
|
import apidemo.compat.jvmOnCloseRequest
|
||||||
|
import apidemo.compat.jvmOnKeyShortcut
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.jetbrains.compose.resources.decodeToImageBitmap
|
import org.jetbrains.compose.resources.decodeToImageBitmap
|
||||||
|
import org.jetbrains.skia.EncodedImageFormat
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
// The JVM comparison app's entry point. Runs the SAME shared App() as the
|
// The JVM comparison app's entry point. Runs the SAME shared App() as the
|
||||||
// native :apidemo, but on Compose Desktop (JVM) against upstream
|
// native :apidemo, but on Compose Desktop (JVM) against upstream
|
||||||
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only
|
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only
|
||||||
// (they drive the bundled libcurl); the jvm actuals report that instead.
|
// (they drive the bundled libcurl); the jvm actuals report that instead.
|
||||||
fun main() = application {
|
//
|
||||||
|
// --screenshot=<path.png> render headlessly to quiescence, write, exit
|
||||||
|
//
|
||||||
|
// The screenshot mode is the JVM half of the native-vs-JVM drift check; the
|
||||||
|
// native half is `apidemo.exe --screenshot=<path.bmp>`. Both render offscreen
|
||||||
|
// (ImageComposeScene here, a GPU readback there), so neither needs a visible
|
||||||
|
// window and the comparison is unaffected by whatever is on screen.
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val screenshot = args.firstOrNull { it.startsWith("--screenshot=") }?.substringAfter('=')
|
||||||
|
if (screenshot != null) {
|
||||||
|
screenshotApp(File(screenshot), kWidth, kHeight)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runInteractive()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val kWidth = 1240
|
||||||
|
private const val kHeight = 820
|
||||||
|
|
||||||
|
private fun runInteractive() = application {
|
||||||
// Voltic window icon (mirrors the native window icon). Staged onto the
|
// Voltic window icon (mirrors the native window icon). Staged onto the
|
||||||
// classpath at icon/ by jvmProcessResources.
|
// classpath at icon/ by jvmProcessResources.
|
||||||
val vIcon = remember {
|
val vIcon = remember {
|
||||||
@@ -23,10 +52,56 @@ fun main() = application {
|
|||||||
// The shared App installs a persist-then-close hook (InstallWindowHooks).
|
// The shared App installs a persist-then-close hook (InstallWindowHooks).
|
||||||
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
|
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
|
||||||
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
|
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
|
||||||
title = "API Manager — JVM (upstream Compose)",
|
title = "API Manager - JVM (upstream Compose)",
|
||||||
icon = vIcon,
|
icon = vIcon,
|
||||||
state = rememberWindowState(width = 1240.dp, height = 820.dp),
|
state = rememberWindowState(width = kWidth.dp, height = kHeight.dp),
|
||||||
) {
|
) {
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Headless screenshot (drift check vs the native leg)
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
private const val kFrameNanos = 16_666_667L
|
||||||
|
private const val kMaxFrames = 300
|
||||||
|
|
||||||
|
/** Infinite animations would never let the scene go quiet; upstream's test policy
|
||||||
|
freezes them at their initial value (same trick :demo's parity leg uses). */
|
||||||
|
private object CancelInfiniteAnimations : InfiniteAnimationPolicy {
|
||||||
|
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
|
||||||
|
throw CancellationException("infinite animations disabled for the drift screenshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render [App] offscreen at density 1 - matching the native leg, which lays out in
|
||||||
|
physical pixels - stepping a virtual 60fps clock until the scene stops invalidating. */
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
private fun screenshotApp(outFile: File, width: Int, height: Int) {
|
||||||
|
outFile.parentFile?.mkdirs()
|
||||||
|
val scene = ImageComposeScene(
|
||||||
|
width, height, density = Density(1f),
|
||||||
|
coroutineContext = Dispatchers.Unconfined + CancelInfiniteAnimations,
|
||||||
|
) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var nanos = 0L
|
||||||
|
var frames = 0
|
||||||
|
var image = scene.render(nanos)
|
||||||
|
while (scene.hasInvalidations() && frames < kMaxFrames) {
|
||||||
|
nanos += kFrameNanos
|
||||||
|
image = scene.render(nanos)
|
||||||
|
frames++
|
||||||
|
}
|
||||||
|
val png = image.encodeToData(EncodedImageFormat.PNG)
|
||||||
|
if (png == null) {
|
||||||
|
println("jvm screenshot: PNG encode failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outFile.writeBytes(png.bytes)
|
||||||
|
println("jvm screenshot: ${outFile.path} (settled after $frames frame(s))")
|
||||||
|
} finally {
|
||||||
|
scene.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: TLS chain / client-certificate — JVM actuals (native-only feature)
|
// MARK: TLS chain / client-certificate - JVM actuals (native-only feature)
|
||||||
// ==================
|
// ==================
|
||||||
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
|
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
|
||||||
// there is no libcurl on the jvm parity stack, so both entry points report
|
// there is no libcurl on the jvm parity stack, so both entry points report
|
||||||
// the limitation instead of half-implementing it over JSSE.
|
// the limitation instead of half-implementing it over JSSE.
|
||||||
|
|
||||||
private const val kNativeOnly =
|
private const val kNativeOnly =
|
||||||
"Client-certificate (mTLS) features need the native build — they drive the bundled libcurl directly."
|
"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 inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.Constraints
|
|||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import apidemo.monoFontFamily
|
||||||
import org.jetbrains.compose.resources.decodeToSvgPainter
|
import org.jetbrains.compose.resources.decodeToSvgPainter
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
@@ -29,7 +30,7 @@ import java.awt.datatransfer.StringSelection
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: JVM actuals — AWT + upstream Compose Desktop
|
// MARK: JVM actuals - AWT + upstream Compose Desktop
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
|
private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
|
||||||
@@ -41,7 +42,7 @@ actual fun createApiHttpClient(): io.ktor.client.HttpClient =
|
|||||||
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
|
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Window hooks — the shared App installs them; MainJvm wires them into the
|
// Window hooks - the shared App installs them; MainJvm wires them into the
|
||||||
// upstream Window() (onCloseRequest / onPreviewKeyEvent).
|
// upstream Window() (onCloseRequest / onPreviewKeyEvent).
|
||||||
|
|
||||||
internal var jvmOnCloseRequest: (() -> Boolean)? = null
|
internal var jvmOnCloseRequest: (() -> Boolean)? = null
|
||||||
@@ -154,11 +155,11 @@ actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inF
|
|||||||
).lineCount
|
).lineCount
|
||||||
}.getOrDefault(1)
|
}.getOrDefault(1)
|
||||||
|
|
||||||
/** The jvm parity app only stores the preference — upstream's text pipeline has
|
/** 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. */
|
no global tab-width knob. Backed by state so menu checkmarks recompose. */
|
||||||
private var fTabWidth by mutableStateOf(4)
|
private var mTabWidth by mutableStateOf(4)
|
||||||
actual var editorTabWidth: Int
|
actual var editorTabWidth: Int
|
||||||
get() = fTabWidth
|
get() = mTabWidth
|
||||||
set(value) {
|
set(value) {
|
||||||
fTabWidth = value
|
mTabWidth = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
// Linux: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
|
// Linux: Ktor bundles an OpenSSL-backed libcurl, which reads PEM / DER / PKCS#12
|
||||||
// certificate and key files directly — no certificate-store dance needed.
|
// certificate and key files directly - no certificate-store dance needed.
|
||||||
|
|
||||||
/** Point libcurl straight at the certificate / key files. */
|
/** Point libcurl straight at the certificate / key files. */
|
||||||
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
||||||
@@ -17,6 +17,6 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert =
|
|||||||
/** No temporary certificate store off Windows. */
|
/** No temporary certificate store off Windows. */
|
||||||
actual fun sweepTempClientCerts() {}
|
actual fun sweepTempClientCerts() {}
|
||||||
|
|
||||||
/** No OS-store issuer resolution here — just continue with the name-only issuer. */
|
/** No OS-store issuer resolution here - just continue with the name-only issuer. */
|
||||||
actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<ChainCert> =
|
actual fun extendChain(inServerCerts: List<List<Pair<String, String>>>): List<ChainCert> =
|
||||||
serverChainWithIssuerName(inServerCerts)
|
serverChainWithIssuerName(inServerCerts)
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import kotlin.io.encoding.Base64
|
|||||||
// Windows certificate store, referenced by SHA-1 thumbprint. So for a request
|
// Windows certificate store, referenced by SHA-1 thumbprint. So for a request
|
||||||
// that carries a certificate we import the cert + private key into the standard
|
// that carries a certificate we import the cert + private key into the standard
|
||||||
// CurrentUser\MY ("Personal") store, hand curl "CurrentUser\MY\<thumbprint>",
|
// CurrentUser\MY ("Personal") store, hand curl "CurrentUser\MY\<thumbprint>",
|
||||||
// and delete it again the instant the request finishes — nothing accumulates.
|
// and delete it again the instant the request finishes - nothing accumulates.
|
||||||
//
|
//
|
||||||
// Our entries are tagged (friendly name + key-container name) with a unique
|
// Our entries are tagged (friendly name + key-container name) with a unique
|
||||||
// prefix so the startup sweep can clear crash leftovers without ever touching
|
// prefix so the startup sweep can clear crash leftovers without ever touching
|
||||||
// the user's own certificates.
|
// the user's own certificates.
|
||||||
//
|
//
|
||||||
// Kotlin/Native maps several CryptoAPI LPCSTR params to String?, which makes the
|
// Kotlin/Native maps several CryptoAPI LPCSTR params to String?, which makes the
|
||||||
// CryptDecodeObjectEx route (numeric struct-type sentinels) unusable — so the
|
// CryptDecodeObjectEx route (numeric struct-type sentinels) unusable - so the
|
||||||
// RSA private key is parsed here (ASN.1) and laid out as a CryptoAPI
|
// RSA private key is parsed here (ASN.1) and laid out as a CryptoAPI
|
||||||
// PRIVATEKEYBLOB directly, then handed to CryptImportKey. RSA (PKCS#1 or PKCS#8)
|
// PRIVATEKEYBLOB directly, then handed to CryptImportKey. RSA (PKCS#1 or PKCS#8)
|
||||||
// and PKCS#12 are handled; EC PEM/DER is reported as unsupported with a hint.
|
// and PKCS#12 are handled; EC PEM/DER is reported as unsupported with a hint.
|
||||||
@@ -42,7 +42,7 @@ actual fun prepareClientCert(inReq: ApiRequest): PreparedCert {
|
|||||||
}
|
}
|
||||||
return PreparedCert(
|
return PreparedCert(
|
||||||
sslCert = "$kStorePath\\$vThumb",
|
sslCert = "$kStorePath\\$vThumb",
|
||||||
sslCertType = null, // store reference — not a file type
|
sslCertType = null, // store reference - not a file type
|
||||||
sslKey = null,
|
sslKey = null,
|
||||||
sslKeyType = null,
|
sslKeyType = null,
|
||||||
keyPassword = null,
|
keyPassword = null,
|
||||||
@@ -62,7 +62,7 @@ private fun importPemDerRsa(inCertBytes: ByteArray, inReq: ApiRequest): Pair<Str
|
|||||||
val vCertDer = derFromMaybePem(inCertBytes, "CERTIFICATE")
|
val vCertDer = derFromMaybePem(inCertBytes, "CERTIFICATE")
|
||||||
val vKeyBytes = if (inReq.keyPath.isNotBlank()) readFileBytes(inReq.keyPath) else inCertBytes
|
val vKeyBytes = if (inReq.keyPath.isNotBlank()) readFileBytes(inReq.keyPath) else inCertBytes
|
||||||
val vKeyDer = privateKeyDer(vKeyBytes)
|
val vKeyDer = privateKeyDer(vKeyBytes)
|
||||||
?: throw RuntimeException("No private key found — PEM/DER on Windows needs the RSA key (a key file, or a key block in the certificate file).")
|
?: throw RuntimeException("No private key found - PEM/DER on Windows needs the RSA key (a key file, or a key block in the certificate file).")
|
||||||
val vBlob = try {
|
val vBlob = try {
|
||||||
rsaKeyDerToBlob(vKeyDer)
|
rsaKeyDerToBlob(vKeyDer)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -374,7 +374,7 @@ private fun deleteContainer(inContainer: String) = memScoped {
|
|||||||
CryptAcquireContextW(vProv.ptr, inContainer, kProvName, PROV_RSA_FULL.toUInt(), CRYPT_DELETEKEYSET.toUInt())
|
CryptAcquireContextW(vProv.ptr, inContainer, kProvName, PROV_RSA_FULL.toUInt(), CRYPT_DELETEKEYSET.toUInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove every temp cert we ever added (prefix-tagged) plus its container — for
|
/** Remove every temp cert we ever added (prefix-tagged) plus its container - for
|
||||||
clearing crash leftovers at startup. The user's own certs are never matched. */
|
clearing crash leftovers at startup. The user's own certs are never matched. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
actual fun sweepTempClientCerts() {
|
actual fun sweepTempClientCerts() {
|
||||||
@@ -417,7 +417,7 @@ private fun friendlyNameOf(inCert: CPointer<CERT_CONTEXT>): String? = memScoped
|
|||||||
// Chain extension via the OS certificate store
|
// Chain extension via the OS certificate store
|
||||||
// ============
|
// ============
|
||||||
|
|
||||||
/** Continue the server's chain by asking the OS to build it from the leaf — this
|
/** Continue the server's chain by asking the OS to build it from the leaf - this
|
||||||
pulls intermediates/roots from the Windows store with full info. Falls back to
|
pulls intermediates/roots from the Windows store with full info. Falls back to
|
||||||
a name-only issuer if the OS can't (or the leaf can't be parsed). */
|
a name-only issuer if the OS can't (or the leaf can't be parsed). */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlin.time.TimeSource
|
|||||||
// MARK: Client-certificate (mTLS) sending via libcurl
|
// MARK: Client-certificate (mTLS) sending via libcurl
|
||||||
// ==================
|
// ==================
|
||||||
// Ktor's native engines expose no client-certificate API, so a request that
|
// Ktor's native engines expose no client-certificate API, so a request that
|
||||||
// carries a client certificate is sent here instead — straight through the
|
// carries a client certificate is sent here instead - straight through the
|
||||||
// libcurl that ktor-client-curl already bundles (package `libcurl`, embedded
|
// libcurl that ktor-client-curl already bundles (package `libcurl`, embedded
|
||||||
// static archive; Schannel on Windows, OpenSSL on macOS/Linux). This is the
|
// static archive; Schannel on Windows, OpenSSL on macOS/Linux). This is the
|
||||||
// same TLS stack the default engine uses, so behaviour matches; we just get to
|
// same TLS stack the default engine uses, so behaviour matches; we just get to
|
||||||
@@ -19,7 +19,7 @@ import kotlin.time.TimeSource
|
|||||||
//
|
//
|
||||||
// The transfer is synchronous (curl_easy_perform) and runs on Dispatchers.Default
|
// The transfer is synchronous (curl_easy_perform) and runs on Dispatchers.Default
|
||||||
// from HttpRunner.run(); cancellation is best-effort (an in-flight perform can't
|
// from HttpRunner.run(); cancellation is best-effort (an in-flight perform can't
|
||||||
// be interrupted — the result is simply discarded if the caller cancelled).
|
// be interrupted - the result is simply discarded if the caller cancelled).
|
||||||
|
|
||||||
/** A client certificate readied for libcurl: the CURLOPT_SSLCERT value plus the
|
/** A client certificate readied for libcurl: the CURLOPT_SSLCERT value plus the
|
||||||
other SSL options to set, and a cleanup to run once the request is done.
|
other SSL options to set, and a cleanup to run once the request is done.
|
||||||
@@ -40,7 +40,7 @@ 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
|
||||||
|
|
||||||
// ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
|
// ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
|
||||||
// curlSendWithClientCert expects live in commonMain (TlsCommon.kt) — the UI
|
// curlSendWithClientCert expects live in commonMain (TlsCommon.kt) - the UI
|
||||||
// consumes them from shared code; the per-OS ClientCert*.kt actuals below the
|
// consumes them from shared code; the per-OS ClientCert*.kt actuals below the
|
||||||
// native tree actualize sweepTempClientCerts directly.
|
// native tree actualize sweepTempClientCerts directly.
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ private class CurlSink {
|
|||||||
val headerRaw = StringBuilder() // raw response header lines, all responses incl. redirects
|
val headerRaw = StringBuilder() // raw response header lines, all responses incl. redirects
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Body write callback — appends each chunk to the sink's buffer. */
|
/** Body write callback - appends each chunk to the sink's buffer. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
private fun onCurlBody(
|
private fun onCurlBody(
|
||||||
inBuffer: CPointer<ByteVar>,
|
inBuffer: CPointer<ByteVar>,
|
||||||
@@ -89,7 +89,7 @@ private fun onCurlBody(
|
|||||||
return vLen.convert()
|
return vLen.convert()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Header callback — one parsed header line per call; accumulated for parsing. */
|
/** Header callback - one parsed header line per call; accumulated for parsing. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
private fun onCurlHeader(
|
private fun onCurlHeader(
|
||||||
inBuffer: CPointer<ByteVar>,
|
inBuffer: CPointer<ByteVar>,
|
||||||
@@ -114,7 +114,7 @@ private fun onCurlDiscard(
|
|||||||
): size_t =
|
): size_t =
|
||||||
(inSize * inCount)
|
(inSize * inCount)
|
||||||
|
|
||||||
/** Open a TLS connection to the request's URL (handshake only — no body) and
|
/** Open a TLS connection to the request's URL (handshake only - no body) and
|
||||||
return the server's certificate chain via CURLINFO_CERTINFO. Reuses the
|
return the server's certificate chain via CURLINFO_CERTINFO. Reuses the
|
||||||
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. */
|
||||||
@@ -294,7 +294,7 @@ actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse {
|
|||||||
headers = parseRawHeaders(vSink.headerRaw.toString()),
|
headers = parseRawHeaders(vSink.headerRaw.toString()),
|
||||||
body = when {
|
body = when {
|
||||||
vIsImage -> ""
|
vIsImage -> ""
|
||||||
vBinary -> "(${vCt ?: "binary"} · ${vBytes.size} bytes — not shown; use Save as…)"
|
vBinary -> "(${vCt ?: "binary"} · ${vBytes.size} bytes - not shown; use Save as…)"
|
||||||
else -> vBytes.decodeToString()
|
else -> vBytes.decodeToString()
|
||||||
},
|
},
|
||||||
bytes = vBytes,
|
bytes = vBytes,
|
||||||
@@ -314,7 +314,7 @@ actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse {
|
|||||||
private fun errorResponse(inMessage: String, inMs: Long): ApiResponse =
|
private fun errorResponse(inMessage: String, inMs: Long): ApiResponse =
|
||||||
ApiResponse(
|
ApiResponse(
|
||||||
status = 0,
|
status = 0,
|
||||||
statusText = "—",
|
statusText = "-",
|
||||||
timeMs = inMs,
|
timeMs = inMs,
|
||||||
sizeBytes = 0,
|
sizeBytes = 0,
|
||||||
headers = emptyList(),
|
headers = emptyList(),
|
||||||
@@ -341,8 +341,8 @@ private fun requestBodyBytes(inReq: ApiRequest): CurlBody? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Parse libcurl's accumulated raw header text into ordered key/value pairs,
|
/** Parse libcurl's accumulated raw header text into ordered key/value pairs,
|
||||||
keeping only the final response's block (a new "HTTP/" status line — emitted
|
keeping only the final response's block (a new "HTTP/" status line - emitted
|
||||||
per redirect hop — resets what we've gathered). */
|
per redirect hop - resets what we've gathered). */
|
||||||
private fun parseRawHeaders(inRaw: String): List<Pair<String, String>> {
|
private fun parseRawHeaders(inRaw: String): List<Pair<String, String>> {
|
||||||
var vCurrent = mutableListOf<Pair<String, String>>()
|
var vCurrent = mutableListOf<Pair<String, String>>()
|
||||||
for (vRawLine in inRaw.split("\r\n", "\n")) {
|
for (vRawLine in inRaw.split("\r\n", "\n")) {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import com.compose.sdl.loadComposeResourceBytes
|
|||||||
import com.compose.sdl.text.namedFontFamily
|
import com.compose.sdl.text.namedFontFamily
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Monospace body font — native actuals (data.kres + IconFont)
|
// MARK: Monospace body font - native actuals (data.kres + IconFont)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
actual val monoFontFamilyName: String? by lazy {
|
actual 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")
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
IconFont.register(kMonoFamily, vBytes)
|
IconFont.register(kMonoFamily, vBytes)
|
||||||
@@ -23,3 +23,16 @@ actual val monoFontFamilyName: String? by lazy {
|
|||||||
actual val monoFontFamily: FontFamily? by lazy {
|
actual val monoFontFamily: FontFamily? by lazy {
|
||||||
monoFontFamilyName?.let { namedFontFamily(it) }
|
monoFontFamilyName?.let { namedFontFamily(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Native already resolves FontFamily.Default to the bundled NotoSans, but the
|
||||||
|
family is registered by name so both stacks go through the same lookup. */
|
||||||
|
actual val defaultFontFamily: FontFamily? by lazy {
|
||||||
|
val vBytes = loadComposeResourceBytes("font/NotoSans.ttf")
|
||||||
|
if (vBytes == null) {
|
||||||
|
println("apidemo: NotoSans.ttf not bundled - using the platform default font")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
IconFont.register(kDefaultFamily, vBytes)
|
||||||
|
namedFontFamily(kDefaultFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package apidemo
|
package apidemo
|
||||||
|
|
||||||
import com.compose.sdl.AppWindowIcon
|
import com.compose.sdl.AppWindowIcon
|
||||||
|
import com.compose.sdl.disableInfiniteAnimations
|
||||||
import com.compose.sdl.nativeComposeWindow
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import com.compose.sdl.useVirtualFrameTime
|
||||||
|
import com.compose.sdl.windowHasInvalidations
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import utils.encodeBmpBgra32
|
||||||
|
import utils.writeFile
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Entry point (native — SDL window shell)
|
// MARK: Entry point (native - SDL window shell)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
// The voltic window/taskbar icon — pre-decoded .rgba blobs bundled into data.kres
|
// The voltic window/taskbar icon - pre-decoded .rgba blobs bundled into data.kres
|
||||||
// under icon/ by the bridge plugin (compose.desktop.native { icon {} }); the
|
// under icon/ by the bridge plugin (compose.desktop.native { icon {} }); the
|
||||||
// backend uses the largest as the base and the rest as alternate sizes.
|
// backend uses the largest as the base and the rest as alternate sizes.
|
||||||
private val kAppIcon = AppWindowIcon(
|
private val kAppIcon = AppWindowIcon(
|
||||||
@@ -15,6 +21,57 @@ private val kAppIcon = AppWindowIcon(
|
|||||||
dark = listOf("icon/voltic-icon-dark-128.rgba", "icon/voltic-icon-dark-32.rgba")
|
dark = listOf("icon/voltic-icon-dark-128.rgba", "icon/voltic-icon-dark-32.rgba")
|
||||||
)
|
)
|
||||||
|
|
||||||
fun main() {
|
private const val kWidth = 1240
|
||||||
nativeComposeWindow(title = "API Manager", width = 1240, height = 820, icon = kAppIcon) { App() }
|
private const val kHeight = 820
|
||||||
|
|
||||||
|
/** Safety net for content that never stops invalidating. */
|
||||||
|
private const val kMaxFrames = 300
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point.
|
||||||
|
*
|
||||||
|
* `--screenshot=<path.bmp>` renders to quiescence and dumps the framebuffer, then
|
||||||
|
* exits - the native half of the JVM-vs-native drift check (the JVM half is
|
||||||
|
* `./gradlew :apidemo:run --args=--screenshot=<path.png>`, which goes through
|
||||||
|
* ImageComposeScene). The capture is a GPU READBACK, not a screen grab, so it
|
||||||
|
* does not care whether the window is visible or occluded.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val screenshot = args.firstOrNull { it.startsWith("--screenshot=") }?.substringAfter('=')
|
||||||
|
if (screenshot != null) {
|
||||||
|
// Match the JVM leg: freeze infinite animations and drive a virtual clock so
|
||||||
|
// entrance animations settle deterministically instead of run-to-run.
|
||||||
|
disableInfiniteAnimations = true
|
||||||
|
useVirtualFrameTime = true
|
||||||
|
}
|
||||||
|
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "API Manager",
|
||||||
|
width = kWidth,
|
||||||
|
height = kHeight,
|
||||||
|
icon = kAppIcon,
|
||||||
|
onFrame = if (screenshot != null) {
|
||||||
|
// Capture once the window reports no pending invalidations for a few
|
||||||
|
// consecutive frames, or at the cap. Mirrors :demo's parity path.
|
||||||
|
var quietFrames = 0
|
||||||
|
{ backend, frameIndex ->
|
||||||
|
quietFrames = if (windowHasInvalidations()) 0 else quietFrames + 1
|
||||||
|
if (quietFrames >= 3 || frameIndex >= kMaxFrames) {
|
||||||
|
if (frameIndex >= kMaxFrames) {
|
||||||
|
println("Screenshot: quiescence not reached by frame $frameIndex - capturing anyway")
|
||||||
|
}
|
||||||
|
val snap = backend.snapshotBgra()
|
||||||
|
if (snap != null) {
|
||||||
|
val (w, h, bgra) = snap
|
||||||
|
writeFile(screenshot, encodeBmpBgra32(w, h, bgra))
|
||||||
|
println("Wrote screenshot: $screenshot (${w}x$h, settled at frame $frameIndex)")
|
||||||
|
} else {
|
||||||
|
println("Screenshot snapshot was null")
|
||||||
|
}
|
||||||
|
false // quit
|
||||||
|
} else true
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
) { App() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import io.ktor.client.engine.curl.*
|
|||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Native actuals — pure delegation to the port's SDL-backed APIs
|
// MARK: Native actuals - pure delegation to the port's SDL-backed APIs
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
actual fun appDataDir(inOrg: String, inApp: String): String? =
|
actual fun appDataDir(inOrg: String, inApp: String): String? =
|
||||||
@@ -55,7 +55,7 @@ actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
|
|||||||
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
||||||
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
||||||
|
|
||||||
// Cached resolver for wrappedRowCount — the skiko engine resolves fonts by name
|
// Cached resolver for wrappedRowCount - the skiko engine resolves fonts by name
|
||||||
// (projectFontName), so this is just the required non-null Paragraph argument.
|
// (projectFontName), so this is just the required non-null Paragraph argument.
|
||||||
private val mRowCountFontResolver = createFontFamilyResolver()
|
private val mRowCountFontResolver = createFontFamilyResolver()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import kotlinx.cinterop.refTo
|
||||||
|
import platform.posix.fclose
|
||||||
|
import platform.posix.fopen
|
||||||
|
import platform.posix.fwrite
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: BMP save
|
||||||
|
// ==================
|
||||||
|
// Copy of demo/src/nativeMain/kotlin/utils/Bmp.kt. Kept duplicated rather than
|
||||||
|
// promoted into a library module: it is app-level tooling for the drift-check
|
||||||
|
// screenshots, and :ui-graphics deliberately does not re-export skiko to apps,
|
||||||
|
// so there is no natural shared home that would not widen the published API.
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
internal fun writeFile(inPath: String, inBytes: ByteArray) {
|
||||||
|
val vFile = fopen(inPath, "wb") ?: run {
|
||||||
|
println("fopen failed for $inPath")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fwrite(inBytes.refTo(0), 1u, inBytes.size.toULong(), vFile)
|
||||||
|
fclose(vFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal BMP writer: 24-bit BGR (drop alpha) with the classic 40-byte
|
||||||
|
BITMAPINFOHEADER. Universally readable, including `sips`. Rows are
|
||||||
|
bottom-up (positive height) and padded to 4 bytes. */
|
||||||
|
internal fun encodeBmpBgra32(inWidth: Int, inHeight: Int, inBgra: ByteArray): ByteArray {
|
||||||
|
val kFileHeader = 14
|
||||||
|
val kInfoHeader = 40
|
||||||
|
val vRowBytes = inWidth * 3
|
||||||
|
val vRowPad = (4 - vRowBytes % 4) % 4
|
||||||
|
val vStride = vRowBytes + vRowPad
|
||||||
|
val vPixelBytes = vStride * inHeight
|
||||||
|
val vTotal = kFileHeader + kInfoHeader + vPixelBytes
|
||||||
|
val vOut = ByteArray(vTotal)
|
||||||
|
|
||||||
|
fun putU16LE(off: Int, v: Int) {
|
||||||
|
vOut[off] = (v and 0xFF).toByte()
|
||||||
|
vOut[off + 1] = ((v ushr 8) and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
fun putU32LE(off: Int, v: Int) {
|
||||||
|
vOut[off] = (v and 0xFF).toByte()
|
||||||
|
vOut[off + 1] = ((v ushr 8) and 0xFF).toByte()
|
||||||
|
vOut[off + 2] = ((v ushr 16) and 0xFF).toByte()
|
||||||
|
vOut[off + 3] = ((v ushr 24) and 0xFF).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BITMAPFILEHEADER
|
||||||
|
vOut[0] = 'B'.code.toByte(); vOut[1] = 'M'.code.toByte()
|
||||||
|
putU32LE(2, vTotal)
|
||||||
|
putU32LE(10, kFileHeader + kInfoHeader)
|
||||||
|
|
||||||
|
// BITMAPINFOHEADER
|
||||||
|
val info = kFileHeader
|
||||||
|
putU32LE(info + 0, kInfoHeader)
|
||||||
|
putU32LE(info + 4, inWidth)
|
||||||
|
putU32LE(info + 8, inHeight) // positive → bottom-up
|
||||||
|
putU16LE(info + 12, 1)
|
||||||
|
putU16LE(info + 14, 24)
|
||||||
|
putU32LE(info + 16, 0) // BI_RGB
|
||||||
|
putU32LE(info + 20, vPixelBytes)
|
||||||
|
putU32LE(info + 24, 2835)
|
||||||
|
putU32LE(info + 28, 2835)
|
||||||
|
putU32LE(info + 32, 0)
|
||||||
|
putU32LE(info + 36, 0)
|
||||||
|
|
||||||
|
// Source is top-down BGRA; convert to bottom-up 24-bit BGR.
|
||||||
|
val vPixelOffset = kFileHeader + kInfoHeader
|
||||||
|
for (y in 0 until inHeight) {
|
||||||
|
val vSrcRow = (inHeight - 1 - y) * inWidth * 4
|
||||||
|
val vDstRow = vPixelOffset + y * vStride
|
||||||
|
for (x in 0 until inWidth) {
|
||||||
|
vOut[vDstRow + x * 3 + 0] = inBgra[vSrcRow + x * 4 + 0] // B
|
||||||
|
vOut[vDstRow + x * 3 + 1] = inBgra[vSrcRow + x * 4 + 1] // G
|
||||||
|
vOut[vDstRow + x * 3 + 2] = inBgra[vSrcRow + x * 4 + 2] // R
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vOut
|
||||||
|
}
|
||||||
+113
-49
@@ -12,7 +12,7 @@ plugins {
|
|||||||
apiValidation {
|
apiValidation {
|
||||||
@OptIn(ExperimentalBCVApi::class)
|
@OptIn(ExperimentalBCVApi::class)
|
||||||
klib { enabled = true }
|
klib { enabled = true }
|
||||||
// demo/apidemo: apps, no published API. sdl-core: naked SDL cinterop — its
|
// demo/apidemo: apps, no published API. sdl-core: naked SDL cinterop - its
|
||||||
// "API" is the generated sdl3.* bindings (per-OS, can't infer macos), not our
|
// "API" is the generated sdl3.* bindings (per-OS, can't infer macos), not our
|
||||||
// surface. material-symbols: auto-generated icon codepoint maps + its jvm
|
// surface. material-symbols: auto-generated icon codepoint maps + its jvm
|
||||||
// dump trips BCV's ASM on newer JDKs (class file major 69). Track the
|
// dump trips BCV's ASM on newer JDKs (class file major 69). Track the
|
||||||
@@ -32,27 +32,50 @@ val vPublishVersion = (System.getenv("PUBLISH_VERSION") ?: "0.0.0-SNAPSHOT").rem
|
|||||||
// headline app-shell artifact, published under com.bitsycore.compose.
|
// headline app-shell artifact, published under com.bitsycore.compose.
|
||||||
val kDefaultGroup = "com.bitsycore.compose.sdl"
|
val kDefaultGroup = "com.bitsycore.compose.sdl"
|
||||||
val kAreaGroups = mapOf(
|
val kAreaGroups = mapOf(
|
||||||
":ui" to "com.bitsycore.compose.ui",
|
":compose:ui:ui" to "com.bitsycore.compose.ui",
|
||||||
":ui-graphics" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-graphics" to "com.bitsycore.compose.ui",
|
||||||
":ui-text" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-text" to "com.bitsycore.compose.ui",
|
||||||
":ui-unit" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-unit" to "com.bitsycore.compose.ui",
|
||||||
":ui-geometry" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-geometry" to "com.bitsycore.compose.ui",
|
||||||
":ui-util" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-util" to "com.bitsycore.compose.ui",
|
||||||
":ui-backhandler" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-backhandler" to "com.bitsycore.compose.ui",
|
||||||
":ui-tooling-preview" to "com.bitsycore.compose.ui",
|
":compose:ui:ui-tooling-preview" to "com.bitsycore.compose.ui",
|
||||||
":foundation" to "com.bitsycore.compose.foundation",
|
":compose:foundation:foundation" to "com.bitsycore.compose.foundation",
|
||||||
":foundation-layout" to "com.bitsycore.compose.foundation",
|
":compose:foundation:foundation-layout" to "com.bitsycore.compose.foundation",
|
||||||
":animation" to "com.bitsycore.compose.animation",
|
":compose:animation:animation" to "com.bitsycore.compose.animation",
|
||||||
":animation-core" to "com.bitsycore.compose.animation",
|
":compose:animation:animation-core" to "com.bitsycore.compose.animation",
|
||||||
":animation-graphics" to "com.bitsycore.compose.animation",
|
":compose:animation:animation-graphics" to "com.bitsycore.compose.animation",
|
||||||
":material3" to "com.bitsycore.compose.material3",
|
":compose:material3:material3" to "com.bitsycore.compose.material3",
|
||||||
":material-ripple" to "com.bitsycore.compose.material",
|
":compose:material3:adaptive:adaptive" to "com.bitsycore.compose.material3.adaptive",
|
||||||
":components-resources" to "com.bitsycore.compose.components",
|
":compose:material3:adaptive:adaptive-layout" to "com.bitsycore.compose.material3.adaptive",
|
||||||
":navigation3-ui" to "com.bitsycore.navigation3",
|
":compose:material3:adaptive:adaptive-navigation" to "com.bitsycore.compose.material3.adaptive",
|
||||||
":desktop-native-window" to "com.bitsycore.compose",
|
":compose:material3:adaptive:adaptive-navigation3" to "com.bitsycore.compose.material3.adaptive",
|
||||||
|
":compose:material:material-ripple" to "com.bitsycore.compose.material",
|
||||||
|
":components:resources:components-resources" to "com.bitsycore.compose.components",
|
||||||
|
":navigation3:navigation3-ui" to "com.bitsycore.navigation3",
|
||||||
|
":koin:koin-core-viewmodel" to "com.bitsycore.koin",
|
||||||
|
":koin:koin-compose" to "com.bitsycore.koin",
|
||||||
|
":koin:koin-compose-viewmodel" to "com.bitsycore.koin",
|
||||||
|
":koin:koin-compose-navigation3" to "com.bitsycore.koin",
|
||||||
|
":coil:coil-core" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil-compose-core" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil-compose" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil-svg" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil-network-core" to "com.bitsycore.coil3",
|
||||||
|
":coil:coil-network-ktor3" to "com.bitsycore.coil3",
|
||||||
|
":pulse:pulse" to "com.bitsycore.compose.desktop.native.pulse",
|
||||||
|
":pulse:pulse-viewmodel" to "com.bitsycore.compose.desktop.native.pulse",
|
||||||
|
":pulse:pulse-savedstate" to "com.bitsycore.compose.desktop.native.pulse",
|
||||||
|
":pulse:pulse-compose" to "com.bitsycore.compose.desktop.native.pulse",
|
||||||
|
":compose:desktop:native:desktop-native-window" to "com.bitsycore.compose",
|
||||||
)
|
)
|
||||||
fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup
|
fun groupFor(path: String): String = kAreaGroups[path] ?: kDefaultGroup
|
||||||
|
|
||||||
|
// The published artifactId is always the leaf project name - settings.gradle.kts
|
||||||
|
// keeps the leaf equal to the artifactId even where the directory differs.
|
||||||
|
fun artifactIdFor(path: String): String = path.substringAfterLast(':')
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = groupFor(path)
|
group = groupFor(path)
|
||||||
version = vPublishVersion
|
version = vPublishVersion
|
||||||
@@ -63,7 +86,7 @@ allprojects {
|
|||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
// Every library module (everything except the two demo apps) auto-registers a
|
// Every library module (everything except the two demo apps) auto-registers a
|
||||||
// MavenPublication via the kotlin-multiplatform plugin — one per target + one
|
// MavenPublication via the kotlin-multiplatform plugin - one per target + one
|
||||||
// for the shared kotlinMultiplatform metadata. The CI publish workflow runs on
|
// for the shared kotlinMultiplatform metadata. The CI publish workflow runs on
|
||||||
// three hosts (macOS / Linux / Windows) and each invokes only the publication
|
// three hosts (macOS / Linux / Windows) and each invokes only the publication
|
||||||
// tasks Gradle actually generated for its own targets, so the group of hosts
|
// tasks Gradle actually generated for its own targets, so the group of hosts
|
||||||
@@ -72,19 +95,27 @@ allprojects {
|
|||||||
|
|
||||||
val kAppModules = setOf(":demo", ":apidemo")
|
val kAppModules = setOf(":demo", ":apidemo")
|
||||||
val kPublishedLibs = setOf(
|
val kPublishedLibs = setOf(
|
||||||
":sdl-core",
|
":sdl:sdl-core",
|
||||||
":ui", ":ui-util", ":ui-geometry", ":ui-graphics", ":ui-text",
|
":compose:ui:ui", ":compose:ui:ui-util", ":compose:ui:ui-geometry",
|
||||||
":ui-unit", ":ui-backhandler", ":ui-tooling-preview",
|
":compose:ui:ui-graphics", ":compose:ui:ui-text",
|
||||||
":animation-core", ":animation", ":animation-graphics",
|
":compose:ui:ui-unit", ":compose:ui:ui-backhandler", ":compose:ui:ui-tooling-preview",
|
||||||
":foundation", ":foundation-layout",
|
":compose:animation:animation-core", ":compose:animation:animation",
|
||||||
":material3", ":material-ripple",
|
":compose:animation:animation-graphics",
|
||||||
":desktop-native-window", ":material-symbols",
|
":compose:foundation:foundation", ":compose:foundation:foundation-layout",
|
||||||
":navigation3-ui", ":components-resources",
|
":compose:material3:material3", ":compose:material:material-ripple",
|
||||||
|
":compose:material3:adaptive:adaptive", ":compose:material3:adaptive:adaptive-layout", ":compose:material3:adaptive:adaptive-navigation", ":compose:material3:adaptive:adaptive-navigation3",
|
||||||
|
":compose:desktop:native:desktop-native-window", ":utils:material-symbols",
|
||||||
|
":navigation3:navigation3-ui", ":components:resources:components-resources",
|
||||||
|
":koin:koin-core-viewmodel", ":koin:koin-compose",
|
||||||
|
":koin:koin-compose-viewmodel", ":koin:koin-compose-navigation3",
|
||||||
|
":coil:coil-core", ":coil:coil", ":coil:coil-compose-core", ":coil:coil-compose",
|
||||||
|
":coil:coil-svg", ":coil:coil-network-core", ":coil:coil-network-ktor3",
|
||||||
|
":pulse:pulse", ":pulse:pulse-viewmodel", ":pulse:pulse-savedstate", ":pulse:pulse-compose",
|
||||||
)
|
)
|
||||||
|
|
||||||
// -PuseGithubPackages=true swaps every `project(":<lib>")` reference the demo
|
// -PuseGithubPackages=true swaps every `project(":<lib>")` reference the demo
|
||||||
// apps make for the published Maven coordinate. Library modules keep resolving
|
// apps make for the published Maven coordinate. Library modules keep resolving
|
||||||
// each other as `project(...)` — the substitution only fires at the
|
// each other as `project(...)` - the substitution only fires at the
|
||||||
// app→library boundary, so the swap validates end-to-end consumption of the
|
// app→library boundary, so the swap validates end-to-end consumption of the
|
||||||
// published klibs without touching the source of `implementation(project(...))`.
|
// published klibs without touching the source of `implementation(project(...))`.
|
||||||
// Version defaults to 0.1.0 (matches the git tag) but can be pinned via -PconsumeVersion=….
|
// Version defaults to 0.1.0 (matches the git tag) but can be pinned via -PconsumeVersion=….
|
||||||
@@ -110,7 +141,7 @@ subprojects {
|
|||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
resolutionStrategy.dependencySubstitution {
|
resolutionStrategy.dependencySubstitution {
|
||||||
kPublishedLibs.forEach { modulePath ->
|
kPublishedLibs.forEach { modulePath ->
|
||||||
val vArtifactId = modulePath.removePrefix(":")
|
val vArtifactId = artifactIdFor(modulePath)
|
||||||
substitute(project(modulePath))
|
substitute(project(modulePath))
|
||||||
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
|
.using(module("${groupFor(modulePath)}:$vArtifactId:$kConsumeVersion"))
|
||||||
.because("-PuseGithubPackages=true")
|
.because("-PuseGithubPackages=true")
|
||||||
@@ -128,6 +159,9 @@ subprojects {
|
|||||||
}
|
}
|
||||||
return@subprojects
|
return@subprojects
|
||||||
}
|
}
|
||||||
|
// Nesting the paths under compose/ / sdl/ / utils/ … materialises container
|
||||||
|
// projects (:compose, :compose:ui, …) that hold no code - skip them.
|
||||||
|
if (path !in kPublishedLibs) return@subprojects
|
||||||
plugins.apply("maven-publish")
|
plugins.apply("maven-publish")
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
extensions.configure<PublishingExtension> {
|
extensions.configure<PublishingExtension> {
|
||||||
@@ -153,7 +187,7 @@ subprojects {
|
|||||||
publications.withType<MavenPublication>().configureEach {
|
publications.withType<MavenPublication>().configureEach {
|
||||||
pom {
|
pom {
|
||||||
name.set("${rootProject.name} ${project.name}")
|
name.set("${rootProject.name} ${project.name}")
|
||||||
description.set("Compose Multiplatform on SDL3 (Kotlin/Native, no JVM) — ${project.name}")
|
description.set("Compose Multiplatform on SDL3 (Kotlin/Native, no JVM) - ${project.name}")
|
||||||
url.set("https://github.com/${System.getenv("GITHUB_REPOSITORY") ?: "bitsycore/ComposeDesktopNative"}")
|
url.set("https://github.com/${System.getenv("GITHUB_REPOSITORY") ?: "bitsycore/ComposeDesktopNative"}")
|
||||||
licenses {
|
licenses {
|
||||||
license {
|
license {
|
||||||
@@ -170,7 +204,7 @@ subprojects {
|
|||||||
// FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL
|
// FULL-COMMONIZATION BRIDGE (repo-wide): a module may declare the OFFICIAL
|
||||||
// Maven Compose artifacts in its commonMain so metadata + jvm resolve them
|
// Maven Compose artifacts in its commonMain so metadata + jvm resolve them
|
||||||
// (e.g. :material-symbols' common API); every NATIVE target configuration
|
// (e.g. :material-symbols' common API); every NATIVE target configuration
|
||||||
// swaps those modules for the port's project equivalents — the Maven
|
// swaps those modules for the port's project equivalents - the Maven
|
||||||
// artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is
|
// artifacts ship no mingwX64/linux klibs. org.jetbrains.compose.runtime is
|
||||||
// deliberately NOT here: the port uses the official runtime klibs everywhere.
|
// deliberately NOT here: the port uses the official runtime klibs everywhere.
|
||||||
val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64")
|
val vNativeTargetTokens = listOf("mingwX64", "linuxX64", "linuxArm64", "macosArm64")
|
||||||
@@ -178,34 +212,64 @@ allprojects {
|
|||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
||||||
resolutionStrategy.dependencySubstitution {
|
resolutionStrategy.dependencySubstitution {
|
||||||
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
|
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":compose:ui:ui"))
|
||||||
// ui-graphics / ui-text are their own modules (split out of :ui,
|
// ui-graphics / ui-text are their own modules (split out of :ui,
|
||||||
// upstream layout). Declare each DIRECTLY so the app commonMain sees
|
// upstream layout). Declare each DIRECTLY so the app commonMain sees
|
||||||
// them under the granular-metadata visibility rule.
|
// them under the granular-metadata visibility rule.
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui-graphics"))
|
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":compose:ui:ui-graphics"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui-text"))
|
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":compose:ui:ui-text"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit"))
|
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":compose:ui:ui-unit"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
|
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":compose:ui:ui-geometry"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
|
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":compose:ui:ui-util"))
|
||||||
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":ui-tooling-preview"))
|
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":compose:ui:ui-tooling-preview"))
|
||||||
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":foundation"))
|
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":compose:foundation:foundation"))
|
||||||
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":foundation-layout"))
|
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":compose:foundation:foundation-layout"))
|
||||||
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":animation"))
|
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":compose:animation:animation"))
|
||||||
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":animation-core"))
|
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":compose:animation:animation-core"))
|
||||||
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":material3"))
|
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":compose:material3:material3"))
|
||||||
|
// material3-adaptive: upstream publishes ios + macosArm64 only - no
|
||||||
|
// linux, no mingw (issues/4). Vendored as :compose:material3:adaptive:*.
|
||||||
|
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive")).using(project(":compose:material3:adaptive:adaptive"))
|
||||||
|
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-layout")).using(project(":compose:material3:adaptive:adaptive-layout"))
|
||||||
|
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-navigation")).using(project(":compose:material3:adaptive:adaptive-navigation"))
|
||||||
|
substitute(module("org.jetbrains.compose.material3.adaptive:adaptive-navigation3")).using(project(":compose:material3:adaptive:adaptive-navigation3"))
|
||||||
// navigation3-ui: the JB Maven artifact has no K/N desktop
|
// navigation3-ui: the JB Maven artifact has no K/N desktop
|
||||||
// klibs — the port vendors it as :navigation3-ui.
|
// klibs - the port vendors it as :navigation3-ui.
|
||||||
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3-ui"))
|
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3:navigation3-ui"))
|
||||||
// components-resources: the official resources runtime ships no
|
// components-resources: the official resources runtime ships no
|
||||||
// mingwX64/linux klibs — the port vendors it as :components-resources.
|
// mingwX64/linux klibs - the port vendors it as :components-resources.
|
||||||
substitute(module("org.jetbrains.compose.components:components-resources")).using(project(":components-resources"))
|
substitute(module("org.jetbrains.compose.components:components-resources")).using(project(":components:resources:components-resources"))
|
||||||
|
// Koin: koin-core itself publishes mingwX64 + linux and is used
|
||||||
|
// from Maven; these four stop at apple+android upstream.
|
||||||
|
substitute(module("io.insert-koin:koin-core-viewmodel")).using(project(":koin:koin-core-viewmodel"))
|
||||||
|
substitute(module("io.insert-koin:koin-compose")).using(project(":koin:koin-compose"))
|
||||||
|
substitute(module("io.insert-koin:koin-compose-viewmodel")).using(project(":koin:koin-compose-viewmodel"))
|
||||||
|
substitute(module("io.insert-koin:koin-compose-navigation3")).using(project(":koin:koin-compose-navigation3"))
|
||||||
|
// Coil 3: no mingwX64 upstream anywhere, and no linux for the
|
||||||
|
// compose layer either.
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-core")).using(project(":coil:coil-core"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil")).using(project(":coil:coil"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-compose-core")).using(project(":coil:coil-compose-core"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-compose")).using(project(":coil:coil-compose"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-svg")).using(project(":coil:coil-svg"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-network-core")).using(project(":coil:coil-network-core"))
|
||||||
|
substitute(module("io.coil-kt.coil3:coil-network-ktor3")).using(project(":coil:coil-network-ktor3"))
|
||||||
|
// Pulse MVI: upstream publishes com.bitsycore.lib:* with no desktop
|
||||||
|
// native targets. Pulse is itself a com.bitsycore library, so the port's
|
||||||
|
// republish is namespaced under THIS project rather than a bare
|
||||||
|
// com.bitsycore.pulse, which would read like the upstream artifact. The
|
||||||
|
// two coordinates never collide, and substitutes the official one here.
|
||||||
|
substitute(module("com.bitsycore.lib:pulse")).using(project(":pulse:pulse"))
|
||||||
|
substitute(module("com.bitsycore.lib:pulse-viewmodel")).using(project(":pulse:pulse-viewmodel"))
|
||||||
|
substitute(module("com.bitsycore.lib:pulse-savedstate")).using(project(":pulse:pulse-savedstate"))
|
||||||
|
substitute(module("com.bitsycore.lib:pulse-compose")).using(project(":pulse:pulse-compose"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 the mingwX64 sdl3 cinterop from a Windows host — it needs
|
// only cross-compile the mingwX64 sdl3 cinterop from a Windows host - it needs
|
||||||
// the Windows SDL3 headers under libs/ (produced by scripts/build-sdl/build-all.py
|
// the Windows SDL3 headers under libs/ (produced by scripts/build-sdl/build-all.py
|
||||||
// run on a Windows host). Declaring `mingwX64()` on a non-Windows host is safe
|
// run on a Windows host). Declaring `mingwX64()` on a non-Windows host is safe
|
||||||
// for pure-Kotlin modules but blows up the moment the sdl3 cinterop tries to
|
// for pure-Kotlin modules but blows up the moment the sdl3 cinterop tries to
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import java.net.URI
|
|||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-app knobs for [registerComposeFontBundling]. EVERYTHING is opt-in — with no
|
* Per-app knobs for [registerComposeFontBundling]. EVERYTHING is opt-in - with no
|
||||||
* flag set the call registers no tasks and stages nothing.
|
* flag set the call registers no tasks and stages nothing.
|
||||||
*/
|
*/
|
||||||
class ComposeFontBundlingConfig {
|
class ComposeFontBundlingConfig {
|
||||||
@@ -31,7 +31,7 @@ class ComposeFontBundlingConfig {
|
|||||||
var bundleNotoSans: Boolean = false
|
var bundleNotoSans: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Always bundle font/NotoSansMono.ttf — for apps that load the mono font through
|
* Always bundle font/NotoSansMono.ttf - for apps that load the mono font through
|
||||||
* their own seam (apidemo's body font, Fonts.kt), invisible to auto-detection.
|
* their own seam (apidemo's body font, Fonts.kt), invisible to auto-detection.
|
||||||
*/
|
*/
|
||||||
var bundleNotoSansMono: Boolean = false
|
var bundleNotoSansMono: Boolean = false
|
||||||
@@ -51,7 +51,7 @@ class ComposeFontBundlingConfig {
|
|||||||
/**
|
/**
|
||||||
* Subset each bundled Material Symbols font to the glyphs the sources reference
|
* Subset each bundled Material Symbols font to the glyphs the sources reference
|
||||||
* (scripts/subset-material-symbols.py picks the codepoints; the subset task uses
|
* (scripts/subset-material-symbols.py picks the codepoints; the subset task uses
|
||||||
* hb-subset when present, else fontTools via Python — auto-installed — and only
|
* hb-subset when present, else fontTools via Python - auto-installed - and only
|
||||||
* bundles the full font if neither is available). Needs [bundleMaterialSymbols];
|
* bundles the full font if neither is available). Needs [bundleMaterialSymbols];
|
||||||
* still gated by -PsubsetIcons.
|
* still gated by -PsubsetIcons.
|
||||||
*/
|
*/
|
||||||
@@ -74,11 +74,11 @@ private val kMonospaceRegex = Regex("\\bFontFamily\\.Monospace\\b")
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Wires the app font pipeline shared by :demo and :apidemo. Everything is OPT-IN via
|
* Wires the app font pipeline shared by :demo and :apidemo. Everything is OPT-IN via
|
||||||
* [ComposeFontBundlingConfig] — a bare call is a no-op.
|
* [ComposeFontBundlingConfig] - a bare call is a no-op.
|
||||||
*
|
*
|
||||||
* - Registers downloadNotoFonts (Noto Sans + Noto Sans Mono variable fonts →
|
* - Registers downloadNotoFonts (Noto Sans + Noto Sans Mono variable fonts →
|
||||||
* build/fonts/) when a Noto font is bundled.
|
* build/fonts/) when a Noto font is bundled.
|
||||||
* - Adds the opted-in font/ entries to every data.kres Zip task — the bridge plugin's
|
* - Adds the opted-in font/ entries to every data.kres Zip task - the bridge plugin's
|
||||||
* package<Variant>ComposeResources<Target> tasks or an app's own copy* tasks
|
* package<Variant>ComposeResources<Target> tasks or an app's own copy* tasks
|
||||||
* (matching{} is lazy: the plugin's tasks appear in its afterEvaluate). Material
|
* (matching{} is lazy: the plugin's tasks appear in its afterEvaluate). Material
|
||||||
* Symbols fonts are hb-subset to the used glyphs when subsetting is enabled.
|
* Symbols fonts are hb-subset to the used glyphs when subsetting is enabled.
|
||||||
@@ -142,7 +142,7 @@ fun Project.registerComposeFontBundling(configure: ComposeFontBundlingConfig.()
|
|||||||
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, vSubsetTasksByStyle)
|
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, vSubsetTasksByStyle)
|
||||||
}
|
}
|
||||||
tasks.withType<ProcessResources>().matching { it.name == "jvmProcessResources" }.configureEach {
|
tasks.withType<ProcessResources>().matching { it.name == "jvmProcessResources" }.configureEach {
|
||||||
// Full fonts on JVM — Skiko applies the variable axes itself (Typeface.makeClone).
|
// Full fonts on JVM - Skiko applies the variable axes itself (Typeface.makeClone).
|
||||||
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, emptyMap())
|
addFontEntries(this, vNotoFiles, vDownloadNotoFonts, vUsedStyles, emptyMap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ private fun Project.detectUsedStyles(): List<String> {
|
|||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the font/ entries (Notos + used Material Symbols styles) to inTask — a data.kres
|
* Adds the font/ entries (Notos + used Material Symbols styles) to inTask - a data.kres
|
||||||
* Zip or jvmProcessResources. inSubsetTasksByStyle maps styles to hb-subset tasks whose
|
* Zip or jvmProcessResources. inSubsetTasksByStyle maps styles to hb-subset tasks whose
|
||||||
* output replaces the full font; pass an empty map to stage the full fonts.
|
* output replaces the full font; pass an empty map to stage the full fonts.
|
||||||
*/
|
*/
|
||||||
@@ -197,7 +197,7 @@ private fun Project.addFontEntries(
|
|||||||
inTask.from(vNotoFile) { into("font") }
|
inTask.from(vNotoFile) { into("font") }
|
||||||
inDownloadNotoFonts?.let { inTask.dependsOn(it) }
|
inDownloadNotoFonts?.let { inTask.dependsOn(it) }
|
||||||
}
|
}
|
||||||
val vSymbolsProject = rootProject.project(":material-symbols")
|
val vSymbolsProject = rootProject.project(":utils:material-symbols")
|
||||||
for (vStyle in inUsedStyles) {
|
for (vStyle in inUsedStyles) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as Provider<RegularFile>
|
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as Provider<RegularFile>
|
||||||
@@ -232,7 +232,7 @@ private fun Project.registerIconSubsetPipeline(inUsedStyles: List<String>): Map<
|
|||||||
val vFindUsage = tasks.register<Exec>("findMaterialSymbolsUsage") {
|
val vFindUsage = tasks.register<Exec>("findMaterialSymbolsUsage") {
|
||||||
description = "Scan src/ for MaterialSymbols.<Name> usages → usage-codepoint.txt."
|
description = "Scan src/ for MaterialSymbols.<Name> usages → usage-codepoint.txt."
|
||||||
val vScript = rootProject.layout.projectDirectory.file("scripts/subset-material-symbols.py").asFile
|
val vScript = rootProject.layout.projectDirectory.file("scripts/subset-material-symbols.py").asFile
|
||||||
val vConstants = rootProject.project(":material-symbols").layout.projectDirectory
|
val vConstants = rootProject.project(":utils:material-symbols").layout.projectDirectory
|
||||||
.file("src/commonMain/kotlin/com/compose/sdl/icons/MaterialSymbols.kt").asFile
|
.file("src/commonMain/kotlin/com/compose/sdl/icons/MaterialSymbols.kt").asFile
|
||||||
val vUsageFile = vIconsBuildDir.get().file("usage-codepoint.txt").asFile
|
val vUsageFile = vIconsBuildDir.get().file("usage-codepoint.txt").asFile
|
||||||
inputs.files(fileTree("src") { include("**/*.kt") })
|
inputs.files(fileTree("src") { include("**/*.kt") })
|
||||||
@@ -259,7 +259,7 @@ private fun Project.registerSubsetTask(
|
|||||||
inIconsBuildDir: Provider<Directory>,
|
inIconsBuildDir: Provider<Directory>,
|
||||||
inFindUsage: TaskProvider<*>,
|
inFindUsage: TaskProvider<*>,
|
||||||
): TaskProvider<*> {
|
): TaskProvider<*> {
|
||||||
val vSymbolsProject = rootProject.project(":material-symbols")
|
val vSymbolsProject = rootProject.project(":utils:material-symbols")
|
||||||
return tasks.register("subsetMaterialSymbols$inStyle") {
|
return tasks.register("subsetMaterialSymbols$inStyle") {
|
||||||
description = "hb-subset the $inStyle Material Symbols font to icons actually used."
|
description = "hb-subset the $inStyle Material Symbols font to icons actually used."
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -304,21 +304,21 @@ private fun Project.registerSubsetTask(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. hb-subset (harfbuzz) — fastest path when it is on PATH.
|
// 1. hb-subset (harfbuzz) - fastest path when it is on PATH.
|
||||||
if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first
|
if (runCmd("hb-subset", vInputFile.absolutePath, "-o", vOut.absolutePath, "--unicodes=$vUnicodes").first
|
||||||
&& vOut.length() > 0L
|
&& vOut.length() > 0L
|
||||||
) {
|
) {
|
||||||
logShrink("hb-subset"); return@doLast
|
logShrink("hb-subset"); return@doLast
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. fontTools (pyftsubset) via Python — the portable fallback. Python is
|
// 2. fontTools (pyftsubset) via Python - the portable fallback. Python is
|
||||||
// already a build prerequisite (SDL build + vendor sync) and fontTools is
|
// already a build prerequisite (SDL build + vendor sync) and fontTools is
|
||||||
// variable-font-aware, so the FILL/wght/GRAD/opsz axes survive. It is
|
// variable-font-aware, so the FILL/wght/GRAD/opsz axes survive. It is
|
||||||
// auto-installed if missing, so no manual harfbuzz install is ever required.
|
// auto-installed if missing, so no manual harfbuzz install is ever required.
|
||||||
val vPython = listOf("python3", "python", "py").firstOrNull { runCmd(it, "--version").first }
|
val vPython = listOf("python3", "python", "py").firstOrNull { runCmd(it, "--version").first }
|
||||||
if (vPython != null) {
|
if (vPython != null) {
|
||||||
if (!runCmd(vPython, "-c", "import fontTools").first) {
|
if (!runCmd(vPython, "-c", "import fontTools").first) {
|
||||||
logger.lifecycle("[subset $inStyle] fontTools not present — installing it via pip…")
|
logger.lifecycle("[subset $inStyle] fontTools not present - installing it via pip…")
|
||||||
// --user (no root) → --break-system-packages (PEP 668) → plain.
|
// --user (no root) → --break-system-packages (PEP 668) → plain.
|
||||||
listOf(listOf("--user"), listOf("--break-system-packages"), emptyList<String>())
|
listOf(listOf("--user"), listOf("--break-system-packages"), emptyList<String>())
|
||||||
.firstOrNull { vExtra ->
|
.firstOrNull { vExtra ->
|
||||||
@@ -336,7 +336,7 @@ private fun Project.registerSubsetTask(
|
|||||||
logger.warn("[subset $inStyle] fontTools subset failed :\n$vFtOut")
|
logger.warn("[subset $inStyle] fontTools subset failed :\n$vFtOut")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Last resort : the full font — correct, just larger.
|
// 3. Last resort : the full font - correct, just larger.
|
||||||
vInputFile.copyTo(vOut, overwrite = true)
|
vInputFile.copyTo(vOut, overwrite = true)
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"[subset $inStyle] no working subsetter found (hb-subset or python3+fontTools) : " +
|
"[subset $inStyle] no working subsetter found (hb-subset or python3+fontTools) : " +
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
// 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.coil3:coil-compose-core>
|
||||||
|
abstract fun interface coil3.compose/AsyncImagePreviewHandler { // coil3.compose/AsyncImagePreviewHandler|null[0]
|
||||||
|
abstract suspend fun handle(coil3/ImageLoader, coil3.request/ImageRequest): coil3.compose/AsyncImagePainter.State // coil3.compose/AsyncImagePreviewHandler.handle|handle(coil3.ImageLoader;coil3.request.ImageRequest){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.compose/AsyncImagePreviewHandler.Companion|null[0]
|
||||||
|
final val Default // coil3.compose/AsyncImagePreviewHandler.Companion.Default|{}Default[0]
|
||||||
|
final fun <get-Default>(): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler.Companion.Default.<get-Default>|<get-Default>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.compose/AsyncImageModelEqualityDelegate { // coil3.compose/AsyncImageModelEqualityDelegate|null[0]
|
||||||
|
abstract fun equals(kotlin/Any?, kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImageModelEqualityDelegate.equals|equals(kotlin.Any?;kotlin.Any?){}[0]
|
||||||
|
abstract fun hashCode(kotlin/Any?): kotlin/Int // coil3.compose/AsyncImageModelEqualityDelegate.hashCode|hashCode(kotlin.Any?){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.compose/AsyncImageModelEqualityDelegate.Companion|null[0]
|
||||||
|
final val AllProperties // coil3.compose/AsyncImageModelEqualityDelegate.Companion.AllProperties|{}AllProperties[0]
|
||||||
|
final fun <get-AllProperties>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImageModelEqualityDelegate.Companion.AllProperties.<get-AllProperties>|<get-AllProperties>(){}[0]
|
||||||
|
final val Default // coil3.compose/AsyncImageModelEqualityDelegate.Companion.Default|{}Default[0]
|
||||||
|
final fun <get-Default>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImageModelEqualityDelegate.Companion.Default.<get-Default>|<get-Default>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.compose/DrawScopeSizeResolver : coil3.size/SizeResolver { // coil3.compose/DrawScopeSizeResolver|null[0]
|
||||||
|
abstract fun connect(kotlinx.coroutines.flow/Flow<androidx.compose.ui.geometry/Size>) // coil3.compose/DrawScopeSizeResolver.connect|connect(kotlinx.coroutines.flow.Flow<androidx.compose.ui.geometry.Size>){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.compose/SubcomposeAsyncImageScope : androidx.compose.foundation.layout/BoxScope { // coil3.compose/SubcomposeAsyncImageScope|null[0]
|
||||||
|
abstract val alignment // coil3.compose/SubcomposeAsyncImageScope.alignment|{}alignment[0]
|
||||||
|
abstract fun <get-alignment>(): androidx.compose.ui/Alignment // coil3.compose/SubcomposeAsyncImageScope.alignment.<get-alignment>|<get-alignment>(){}[0]
|
||||||
|
abstract val alpha // coil3.compose/SubcomposeAsyncImageScope.alpha|{}alpha[0]
|
||||||
|
abstract fun <get-alpha>(): kotlin/Float // coil3.compose/SubcomposeAsyncImageScope.alpha.<get-alpha>|<get-alpha>(){}[0]
|
||||||
|
abstract val clipToBounds // coil3.compose/SubcomposeAsyncImageScope.clipToBounds|{}clipToBounds[0]
|
||||||
|
abstract fun <get-clipToBounds>(): kotlin/Boolean // coil3.compose/SubcomposeAsyncImageScope.clipToBounds.<get-clipToBounds>|<get-clipToBounds>(){}[0]
|
||||||
|
abstract val colorFilter // coil3.compose/SubcomposeAsyncImageScope.colorFilter|{}colorFilter[0]
|
||||||
|
abstract fun <get-colorFilter>(): androidx.compose.ui.graphics/ColorFilter? // coil3.compose/SubcomposeAsyncImageScope.colorFilter.<get-colorFilter>|<get-colorFilter>(){}[0]
|
||||||
|
abstract val contentDescription // coil3.compose/SubcomposeAsyncImageScope.contentDescription|{}contentDescription[0]
|
||||||
|
abstract fun <get-contentDescription>(): kotlin/String? // coil3.compose/SubcomposeAsyncImageScope.contentDescription.<get-contentDescription>|<get-contentDescription>(){}[0]
|
||||||
|
abstract val contentScale // coil3.compose/SubcomposeAsyncImageScope.contentScale|{}contentScale[0]
|
||||||
|
abstract fun <get-contentScale>(): androidx.compose.ui.layout/ContentScale // coil3.compose/SubcomposeAsyncImageScope.contentScale.<get-contentScale>|<get-contentScale>(){}[0]
|
||||||
|
abstract val painter // coil3.compose/SubcomposeAsyncImageScope.painter|{}painter[0]
|
||||||
|
abstract fun <get-painter>(): coil3.compose/AsyncImagePainter // coil3.compose/SubcomposeAsyncImageScope.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.compose/AsyncImagePainter : androidx.compose.runtime/RememberObserver, androidx.compose.ui.graphics.painter/Painter { // coil3.compose/AsyncImagePainter|null[0]
|
||||||
|
final val input // coil3.compose/AsyncImagePainter.input|{}input[0]
|
||||||
|
final fun <get-input>(): kotlinx.coroutines.flow/StateFlow<coil3.compose/AsyncImagePainter.Input> // coil3.compose/AsyncImagePainter.input.<get-input>|<get-input>(){}[0]
|
||||||
|
final val intrinsicSize // coil3.compose/AsyncImagePainter.intrinsicSize|{}intrinsicSize[0]
|
||||||
|
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/AsyncImagePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
|
||||||
|
final val state // coil3.compose/AsyncImagePainter.state|{}state[0]
|
||||||
|
final fun <get-state>(): kotlinx.coroutines.flow/StateFlow<coil3.compose/AsyncImagePainter.State> // coil3.compose/AsyncImagePainter.state.<get-state>|<get-state>(){}[0]
|
||||||
|
|
||||||
|
final fun onAbandoned() // coil3.compose/AsyncImagePainter.onAbandoned|onAbandoned(){}[0]
|
||||||
|
final fun onForgotten() // coil3.compose/AsyncImagePainter.onForgotten|onForgotten(){}[0]
|
||||||
|
final fun onRemembered() // coil3.compose/AsyncImagePainter.onRemembered|onRemembered(){}[0]
|
||||||
|
final fun restart() // coil3.compose/AsyncImagePainter.restart|restart(){}[0]
|
||||||
|
|
||||||
|
sealed interface State { // coil3.compose/AsyncImagePainter.State|null[0]
|
||||||
|
abstract val painter // coil3.compose/AsyncImagePainter.State.painter|{}painter[0]
|
||||||
|
abstract fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
|
||||||
|
final class Error : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Error|null[0]
|
||||||
|
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, coil3.request/ErrorResult) // coil3.compose/AsyncImagePainter.State.Error.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;coil3.request.ErrorResult){}[0]
|
||||||
|
|
||||||
|
final val painter // coil3.compose/AsyncImagePainter.State.Error.painter|{}painter[0]
|
||||||
|
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Error.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
final val result // coil3.compose/AsyncImagePainter.State.Error.result|{}result[0]
|
||||||
|
final fun <get-result>(): coil3.request/ErrorResult // coil3.compose/AsyncImagePainter.State.Error.result.<get-result>|<get-result>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Error.component1|component1(){}[0]
|
||||||
|
final fun component2(): coil3.request/ErrorResult // coil3.compose/AsyncImagePainter.State.Error.component2|component2(){}[0]
|
||||||
|
final fun copy(androidx.compose.ui.graphics.painter/Painter? = ..., coil3.request/ErrorResult = ...): coil3.compose/AsyncImagePainter.State.Error // coil3.compose/AsyncImagePainter.State.Error.copy|copy(androidx.compose.ui.graphics.painter.Painter?;coil3.request.ErrorResult){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Error.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Error.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Error.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Loading : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Loading|null[0]
|
||||||
|
constructor <init>(androidx.compose.ui.graphics.painter/Painter?) // coil3.compose/AsyncImagePainter.State.Loading.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?){}[0]
|
||||||
|
|
||||||
|
final val painter // coil3.compose/AsyncImagePainter.State.Loading.painter|{}painter[0]
|
||||||
|
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Loading.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Loading.component1|component1(){}[0]
|
||||||
|
final fun copy(androidx.compose.ui.graphics.painter/Painter? = ...): coil3.compose/AsyncImagePainter.State.Loading // coil3.compose/AsyncImagePainter.State.Loading.copy|copy(androidx.compose.ui.graphics.painter.Painter?){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Loading.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Loading.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Loading.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Success : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Success|null[0]
|
||||||
|
constructor <init>(androidx.compose.ui.graphics.painter/Painter, coil3.request/SuccessResult) // coil3.compose/AsyncImagePainter.State.Success.<init>|<init>(androidx.compose.ui.graphics.painter.Painter;coil3.request.SuccessResult){}[0]
|
||||||
|
|
||||||
|
final val painter // coil3.compose/AsyncImagePainter.State.Success.painter|{}painter[0]
|
||||||
|
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter // coil3.compose/AsyncImagePainter.State.Success.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
final val result // coil3.compose/AsyncImagePainter.State.Success.result|{}result[0]
|
||||||
|
final fun <get-result>(): coil3.request/SuccessResult // coil3.compose/AsyncImagePainter.State.Success.result.<get-result>|<get-result>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): androidx.compose.ui.graphics.painter/Painter // coil3.compose/AsyncImagePainter.State.Success.component1|component1(){}[0]
|
||||||
|
final fun component2(): coil3.request/SuccessResult // coil3.compose/AsyncImagePainter.State.Success.component2|component2(){}[0]
|
||||||
|
final fun copy(androidx.compose.ui.graphics.painter/Painter = ..., coil3.request/SuccessResult = ...): coil3.compose/AsyncImagePainter.State.Success // coil3.compose/AsyncImagePainter.State.Success.copy|copy(androidx.compose.ui.graphics.painter.Painter;coil3.request.SuccessResult){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Success.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Success.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Success.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Empty : coil3.compose/AsyncImagePainter.State { // coil3.compose/AsyncImagePainter.State.Empty|null[0]
|
||||||
|
final val painter // coil3.compose/AsyncImagePainter.State.Empty.painter|{}painter[0]
|
||||||
|
final fun <get-painter>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/AsyncImagePainter.State.Empty.painter.<get-painter>|<get-painter>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.State.Empty.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.State.Empty.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.compose/AsyncImagePainter.State.Empty.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Input { // coil3.compose/AsyncImagePainter.Input|null[0]
|
||||||
|
constructor <init>(coil3/ImageLoader, coil3.request/ImageRequest, coil3.compose/AsyncImageModelEqualityDelegate) // coil3.compose/AsyncImagePainter.Input.<init>|<init>(coil3.ImageLoader;coil3.request.ImageRequest;coil3.compose.AsyncImageModelEqualityDelegate){}[0]
|
||||||
|
|
||||||
|
final val imageLoader // coil3.compose/AsyncImagePainter.Input.imageLoader|{}imageLoader[0]
|
||||||
|
final fun <get-imageLoader>(): coil3/ImageLoader // coil3.compose/AsyncImagePainter.Input.imageLoader.<get-imageLoader>|<get-imageLoader>(){}[0]
|
||||||
|
final val modelEqualityDelegate // coil3.compose/AsyncImagePainter.Input.modelEqualityDelegate|{}modelEqualityDelegate[0]
|
||||||
|
final fun <get-modelEqualityDelegate>(): coil3.compose/AsyncImageModelEqualityDelegate // coil3.compose/AsyncImagePainter.Input.modelEqualityDelegate.<get-modelEqualityDelegate>|<get-modelEqualityDelegate>(){}[0]
|
||||||
|
final val request // coil3.compose/AsyncImagePainter.Input.request|{}request[0]
|
||||||
|
final fun <get-request>(): coil3.request/ImageRequest // coil3.compose/AsyncImagePainter.Input.request.<get-request>|<get-request>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.compose/AsyncImagePainter.Input.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.compose/AsyncImagePainter.Input.hashCode|hashCode(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Companion { // coil3.compose/AsyncImagePainter.Companion|null[0]
|
||||||
|
final val DefaultTransform // coil3.compose/AsyncImagePainter.Companion.DefaultTransform|{}DefaultTransform[0]
|
||||||
|
final fun <get-DefaultTransform>(): kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State> // coil3.compose/AsyncImagePainter.Companion.DefaultTransform.<get-DefaultTransform>|<get-DefaultTransform>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.compose/ConstraintsSizeResolver : androidx.compose.ui.layout/LayoutModifier, coil3.size/SizeResolver { // coil3.compose/ConstraintsSizeResolver|null[0]
|
||||||
|
constructor <init>() // coil3.compose/ConstraintsSizeResolver.<init>|<init>(){}[0]
|
||||||
|
|
||||||
|
final fun (androidx.compose.ui.layout/MeasureScope).measure(androidx.compose.ui.layout/Measurable, androidx.compose.ui.unit/Constraints): androidx.compose.ui.layout/MeasureResult // coil3.compose/ConstraintsSizeResolver.measure|measure@androidx.compose.ui.layout.MeasureScope(androidx.compose.ui.layout.Measurable;androidx.compose.ui.unit.Constraints){}[0]
|
||||||
|
final fun setConstraints(androidx.compose.ui.unit/Constraints) // coil3.compose/ConstraintsSizeResolver.setConstraints|setConstraints(androidx.compose.ui.unit.Constraints){}[0]
|
||||||
|
final suspend fun size(): coil3.size/Size // coil3.compose/ConstraintsSizeResolver.size|size(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.compose/CrossfadePainter : androidx.compose.ui.graphics.painter/Painter { // coil3.compose/CrossfadePainter|null[0]
|
||||||
|
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.layout/ContentScale = ..., kotlin.time/Duration = ..., kotlin.time/TimeSource = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.compose/CrossfadePainter.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.layout.ContentScale;kotlin.time.Duration;kotlin.time.TimeSource;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
constructor <init>(androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.layout/ContentScale = ..., kotlin.time/Duration = ..., kotlin.time/TimeSource = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.compose/CrossfadePainter.<init>|<init>(androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.layout.ContentScale;kotlin.time.Duration;kotlin.time.TimeSource;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
|
||||||
|
final val contentScale // coil3.compose/CrossfadePainter.contentScale|{}contentScale[0]
|
||||||
|
final fun <get-contentScale>(): androidx.compose.ui.layout/ContentScale // coil3.compose/CrossfadePainter.contentScale.<get-contentScale>|<get-contentScale>(){}[0]
|
||||||
|
final val duration // coil3.compose/CrossfadePainter.duration|{}duration[0]
|
||||||
|
final fun <get-duration>(): kotlin.time/Duration // coil3.compose/CrossfadePainter.duration.<get-duration>|<get-duration>(){}[0]
|
||||||
|
final val end // coil3.compose/CrossfadePainter.end|{}end[0]
|
||||||
|
final fun <get-end>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/CrossfadePainter.end.<get-end>|<get-end>(){}[0]
|
||||||
|
final val fadeStart // coil3.compose/CrossfadePainter.fadeStart|{}fadeStart[0]
|
||||||
|
final fun <get-fadeStart>(): kotlin/Boolean // coil3.compose/CrossfadePainter.fadeStart.<get-fadeStart>|<get-fadeStart>(){}[0]
|
||||||
|
final val intrinsicSize // coil3.compose/CrossfadePainter.intrinsicSize|{}intrinsicSize[0]
|
||||||
|
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/CrossfadePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
|
||||||
|
final val preferEndFirstIntrinsicSize // coil3.compose/CrossfadePainter.preferEndFirstIntrinsicSize|{}preferEndFirstIntrinsicSize[0]
|
||||||
|
final fun <get-preferEndFirstIntrinsicSize>(): kotlin/Boolean // coil3.compose/CrossfadePainter.preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>(){}[0]
|
||||||
|
final val preferExactIntrinsicSize // coil3.compose/CrossfadePainter.preferExactIntrinsicSize|{}preferExactIntrinsicSize[0]
|
||||||
|
final fun <get-preferExactIntrinsicSize>(): kotlin/Boolean // coil3.compose/CrossfadePainter.preferExactIntrinsicSize.<get-preferExactIntrinsicSize>|<get-preferExactIntrinsicSize>(){}[0]
|
||||||
|
final val timeSource // coil3.compose/CrossfadePainter.timeSource|{}timeSource[0]
|
||||||
|
final fun <get-timeSource>(): kotlin.time/TimeSource // coil3.compose/CrossfadePainter.timeSource.<get-timeSource>|<get-timeSource>(){}[0]
|
||||||
|
|
||||||
|
final var start // coil3.compose/CrossfadePainter.start|{}start[0]
|
||||||
|
final fun <get-start>(): androidx.compose.ui.graphics.painter/Painter? // coil3.compose/CrossfadePainter.start.<get-start>|<get-start>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.compose/ImagePainter : androidx.compose.ui.graphics.painter/Painter { // coil3.compose/ImagePainter|null[0]
|
||||||
|
constructor <init>(coil3/Image) // coil3.compose/ImagePainter.<init>|<init>(coil3.Image){}[0]
|
||||||
|
|
||||||
|
final val image // coil3.compose/ImagePainter.image|{}image[0]
|
||||||
|
final fun <get-image>(): coil3/Image // coil3.compose/ImagePainter.image.<get-image>|<get-image>(){}[0]
|
||||||
|
final val intrinsicSize // coil3.compose/ImagePainter.intrinsicSize|{}intrinsicSize[0]
|
||||||
|
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // coil3.compose/ImagePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final val coil3.compose/LocalAsyncImageModelEqualityDelegate // coil3.compose/LocalAsyncImageModelEqualityDelegate|{}LocalAsyncImageModelEqualityDelegate[0]
|
||||||
|
final fun <get-LocalAsyncImageModelEqualityDelegate>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3.compose/AsyncImageModelEqualityDelegate> // coil3.compose/LocalAsyncImageModelEqualityDelegate.<get-LocalAsyncImageModelEqualityDelegate>|<get-LocalAsyncImageModelEqualityDelegate>(){}[0]
|
||||||
|
final val coil3.compose/LocalAsyncImagePreviewHandler // coil3.compose/LocalAsyncImagePreviewHandler|{}LocalAsyncImagePreviewHandler[0]
|
||||||
|
final fun <get-LocalAsyncImagePreviewHandler>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3.compose/AsyncImagePreviewHandler> // coil3.compose/LocalAsyncImagePreviewHandler.<get-LocalAsyncImagePreviewHandler>|<get-LocalAsyncImagePreviewHandler>(){}[0]
|
||||||
|
final val coil3.compose/LocalPlatformContext // coil3.compose/LocalPlatformContext|{}LocalPlatformContext[0]
|
||||||
|
final fun <get-LocalPlatformContext>(): androidx.compose.runtime/ProvidableCompositionLocal<coil3/PlatformContext> // coil3.compose/LocalPlatformContext.<get-LocalPlatformContext>|<get-LocalPlatformContext>(){}[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter$stableprop // coil3.compose/coil3_compose_AsyncImagePainter$stableprop|#static{}coil3_compose_AsyncImagePainter$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop|#static{}coil3_compose_AsyncImagePainter_Input$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Empty$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Error$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Loading$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop // coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop|#static{}coil3_compose_AsyncImagePainter_State_Success$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop // coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop|#static{}coil3_compose_ConstraintsSizeResolver$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_CrossfadePainter$stableprop // coil3.compose/coil3_compose_CrossfadePainter$stableprop|#static{}coil3_compose_CrossfadePainter$stableprop[0]
|
||||||
|
final val coil3.compose/coil3_compose_ImagePainter$stableprop // coil3.compose/coil3_compose_ImagePainter$stableprop|#static{}coil3_compose_ImagePainter$stableprop[0]
|
||||||
|
final val coil3.compose/preferEndFirstIntrinsicSize // coil3.compose/preferEndFirstIntrinsicSize|@coil3.Extras.Key.Companion{}preferEndFirstIntrinsicSize[0]
|
||||||
|
final fun (coil3/Extras.Key.Companion).<get-preferEndFirstIntrinsicSize>(): coil3/Extras.Key<kotlin/Boolean> // coil3.compose/preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>@coil3.Extras.Key.Companion(){}[0]
|
||||||
|
final val coil3.compose/preferEndFirstIntrinsicSize // coil3.compose/preferEndFirstIntrinsicSize|@coil3.request.ImageRequest{}preferEndFirstIntrinsicSize[0]
|
||||||
|
final fun (coil3.request/ImageRequest).<get-preferEndFirstIntrinsicSize>(): kotlin/Boolean // coil3.compose/preferEndFirstIntrinsicSize.<get-preferEndFirstIntrinsicSize>|<get-preferEndFirstIntrinsicSize>@coil3.request.ImageRequest(){}[0]
|
||||||
|
final val coil3.compose/useExistingImageAsPlaceholder // coil3.compose/useExistingImageAsPlaceholder|@coil3.Extras.Key.Companion{}useExistingImageAsPlaceholder[0]
|
||||||
|
final fun (coil3/Extras.Key.Companion).<get-useExistingImageAsPlaceholder>(): coil3/Extras.Key<kotlin/Boolean> // coil3.compose/useExistingImageAsPlaceholder.<get-useExistingImageAsPlaceholder>|<get-useExistingImageAsPlaceholder>@coil3.Extras.Key.Companion(){}[0]
|
||||||
|
final val coil3.compose/useExistingImageAsPlaceholder // coil3.compose/useExistingImageAsPlaceholder|@coil3.request.ImageRequest{}useExistingImageAsPlaceholder[0]
|
||||||
|
final fun (coil3.request/ImageRequest).<get-useExistingImageAsPlaceholder>(): kotlin/Boolean // coil3.compose/useExistingImageAsPlaceholder.<get-useExistingImageAsPlaceholder>|<get-useExistingImageAsPlaceholder>@coil3.request.ImageRequest(){}[0]
|
||||||
|
|
||||||
|
final fun (coil3.compose/SubcomposeAsyncImageScope).coil3.compose/SubcomposeAsyncImageContent(androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, kotlin/String?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImageContent|SubcomposeAsyncImageContent@coil3.compose.SubcomposeAsyncImageScope(androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;kotlin.String?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun (coil3.request/ImageRequest.Builder).coil3.compose/preferEndFirstIntrinsicSize(kotlin/Boolean): coil3.request/ImageRequest.Builder // coil3.compose/preferEndFirstIntrinsicSize|preferEndFirstIntrinsicSize@coil3.request.ImageRequest.Builder(kotlin.Boolean){}[0]
|
||||||
|
final fun (coil3.request/ImageRequest.Builder).coil3.compose/useExistingImageAsPlaceholder(kotlin/Boolean): coil3.request/ImageRequest.Builder // coil3.compose/useExistingImageAsPlaceholder|useExistingImageAsPlaceholder@coil3.request.ImageRequest.Builder(kotlin.Boolean){}[0]
|
||||||
|
final fun (coil3/Image).coil3.compose/asPainter(coil3/PlatformContext, androidx.compose.ui.graphics/FilterQuality = ...): androidx.compose.ui.graphics.painter/Painter // coil3.compose/asPainter|asPainter@coil3.Image(coil3.PlatformContext;androidx.compose.ui.graphics.FilterQuality){}[0]
|
||||||
|
final fun (coil3/ImageLoader.Builder).coil3.compose/preferEndFirstIntrinsicSize(kotlin/Boolean): coil3/ImageLoader.Builder // coil3.compose/preferEndFirstIntrinsicSize|preferEndFirstIntrinsicSize@coil3.ImageLoader.Builder(kotlin.Boolean){}[0]
|
||||||
|
final fun (coil3/ImageLoader.Builder).coil3.compose/useExistingImageAsPlaceholder(kotlin/Boolean): coil3/ImageLoader.Builder // coil3.compose/useExistingImageAsPlaceholder|useExistingImageAsPlaceholder@coil3.ImageLoader.Builder(kotlin.Boolean){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/DrawScopeSizeResolver(): coil3.compose/DrawScopeSizeResolver // coil3.compose/DrawScopeSizeResolver|DrawScopeSizeResolver(){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, coil3/ImageLoader, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;coil3.ImageLoader;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter$stableprop_getter|coil3_compose_AsyncImagePainter$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_Input$stableprop_getter|coil3_compose_AsyncImagePainter_Input$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter|coil3_compose_AsyncImagePainter_State_Empty$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Error$stableprop_getter|coil3_compose_AsyncImagePainter_State_Error$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter|coil3_compose_AsyncImagePainter_State_Loading$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_AsyncImagePainter_State_Success$stableprop_getter|coil3_compose_AsyncImagePainter_State_Success$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_ConstraintsSizeResolver$stableprop_getter|coil3_compose_ConstraintsSizeResolver$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_CrossfadePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_CrossfadePainter$stableprop_getter|coil3_compose_CrossfadePainter$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/coil3_compose_ImagePainter$stableprop_getter(): kotlin/Int // coil3.compose/coil3_compose_ImagePainter$stableprop_getter|coil3_compose_ImagePainter$stableprop_getter(){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, coil3/ImageLoader, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;coil3.ImageLoader;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberConstraintsSizeResolver(androidx.compose.runtime/Composer?, kotlin/Int): coil3.compose/ConstraintsSizeResolver // coil3.compose/rememberConstraintsSizeResolver|rememberConstraintsSizeResolver(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberDrawScopeSizeResolver(androidx.compose.runtime/Composer?, kotlin/Int): coil3.compose/DrawScopeSizeResolver // coil3.compose/rememberDrawScopeSizeResolver|rememberDrawScopeSizeResolver(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final inline fun coil3.compose/AsyncImagePreviewHandler(crossinline kotlin.coroutines/SuspendFunction1<coil3.request/ImageRequest, coil3/Image>): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler|AsyncImagePreviewHandler(kotlin.coroutines.SuspendFunction1<coil3.request.ImageRequest,coil3.Image>){}[0]
|
||||||
|
final inline fun coil3.compose/AsyncImagePreviewHandler(crossinline kotlin.coroutines/SuspendFunction1<coil3.request/ImageRequest, coil3/Image?>): coil3.compose/AsyncImagePreviewHandler // coil3.compose/AsyncImagePreviewHandler|AsyncImagePreviewHandler(kotlin.coroutines.SuspendFunction1<coil3.request.ImageRequest,coil3.Image?>){}[0]
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// :coil:coil-compose-core - AsyncImage / rememberAsyncImagePainter internals, vendored
|
||||||
|
// VERBATIM from coil-kt/coil. Upstream publishes NO desktop-native klibs for the
|
||||||
|
// compose layer at all (neither mingwX64 nor linux), so this whole module exists to
|
||||||
|
// give the port its targets. Compose coords substitute to the port's project modules
|
||||||
|
// on every native configuration (root build's commonization bridge).
|
||||||
|
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(project(":coil:coil-core"))
|
||||||
|
// Declared DIRECTLY, not leaned on transitively: these coords are
|
||||||
|
// SUBSTITUTED to the port's project modules on native configurations,
|
||||||
|
// and KGP's granular-metadata visibility rule then drops the substituted
|
||||||
|
// module's transitives from the commonMain metadata classpath (CLAUDE.md
|
||||||
|
// "Common pitfalls"). Every artifact the vendored common code touches
|
||||||
|
// needs its own declaration + its own bridge rule.
|
||||||
|
api("org.jetbrains.compose.runtime:runtime:${libs.versions.composeRuntime.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui-graphics:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui-text:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui-unit:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui-geometry:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.ui:ui-util:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.foundation:foundation:${libs.versions.compose.get()}")
|
||||||
|
api("org.jetbrains.compose.foundation:foundation-layout:${libs.versions.compose.get()}")
|
||||||
|
}
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
// Upstream's nonAndroid compose layer reaches straight into org.jetbrains.skia
|
||||||
|
// (Canvas / Bitmap.asComposeImageBitmap), so it needs skiko on its own compile
|
||||||
|
// classpath - official for macOS/Linux, the bitsycore FORK for mingwX64, on two
|
||||||
|
// PARALLEL branches. See coil/coil-core/build.gradle.kts.
|
||||||
|
fun nonAndroidBranch(inName: String, inSkiko: Any) = create("${inName}NonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
dependencies { implementation(inSkiko) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val vOfficial = nonAndroidBranch("coilComposeOfficial", libs.skiko)
|
||||||
|
get("macosArm64Main").dependsOn(vOfficial)
|
||||||
|
get("linuxX64Main").dependsOn(vOfficial)
|
||||||
|
get("linuxArm64Main").dependsOn(vOfficial)
|
||||||
|
|
||||||
|
if (vHostSupportsMingw) {
|
||||||
|
val vFork = nonAndroidBranch(
|
||||||
|
"coilComposeFork",
|
||||||
|
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
|
||||||
|
)
|
||||||
|
get("mingwX64Main").dependsOn(vFork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# :coil:coil-compose-core - AsyncImage / rememberAsyncImagePainter internals.
|
||||||
|
# Upstream publishes NEITHER mingwX64 NOR linux for the compose layer.
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-compose-core/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/compose/AsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/AsyncImage.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/AsyncImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/AsyncImagePainter.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/ConstraintsSizeResolver.kt -> src/vendor/common/kotlin/coil3/compose/ConstraintsSizeResolver.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/CrossfadePainter.kt -> src/vendor/common/kotlin/coil3/compose/CrossfadePainter.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/DrawScopeSizeResolver.kt -> src/vendor/common/kotlin/coil3/compose/DrawScopeSizeResolver.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/ImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/ImagePainter.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/LocalAsyncImageModelEqualityDelegate.kt -> src/vendor/common/kotlin/coil3/compose/LocalAsyncImageModelEqualityDelegate.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt -> src/vendor/common/kotlin/coil3/compose/LocalAsyncImagePreviewHandler.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/LocalPlatformContext.kt -> src/vendor/common/kotlin/coil3/compose/LocalPlatformContext.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/SubcomposeAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SubcomposeAsyncImage.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/imageRequests.kt -> src/vendor/common/kotlin/coil3/compose/imageRequests.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/internal/ContentPainterModifier.kt -> src/vendor/common/kotlin/coil3/compose/internal/ContentPainterModifier.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/internal/DeferredDispatch.kt -> src/vendor/common/kotlin/coil3/compose/internal/DeferredDispatch.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt -> src/vendor/common/kotlin/coil3/compose/internal/ForwardingCoroutineContext.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/internal/utils.kt -> src/vendor/common/kotlin/coil3/compose/internal/utils.kt
|
||||||
|
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonAndroidMain/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/AsyncImagePainter.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/compose/ImagePainter.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/ImagePainter.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/compose/LocalPlatformContext.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/LocalPlatformContext.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/compose/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/compose/internal/utils.nonAndroid.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# androidMain (4)
|
||||||
|
# | androidMain/kotlin/coil3/compose/AsyncImagePainter.android.kt -> src/vendor/android/kotlin/coil3/compose/AsyncImagePainter.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/compose/ImagePainter.android.kt -> src/vendor/android/kotlin/coil3/compose/ImagePainter.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/compose/LocalPlatformContext.android.kt -> src/vendor/android/kotlin/coil3/compose/LocalPlatformContext.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/compose/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/compose/internal/utils.android.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 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.coil3:coil-compose>
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/AsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/AsyncImage|AsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, kotlin/Function3<coil3.compose/SubcomposeAsyncImageScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;kotlin.Function3<coil3.compose.SubcomposeAsyncImageScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/SubcomposeAsyncImage(kotlin/Any?, kotlin/String?, androidx.compose.ui/Modifier?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Loading, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Success, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<coil3.compose/SubcomposeAsyncImageScope, coil3.compose/AsyncImagePainter.State.Error, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // coil3.compose/SubcomposeAsyncImage|SubcomposeAsyncImage(kotlin.Any?;kotlin.String?;androidx.compose.ui.Modifier?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Loading,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Success,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<coil3.compose.SubcomposeAsyncImageScope,coil3.compose.AsyncImagePainter.State.Error,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, androidx.compose.ui.graphics.painter/Painter?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Loading, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Success, kotlin/Unit>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State.Error, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;androidx.compose.ui.graphics.painter.Painter?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Loading,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Success,kotlin.Unit>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State.Error,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/rememberAsyncImagePainter(kotlin/Any?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, coil3.compose/AsyncImagePainter.State>?, kotlin/Function1<coil3.compose/AsyncImagePainter.State, kotlin/Unit>?, androidx.compose.ui.layout/ContentScale?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): coil3.compose/AsyncImagePainter // coil3.compose/rememberAsyncImagePainter|rememberAsyncImagePainter(kotlin.Any?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,coil3.compose.AsyncImagePainter.State>?;kotlin.Function1<coil3.compose.AsyncImagePainter.State,kotlin.Unit>?;androidx.compose.ui.layout.ContentScale?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun coil3.compose/setSingletonImageLoaderFactory(kotlin/Function1<coil3/PlatformContext, coil3/ImageLoader>, androidx.compose.runtime/Composer?, kotlin/Int) // coil3.compose/setSingletonImageLoaderFactory|setSingletonImageLoaderFactory(kotlin.Function1<coil3.PlatformContext,coil3.ImageLoader>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// :coil:coil-compose - the public compose entry points (AsyncImage, SubcomposeAsyncImage),
|
||||||
|
// vendored VERBATIM from coil-kt/coil. commonMain only. Upstream publishes no
|
||||||
|
// desktop-native klibs for the compose layer.
|
||||||
|
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(project(":coil:coil"))
|
||||||
|
api(project(":coil:coil-compose-core"))
|
||||||
|
}
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# :coil:coil-compose - the public compose entry points. commonMain only.
|
||||||
|
# Upstream publishes NEITHER mingwX64 NOR linux for the compose layer.
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-compose/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/compose/SingletonAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SingletonAsyncImage.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/SingletonAsyncImagePainter.kt -> src/vendor/common/kotlin/coil3/compose/SingletonAsyncImagePainter.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/SingletonSubcomposeAsyncImage.kt -> src/vendor/common/kotlin/coil3/compose/SingletonSubcomposeAsyncImage.kt
|
||||||
|
# | commonMain/kotlin/coil3/compose/singletonImageLoaders.kt -> src/vendor/common/kotlin/coil3/compose/singletonImageLoaders.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# (none - every source set under SET_FOLDER is vendored)
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
|||||||
|
// :coil:coil-core - coil3.* core, vendored VERBATIM from coil-kt/coil into src/vendor.
|
||||||
|
//
|
||||||
|
// Upstream publishes linux + apple klibs but NOT mingwX64, and the code is
|
||||||
|
// target-agnostic, so the port mostly just re-declares the targets.
|
||||||
|
//
|
||||||
|
// SOURCE-SET COLLAPSE: this module has no android, jvm or js target, so upstream's
|
||||||
|
// nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all apply to every
|
||||||
|
// target here and are vendored into ONE tree (src/vendor/native). Only the
|
||||||
|
// apple-vs-rest split survives: appleMain is macosArm64, nonAppleMain is linux+mingw.
|
||||||
|
//
|
||||||
|
// SKIA: upstream's non-android decoder is skiko-based (SkiaImageDecoder), which is
|
||||||
|
// this port's renderer already. macOS/Linux take the official skiko; mingwX64 takes
|
||||||
|
// the bitsycore FORK, the same swap :compose:ui:ui-graphics makes.
|
||||||
|
//
|
||||||
|
// Provenance = compose-fork.txt + scripts/compose-fork/compose.properties. Never
|
||||||
|
// hand-edit src/vendor - change the manifest + re-run
|
||||||
|
// `python scripts/compose-fork/sync.py coil/coil-core`.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(libs.kotlinx.coroutines.core)
|
||||||
|
api(libs.okio)
|
||||||
|
}
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
// SKIKO BRANCHES. Upstream's nonAndroid files use org.jetbrains.skia.*, so the
|
||||||
|
// intermediate source set that holds them needs skiko on its own compile
|
||||||
|
// classpath (the shared-metadata compilation is a real compile). macOS/Linux
|
||||||
|
// take the OFFICIAL skiko; mingwX64 takes the bitsycore FORK - and the two
|
||||||
|
// cannot share an intermediate, so mingw gets a PARALLEL branch over the same
|
||||||
|
// srcDirs. Same shape as :compose:ui:ui-graphics.
|
||||||
|
//
|
||||||
|
// Each branch is two levels deep because upstream's nonAndroid sets declare
|
||||||
|
// `expect`s whose `actual`s live in nativeMain - siblings in one set is an error.
|
||||||
|
fun skikoBranch(inName: String, inSkiko: Any): org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet {
|
||||||
|
val vNonAndroid = create("${inName}NonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
dependencies { implementation(inSkiko) }
|
||||||
|
}
|
||||||
|
return create("${inName}NativeMain") {
|
||||||
|
dependsOn(vNonAndroid)
|
||||||
|
kotlin.srcDir("src/vendor/native/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val vOfficial = skikoBranch("coilOfficial", libs.skiko)
|
||||||
|
val vAppleMain = create("coilAppleMain") {
|
||||||
|
dependsOn(vOfficial)
|
||||||
|
kotlin.srcDir("src/vendor/apple/kotlin")
|
||||||
|
}
|
||||||
|
val vLinuxMain = create("coilLinuxMain") {
|
||||||
|
dependsOn(vOfficial)
|
||||||
|
kotlin.srcDir("src/vendor/nonApple/kotlin")
|
||||||
|
}
|
||||||
|
get("macosArm64Main").dependsOn(vAppleMain)
|
||||||
|
get("linuxX64Main").dependsOn(vLinuxMain)
|
||||||
|
get("linuxArm64Main").dependsOn(vLinuxMain)
|
||||||
|
|
||||||
|
if (vHostSupportsMingw) {
|
||||||
|
val vFork = skikoBranch(
|
||||||
|
"coilFork",
|
||||||
|
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
|
||||||
|
)
|
||||||
|
val vMingwMain = create("coilMingwMain") {
|
||||||
|
dependsOn(vFork)
|
||||||
|
kotlin.srcDir("src/vendor/nonApple/kotlin")
|
||||||
|
}
|
||||||
|
get("mingwX64Main").dependsOn(vMingwMain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=kotlinx.cinterop.ExperimentalForeignApi",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# :coil:coil-core - coil3.* core, vendored verbatim from coil-kt/coil.
|
||||||
|
# Upstream publishes linux + apple but NOT mingwX64, so the port re-declares the
|
||||||
|
# targets and rebuilds the same sources. Decoding is Skia-based upstream
|
||||||
|
# (SkiaImageDecoder), which is exactly this port's renderer - on mingwX64 the
|
||||||
|
# skiko dependency is the bitsycore FORK, as in :compose:ui:ui-graphics.
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-core/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/ColorImage.kt -> src/vendor/common/kotlin/coil3/ColorImage.kt
|
||||||
|
# | commonMain/kotlin/coil3/ComponentRegistry.kt -> src/vendor/common/kotlin/coil3/ComponentRegistry.kt
|
||||||
|
# | commonMain/kotlin/coil3/EventListener.kt -> src/vendor/common/kotlin/coil3/EventListener.kt
|
||||||
|
# | commonMain/kotlin/coil3/Extras.kt -> src/vendor/common/kotlin/coil3/Extras.kt
|
||||||
|
# | commonMain/kotlin/coil3/Image.kt -> src/vendor/common/kotlin/coil3/Image.kt
|
||||||
|
# | commonMain/kotlin/coil3/ImageLoader.kt -> src/vendor/common/kotlin/coil3/ImageLoader.kt
|
||||||
|
# | commonMain/kotlin/coil3/PlatformContext.kt -> src/vendor/common/kotlin/coil3/PlatformContext.kt
|
||||||
|
# | commonMain/kotlin/coil3/RealImageLoader.kt -> src/vendor/common/kotlin/coil3/RealImageLoader.kt
|
||||||
|
# | commonMain/kotlin/coil3/Uri.kt -> src/vendor/common/kotlin/coil3/Uri.kt
|
||||||
|
# | commonMain/kotlin/coil3/annotation/annotations.kt -> src/vendor/common/kotlin/coil3/annotation/annotations.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/BlackholeDecoder.kt -> src/vendor/common/kotlin/coil3/decode/BlackholeDecoder.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/DataSource.kt -> src/vendor/common/kotlin/coil3/decode/DataSource.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/DecodeResult.kt -> src/vendor/common/kotlin/coil3/decode/DecodeResult.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/DecodeUtils.kt -> src/vendor/common/kotlin/coil3/decode/DecodeUtils.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/Decoder.kt -> src/vendor/common/kotlin/coil3/decode/Decoder.kt
|
||||||
|
# | commonMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/common/kotlin/coil3/decode/ImageSource.kt
|
||||||
|
# | commonMain/kotlin/coil3/disk/DiskCache.kt -> src/vendor/common/kotlin/coil3/disk/DiskCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/disk/DiskLruCache.kt -> src/vendor/common/kotlin/coil3/disk/DiskLruCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/disk/FaultHidingSink.kt -> src/vendor/common/kotlin/coil3/disk/FaultHidingSink.kt
|
||||||
|
# | commonMain/kotlin/coil3/disk/RealDiskCache.kt -> src/vendor/common/kotlin/coil3/disk/RealDiskCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/disk/utils.kt -> src/vendor/common/kotlin/coil3/disk/utils.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/BitmapFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/BitmapFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/ByteArrayFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/ByteArrayFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/DataUriFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/DataUriFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/FetchResult.kt -> src/vendor/common/kotlin/coil3/fetch/FetchResult.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/Fetcher.kt -> src/vendor/common/kotlin/coil3/fetch/Fetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/fetch/FileUriFetcher.kt -> src/vendor/common/kotlin/coil3/fetch/FileUriFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/imageLoaders.kt -> src/vendor/common/kotlin/coil3/imageLoaders.kt
|
||||||
|
# | commonMain/kotlin/coil3/intercept/EngineInterceptor.kt -> src/vendor/common/kotlin/coil3/intercept/EngineInterceptor.kt
|
||||||
|
# | commonMain/kotlin/coil3/intercept/Interceptor.kt -> src/vendor/common/kotlin/coil3/intercept/Interceptor.kt
|
||||||
|
# | commonMain/kotlin/coil3/intercept/RealInterceptorChain.kt -> src/vendor/common/kotlin/coil3/intercept/RealInterceptorChain.kt
|
||||||
|
# | commonMain/kotlin/coil3/key/FileUriKeyer.kt -> src/vendor/common/kotlin/coil3/key/FileUriKeyer.kt
|
||||||
|
# | commonMain/kotlin/coil3/key/Keyer.kt -> src/vendor/common/kotlin/coil3/key/Keyer.kt
|
||||||
|
# | commonMain/kotlin/coil3/key/UriKeyer.kt -> src/vendor/common/kotlin/coil3/key/UriKeyer.kt
|
||||||
|
# | commonMain/kotlin/coil3/map/Mapper.kt -> src/vendor/common/kotlin/coil3/map/Mapper.kt
|
||||||
|
# | commonMain/kotlin/coil3/map/PathMapper.kt -> src/vendor/common/kotlin/coil3/map/PathMapper.kt
|
||||||
|
# | commonMain/kotlin/coil3/map/StringMapper.kt -> src/vendor/common/kotlin/coil3/map/StringMapper.kt
|
||||||
|
# | commonMain/kotlin/coil3/memory/MemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/MemoryCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/memory/MemoryCacheService.kt -> src/vendor/common/kotlin/coil3/memory/MemoryCacheService.kt
|
||||||
|
# | commonMain/kotlin/coil3/memory/RealMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/RealMemoryCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/memory/StrongMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/StrongMemoryCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/memory/WeakMemoryCache.kt -> src/vendor/common/kotlin/coil3/memory/WeakMemoryCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/CachePolicy.kt -> src/vendor/common/kotlin/coil3/request/CachePolicy.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/Disposable.kt -> src/vendor/common/kotlin/coil3/request/Disposable.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/ImageRequest.kt -> src/vendor/common/kotlin/coil3/request/ImageRequest.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/ImageResult.kt -> src/vendor/common/kotlin/coil3/request/ImageResult.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/NullRequestData.kt -> src/vendor/common/kotlin/coil3/request/NullRequestData.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/NullRequestDataException.kt -> src/vendor/common/kotlin/coil3/request/NullRequestDataException.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/Options.kt -> src/vendor/common/kotlin/coil3/request/Options.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/RequestDelegate.kt -> src/vendor/common/kotlin/coil3/request/RequestDelegate.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/RequestService.kt -> src/vendor/common/kotlin/coil3/request/RequestService.kt
|
||||||
|
# | commonMain/kotlin/coil3/request/imageRequests.kt -> src/vendor/common/kotlin/coil3/request/imageRequests.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/Dimension.kt -> src/vendor/common/kotlin/coil3/size/Dimension.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/Precision.kt -> src/vendor/common/kotlin/coil3/size/Precision.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/RealSizeResolver.kt -> src/vendor/common/kotlin/coil3/size/RealSizeResolver.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/Scale.kt -> src/vendor/common/kotlin/coil3/size/Scale.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/Size.kt -> src/vendor/common/kotlin/coil3/size/Size.kt
|
||||||
|
# | commonMain/kotlin/coil3/size/SizeResolver.kt -> src/vendor/common/kotlin/coil3/size/SizeResolver.kt
|
||||||
|
# | commonMain/kotlin/coil3/target/Target.kt -> src/vendor/common/kotlin/coil3/target/Target.kt
|
||||||
|
# | commonMain/kotlin/coil3/transform/Transformation.kt -> src/vendor/common/kotlin/coil3/transform/Transformation.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/LruCache.kt -> src/vendor/common/kotlin/coil3/util/LruCache.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/common/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/common/kotlin/coil3/util/SystemCallbacks.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/collections.kt -> src/vendor/common/kotlin/coil3/util/collections.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/contexts.kt -> src/vendor/common/kotlin/coil3/util/contexts.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/coroutines.kt -> src/vendor/common/kotlin/coil3/util/coroutines.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/fileSystems.kt -> src/vendor/common/kotlin/coil3/util/fileSystems.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/logging.kt -> src/vendor/common/kotlin/coil3/util/logging.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/mimeTypes.kt -> src/vendor/common/kotlin/coil3/util/mimeTypes.kt
|
||||||
|
# | commonMain/kotlin/coil3/util/utils.kt -> src/vendor/common/kotlin/coil3/util/utils.kt
|
||||||
|
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonAndroidMain/kotlin/coil3/ColorImage.kt -> src/vendor/nonAndroid/kotlin/coil3/ColorImage.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/EventListener.kt -> src/vendor/nonAndroid/kotlin/coil3/EventListener.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/Image.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/Image.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/PlatformContext.kt -> src/vendor/nonAndroid/kotlin/coil3/PlatformContext.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/RealImageLoader.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/RealImageLoader.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/decode/SkiaImageDecoder.kt -> src/vendor/nonAndroid/kotlin/coil3/decode/SkiaImageDecoder.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/intercept/EngineInterceptor.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/intercept/EngineInterceptor.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/request/RequestService.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/request/RequestService.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/request/imageRequests.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/request/imageRequests.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/nonAndroid/kotlin/coil3/util/SystemCallbacks.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/util/contexts.kt -> src/vendor/nonAndroid/kotlin/coil3/util/contexts.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/util/mimeTypes.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/util/mimeTypes.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/util/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/util/utils.nonAndroid.kt
|
||||||
|
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/RealImageLoader.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/RealImageLoader.nonJvmCommon.kt
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/nonAndroid/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/util/collections.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/collections.nonJvmCommon.kt
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/util/contexts.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/contexts.nonJvmCommon.kt
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/util/fileSystems.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/fileSystems.nonJvmCommon.kt
|
||||||
|
nonJsCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonJsCommonMain/kotlin/coil3/disk/utils.kt -> src/vendor/nonAndroid/kotlin/coil3/disk/utils.kt
|
||||||
|
# | nonJsCommonMain/kotlin/coil3/imageLoaders.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/imageLoaders.nonJsCommon.kt
|
||||||
|
# | nonJsCommonMain/kotlin/coil3/util/coroutines.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/coroutines.nonJsCommon.kt
|
||||||
|
# | nonJsCommonMain/kotlin/coil3/util/fileSystems.nonJsCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/util/fileSystems.nonJsCommon.kt
|
||||||
|
nativeMain/kotlin/ -> src/vendor/native/kotlin/
|
||||||
|
# | nativeMain/kotlin/coil3/decode/SkiaImageDecoder.native.kt -> src/vendor/native/kotlin/coil3/decode/SkiaImageDecoder.native.kt
|
||||||
|
# | nativeMain/kotlin/coil3/util/utils.native.kt -> src/vendor/native/kotlin/coil3/util/utils.native.kt
|
||||||
|
appleMain/kotlin/ -> src/vendor/apple/kotlin/
|
||||||
|
# | appleMain/kotlin/coil3/RealImageLoader.apple.kt -> src/vendor/apple/kotlin/coil3/RealImageLoader.apple.kt
|
||||||
|
# | appleMain/kotlin/coil3/map/NSURLMapper.kt -> src/vendor/apple/kotlin/coil3/map/NSURLMapper.kt
|
||||||
|
nonAppleMain/kotlin/ -> src/vendor/nonApple/kotlin/
|
||||||
|
# | nonAppleMain/kotlin/coil3/RealImageLoader.nonApple.kt -> src/vendor/nonApple/kotlin/coil3/RealImageLoader.nonApple.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# androidMain (49)
|
||||||
|
# | androidMain/kotlin/coil3/ColorImage.kt -> src/vendor/android/kotlin/coil3/ColorImage.kt
|
||||||
|
# | androidMain/kotlin/coil3/EventListener.kt -> src/vendor/android/kotlin/coil3/EventListener.kt
|
||||||
|
# | androidMain/kotlin/coil3/Image.android.kt -> src/vendor/android/kotlin/coil3/Image.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/PlatformContext.kt -> src/vendor/android/kotlin/coil3/PlatformContext.kt
|
||||||
|
# | androidMain/kotlin/coil3/RealImageLoader.android.kt -> src/vendor/android/kotlin/coil3/RealImageLoader.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/Uri.android.kt -> src/vendor/android/kotlin/coil3/Uri.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/decode/BitmapFactoryDecoder.kt -> src/vendor/android/kotlin/coil3/decode/BitmapFactoryDecoder.kt
|
||||||
|
# | androidMain/kotlin/coil3/decode/ExifOrientationStrategy.kt -> src/vendor/android/kotlin/coil3/decode/ExifOrientationStrategy.kt
|
||||||
|
# | androidMain/kotlin/coil3/decode/ExifUtils.kt -> src/vendor/android/kotlin/coil3/decode/ExifUtils.kt
|
||||||
|
# | androidMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/android/kotlin/coil3/decode/ImageSource.kt
|
||||||
|
# | androidMain/kotlin/coil3/decode/StaticImageDecoder.kt -> src/vendor/android/kotlin/coil3/decode/StaticImageDecoder.kt
|
||||||
|
# | androidMain/kotlin/coil3/fetch/AssetUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/AssetUriFetcher.kt
|
||||||
|
# | androidMain/kotlin/coil3/fetch/ContentUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/ContentUriFetcher.kt
|
||||||
|
# | androidMain/kotlin/coil3/fetch/DrawableFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/DrawableFetcher.kt
|
||||||
|
# | androidMain/kotlin/coil3/fetch/ResourceUriFetcher.kt -> src/vendor/android/kotlin/coil3/fetch/ResourceUriFetcher.kt
|
||||||
|
# | androidMain/kotlin/coil3/imageLoaders.android.kt -> src/vendor/android/kotlin/coil3/imageLoaders.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/intercept/EngineInterceptor.android.kt -> src/vendor/android/kotlin/coil3/intercept/EngineInterceptor.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/key/AndroidResourceUriKeyer.kt -> src/vendor/android/kotlin/coil3/key/AndroidResourceUriKeyer.kt
|
||||||
|
# | androidMain/kotlin/coil3/map/AndroidUriMapper.kt -> src/vendor/android/kotlin/coil3/map/AndroidUriMapper.kt
|
||||||
|
# | androidMain/kotlin/coil3/map/ResourceIntMapper.kt -> src/vendor/android/kotlin/coil3/map/ResourceIntMapper.kt
|
||||||
|
# | androidMain/kotlin/coil3/request/RequestDelegate.android.kt -> src/vendor/android/kotlin/coil3/request/RequestDelegate.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/request/RequestService.android.kt -> src/vendor/android/kotlin/coil3/request/RequestService.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/request/ViewTargetDisposable.kt -> src/vendor/android/kotlin/coil3/request/ViewTargetDisposable.kt
|
||||||
|
# | androidMain/kotlin/coil3/request/ViewTargetRequestManager.kt -> src/vendor/android/kotlin/coil3/request/ViewTargetRequestManager.kt
|
||||||
|
# | androidMain/kotlin/coil3/request/imageRequests.android.kt -> src/vendor/android/kotlin/coil3/request/imageRequests.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/size/RealViewSizeResolver.kt -> src/vendor/android/kotlin/coil3/size/RealViewSizeResolver.kt
|
||||||
|
# | androidMain/kotlin/coil3/size/ScaleDrawable.kt -> src/vendor/android/kotlin/coil3/size/ScaleDrawable.kt
|
||||||
|
# | androidMain/kotlin/coil3/size/ViewSizeResolver.kt -> src/vendor/android/kotlin/coil3/size/ViewSizeResolver.kt
|
||||||
|
# | androidMain/kotlin/coil3/target/GenericViewTarget.kt -> src/vendor/android/kotlin/coil3/target/GenericViewTarget.kt
|
||||||
|
# | androidMain/kotlin/coil3/target/ImageViewTarget.kt -> src/vendor/android/kotlin/coil3/target/ImageViewTarget.kt
|
||||||
|
# | androidMain/kotlin/coil3/target/ViewTarget.kt -> src/vendor/android/kotlin/coil3/target/ViewTarget.kt
|
||||||
|
# | androidMain/kotlin/coil3/transform/CircleCropTransformation.kt -> src/vendor/android/kotlin/coil3/transform/CircleCropTransformation.kt
|
||||||
|
# | androidMain/kotlin/coil3/transform/RoundedCornersTransformation.kt -> src/vendor/android/kotlin/coil3/transform/RoundedCornersTransformation.kt
|
||||||
|
# | androidMain/kotlin/coil3/transform/transformations.kt -> src/vendor/android/kotlin/coil3/transform/transformations.kt
|
||||||
|
# | androidMain/kotlin/coil3/transition/CrossfadeDrawable.kt -> src/vendor/android/kotlin/coil3/transition/CrossfadeDrawable.kt
|
||||||
|
# | androidMain/kotlin/coil3/transition/CrossfadeTransition.kt -> src/vendor/android/kotlin/coil3/transition/CrossfadeTransition.kt
|
||||||
|
# | androidMain/kotlin/coil3/transition/NoneTransition.kt -> src/vendor/android/kotlin/coil3/transition/NoneTransition.kt
|
||||||
|
# | androidMain/kotlin/coil3/transition/Transition.kt -> src/vendor/android/kotlin/coil3/transition/Transition.kt
|
||||||
|
# | androidMain/kotlin/coil3/transition/TransitionTarget.kt -> src/vendor/android/kotlin/coil3/transition/TransitionTarget.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/CoilUtils.kt -> src/vendor/android/kotlin/coil3/util/CoilUtils.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/DrawableUtils.kt -> src/vendor/android/kotlin/coil3/util/DrawableUtils.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/SystemCallbacks.kt -> src/vendor/android/kotlin/coil3/util/SystemCallbacks.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/bitmaps.kt -> src/vendor/android/kotlin/coil3/util/bitmaps.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/contexts.kt -> src/vendor/android/kotlin/coil3/util/contexts.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/fileSystems.android.kt -> src/vendor/android/kotlin/coil3/util/fileSystems.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/hardwareBitmaps.kt -> src/vendor/android/kotlin/coil3/util/hardwareBitmaps.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/lifecycles.kt -> src/vendor/android/kotlin/coil3/util/lifecycles.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/mimeTypes.android.kt -> src/vendor/android/kotlin/coil3/util/mimeTypes.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/util/utils.android.kt -> src/vendor/android/kotlin/coil3/util/utils.android.kt
|
||||||
|
# jsCommonMain (7)
|
||||||
|
# | jsCommonMain/kotlin/coil3/decode/SkiaImageDecoder.jsCommon.kt -> src/vendor/jsCommon/kotlin/coil3/decode/SkiaImageDecoder.jsCommon.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/decode/WebWorker.kt -> src/vendor/jsCommon/kotlin/coil3/decode/WebWorker.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/disk/utils.kt -> src/vendor/jsCommon/kotlin/coil3/disk/utils.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/fetch/BlobUriFetcher.kt -> src/vendor/jsCommon/kotlin/coil3/fetch/BlobUriFetcher.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.kt -> src/vendor/jsCommon/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/util/coroutines.kt -> src/vendor/jsCommon/kotlin/coil3/util/coroutines.kt
|
||||||
|
# | jsCommonMain/kotlin/coil3/util/fileSystems.kt -> src/vendor/jsCommon/kotlin/coil3/util/fileSystems.kt
|
||||||
|
# jsMain (3)
|
||||||
|
# | jsMain/kotlin/coil3/fetch/BlobUriFetcher.js.kt -> src/vendor/js/kotlin/coil3/fetch/BlobUriFetcher.js.kt
|
||||||
|
# | jsMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.js.kt
|
||||||
|
# | jsMain/kotlin/coil3/util/utils.js.kt -> src/vendor/js/kotlin/coil3/util/utils.js.kt
|
||||||
|
# jvmCommonMain (10)
|
||||||
|
# | jvmCommonMain/kotlin/coil3/RealImageLoader.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/RealImageLoader.jvmCommon.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/decode/ImageSource.kt -> src/vendor/jvmCommon/kotlin/coil3/decode/ImageSource.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/disk/diskCache.kt -> src/vendor/jvmCommon/kotlin/coil3/disk/diskCache.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/fetch/ByteBufferFetcher.kt -> src/vendor/jvmCommon/kotlin/coil3/fetch/ByteBufferFetcher.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/fetch/JarFileFetcher.kt -> src/vendor/jvmCommon/kotlin/coil3/fetch/JarFileFetcher.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/imageLoaders.hidden.kt -> src/vendor/jvmCommon/kotlin/coil3/imageLoaders.hidden.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/map/FileMapper.kt -> src/vendor/jvmCommon/kotlin/coil3/map/FileMapper.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt -> src/vendor/jvmCommon/kotlin/coil3/util/ServiceLoaderComponentRegistry.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/util/collections.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/util/collections.jvmCommon.kt
|
||||||
|
# | jvmCommonMain/kotlin/coil3/util/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/util/utils.jvmCommon.kt
|
||||||
|
# jvmMain (3)
|
||||||
|
# | jvmMain/kotlin/coil3/decode/SkiaImageDecoder.jvm.kt -> src/vendor/jvm/kotlin/coil3/decode/SkiaImageDecoder.jvm.kt
|
||||||
|
# | jvmMain/kotlin/coil3/util/contexts.jvm.kt -> src/vendor/jvm/kotlin/coil3/util/contexts.jvm.kt
|
||||||
|
# | jvmMain/kotlin/coil3/util/fileSystems.jvm.kt -> src/vendor/jvm/kotlin/coil3/util/fileSystems.jvm.kt
|
||||||
|
# wasmJsMain (3)
|
||||||
|
# | wasmJsMain/kotlin/coil3/fetch/BlobUriFetcher.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/fetch/BlobUriFetcher.wasmJs.kt
|
||||||
|
# | wasmJsMain/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/fetch/internal/BlobUriFetcherServiceLoaderTarget.wasmJs.kt
|
||||||
|
# | wasmJsMain/kotlin/coil3/util/utils.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/util/utils.wasmJs.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// 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.coil3:coil-network-core>
|
||||||
|
abstract fun interface coil3.network/ConnectivityChecker { // coil3.network/ConnectivityChecker|null[0]
|
||||||
|
abstract fun isOnline(): kotlin/Boolean // coil3.network/ConnectivityChecker.isOnline|isOnline(){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.network/ConnectivityChecker.Companion|null[0]
|
||||||
|
final val ONLINE // coil3.network/ConnectivityChecker.Companion.ONLINE|{}ONLINE[0]
|
||||||
|
final fun <get-ONLINE>(): coil3.network/ConnectivityChecker // coil3.network/ConnectivityChecker.Companion.ONLINE.<get-ONLINE>|<get-ONLINE>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.network/CacheStrategy { // coil3.network/CacheStrategy|null[0]
|
||||||
|
abstract suspend fun read(coil3.network/NetworkResponse, coil3.network/NetworkRequest, coil3.request/Options): coil3.network/CacheStrategy.ReadResult // coil3.network/CacheStrategy.read|read(coil3.network.NetworkResponse;coil3.network.NetworkRequest;coil3.request.Options){}[0]
|
||||||
|
abstract suspend fun write(coil3.network/NetworkResponse?, coil3.network/NetworkRequest, coil3.network/NetworkResponse, coil3.request/Options): coil3.network/CacheStrategy.WriteResult // coil3.network/CacheStrategy.write|write(coil3.network.NetworkResponse?;coil3.network.NetworkRequest;coil3.network.NetworkResponse;coil3.request.Options){}[0]
|
||||||
|
|
||||||
|
final class ReadResult { // coil3.network/CacheStrategy.ReadResult|null[0]
|
||||||
|
constructor <init>(coil3.network/NetworkRequest) // coil3.network/CacheStrategy.ReadResult.<init>|<init>(coil3.network.NetworkRequest){}[0]
|
||||||
|
constructor <init>(coil3.network/NetworkResponse) // coil3.network/CacheStrategy.ReadResult.<init>|<init>(coil3.network.NetworkResponse){}[0]
|
||||||
|
|
||||||
|
final val request // coil3.network/CacheStrategy.ReadResult.request|{}request[0]
|
||||||
|
final fun <get-request>(): coil3.network/NetworkRequest? // coil3.network/CacheStrategy.ReadResult.request.<get-request>|<get-request>(){}[0]
|
||||||
|
final val response // coil3.network/CacheStrategy.ReadResult.response|{}response[0]
|
||||||
|
final fun <get-response>(): coil3.network/NetworkResponse? // coil3.network/CacheStrategy.ReadResult.response.<get-response>|<get-response>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.network/CacheStrategy.ReadResult.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.network/CacheStrategy.ReadResult.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.network/CacheStrategy.ReadResult.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WriteResult { // coil3.network/CacheStrategy.WriteResult|null[0]
|
||||||
|
constructor <init>(coil3.network/NetworkResponse) // coil3.network/CacheStrategy.WriteResult.<init>|<init>(coil3.network.NetworkResponse){}[0]
|
||||||
|
|
||||||
|
final val response // coil3.network/CacheStrategy.WriteResult.response|{}response[0]
|
||||||
|
final fun <get-response>(): coil3.network/NetworkResponse? // coil3.network/CacheStrategy.WriteResult.response.<get-response>|<get-response>(){}[0]
|
||||||
|
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.network/CacheStrategy.WriteResult.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.network/CacheStrategy.WriteResult.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.network/CacheStrategy.WriteResult.toString|toString(){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.network/CacheStrategy.WriteResult.Companion|null[0]
|
||||||
|
final val DISABLED // coil3.network/CacheStrategy.WriteResult.Companion.DISABLED|{}DISABLED[0]
|
||||||
|
final fun <get-DISABLED>(): coil3.network/CacheStrategy.WriteResult // coil3.network/CacheStrategy.WriteResult.Companion.DISABLED.<get-DISABLED>|<get-DISABLED>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Companion { // coil3.network/CacheStrategy.Companion|null[0]
|
||||||
|
final val DEFAULT // coil3.network/CacheStrategy.Companion.DEFAULT|{}DEFAULT[0]
|
||||||
|
final fun <get-DEFAULT>(): coil3.network/CacheStrategy // coil3.network/CacheStrategy.Companion.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.network/ConcurrentRequestStrategy { // coil3.network/ConcurrentRequestStrategy|null[0]
|
||||||
|
abstract suspend fun apply(kotlin/String, kotlin.coroutines/SuspendFunction0<coil3.fetch/FetchResult>): coil3.fetch/FetchResult // coil3.network/ConcurrentRequestStrategy.apply|apply(kotlin.String;kotlin.coroutines.SuspendFunction0<coil3.fetch.FetchResult>){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.network/ConcurrentRequestStrategy.Companion|null[0]
|
||||||
|
final val UNCOORDINATED // coil3.network/ConcurrentRequestStrategy.Companion.UNCOORDINATED|{}UNCOORDINATED[0]
|
||||||
|
final fun <get-UNCOORDINATED>(): coil3.network/ConcurrentRequestStrategy // coil3.network/ConcurrentRequestStrategy.Companion.UNCOORDINATED.<get-UNCOORDINATED>|<get-UNCOORDINATED>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.network/NetworkClient { // coil3.network/NetworkClient|null[0]
|
||||||
|
abstract suspend fun <#A1: kotlin/Any?> executeRequest(coil3.network/NetworkRequest, kotlin.coroutines/SuspendFunction1<coil3.network/NetworkResponse, #A1>): #A1 // coil3.network/NetworkClient.executeRequest|executeRequest(coil3.network.NetworkRequest;kotlin.coroutines.SuspendFunction1<coil3.network.NetworkResponse,0:0>){0§<kotlin.Any?>}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.network/NetworkRequestBody { // coil3.network/NetworkRequestBody|null[0]
|
||||||
|
abstract suspend fun writeTo(okio/BufferedSink) // coil3.network/NetworkRequestBody.writeTo|writeTo(okio.BufferedSink){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface coil3.network/NetworkResponseBody : kotlin/AutoCloseable { // coil3.network/NetworkResponseBody|null[0]
|
||||||
|
abstract suspend fun writeTo(okio/BufferedSink) // coil3.network/NetworkResponseBody.writeTo|writeTo(okio.BufferedSink){}[0]
|
||||||
|
abstract suspend fun writeTo(okio/FileSystem, okio/Path) // coil3.network/NetworkResponseBody.writeTo|writeTo(okio.FileSystem;okio.Path){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/DeDupeConcurrentRequestStrategy : coil3.network/ConcurrentRequestStrategy { // coil3.network/DeDupeConcurrentRequestStrategy|null[0]
|
||||||
|
constructor <init>() // coil3.network/DeDupeConcurrentRequestStrategy.<init>|<init>(){}[0]
|
||||||
|
|
||||||
|
final suspend fun apply(kotlin/String, kotlin.coroutines/SuspendFunction0<coil3.fetch/FetchResult>): coil3.fetch/FetchResult // coil3.network/DeDupeConcurrentRequestStrategy.apply|apply(kotlin.String;kotlin.coroutines.SuspendFunction0<coil3.fetch.FetchResult>){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/HttpException : kotlin/RuntimeException { // coil3.network/HttpException|null[0]
|
||||||
|
constructor <init>(coil3.network/NetworkResponse) // coil3.network/HttpException.<init>|<init>(coil3.network.NetworkResponse){}[0]
|
||||||
|
|
||||||
|
final val response // coil3.network/HttpException.response|{}response[0]
|
||||||
|
final fun <get-response>(): coil3.network/NetworkResponse // coil3.network/HttpException.response.<get-response>|<get-response>(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/NetworkFetcher : coil3.fetch/Fetcher { // coil3.network/NetworkFetcher|null[0]
|
||||||
|
constructor <init>(kotlin/String, coil3.request/Options, kotlin/Lazy<coil3.network/NetworkClient>, kotlin/Lazy<coil3.disk/DiskCache?>, kotlin/Lazy<coil3.network/CacheStrategy>, coil3.network/ConnectivityChecker) // coil3.network/NetworkFetcher.<init>|<init>(kotlin.String;coil3.request.Options;kotlin.Lazy<coil3.network.NetworkClient>;kotlin.Lazy<coil3.disk.DiskCache?>;kotlin.Lazy<coil3.network.CacheStrategy>;coil3.network.ConnectivityChecker){}[0]
|
||||||
|
constructor <init>(kotlin/String, coil3.request/Options, kotlin/Lazy<coil3.network/NetworkClient>, kotlin/Lazy<coil3.disk/DiskCache?>, kotlin/Lazy<coil3.network/CacheStrategy>, kotlin/Lazy<coil3.network/ConnectivityChecker>, kotlin/Lazy<coil3.network/ConcurrentRequestStrategy>) // coil3.network/NetworkFetcher.<init>|<init>(kotlin.String;coil3.request.Options;kotlin.Lazy<coil3.network.NetworkClient>;kotlin.Lazy<coil3.disk.DiskCache?>;kotlin.Lazy<coil3.network.CacheStrategy>;kotlin.Lazy<coil3.network.ConnectivityChecker>;kotlin.Lazy<coil3.network.ConcurrentRequestStrategy>){}[0]
|
||||||
|
|
||||||
|
final fun getMimeType(kotlin/String, kotlin/String?): kotlin/String? // coil3.network/NetworkFetcher.getMimeType|getMimeType(kotlin.String;kotlin.String?){}[0]
|
||||||
|
final suspend fun fetch(): coil3.fetch/FetchResult // coil3.network/NetworkFetcher.fetch|fetch(){}[0]
|
||||||
|
|
||||||
|
final class Factory : coil3.fetch/Fetcher.Factory<coil3/Uri> { // coil3.network/NetworkFetcher.Factory|null[0]
|
||||||
|
constructor <init>(kotlin/Function0<coil3.network/NetworkClient>, kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ...) // coil3.network/NetworkFetcher.Factory.<init>|<init>(kotlin.Function0<coil3.network.NetworkClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>){}[0]
|
||||||
|
constructor <init>(kotlin/Function0<coil3.network/NetworkClient>, kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ..., kotlin/Function0<coil3.network/ConcurrentRequestStrategy> = ...) // coil3.network/NetworkFetcher.Factory.<init>|<init>(kotlin.Function0<coil3.network.NetworkClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>;kotlin.Function0<coil3.network.ConcurrentRequestStrategy>){}[0]
|
||||||
|
|
||||||
|
final fun create(coil3/Uri, coil3.request/Options, coil3/ImageLoader): coil3.fetch/Fetcher? // coil3.network/NetworkFetcher.Factory.create|create(coil3.Uri;coil3.request.Options;coil3.ImageLoader){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/NetworkHeaders { // coil3.network/NetworkHeaders|null[0]
|
||||||
|
final fun asMap(): kotlin.collections/Map<kotlin/String, kotlin.collections/List<kotlin/String>> // coil3.network/NetworkHeaders.asMap|asMap(){}[0]
|
||||||
|
final fun get(kotlin/String): kotlin/String? // coil3.network/NetworkHeaders.get|get(kotlin.String){}[0]
|
||||||
|
final fun getAll(kotlin/String): kotlin.collections/List<kotlin/String> // coil3.network/NetworkHeaders.getAll|getAll(kotlin.String){}[0]
|
||||||
|
final fun newBuilder(): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.newBuilder|newBuilder(){}[0]
|
||||||
|
|
||||||
|
final class Builder { // coil3.network/NetworkHeaders.Builder|null[0]
|
||||||
|
constructor <init>() // coil3.network/NetworkHeaders.Builder.<init>|<init>(){}[0]
|
||||||
|
constructor <init>(coil3.network/NetworkHeaders) // coil3.network/NetworkHeaders.Builder.<init>|<init>(coil3.network.NetworkHeaders){}[0]
|
||||||
|
|
||||||
|
final fun add(kotlin/String, kotlin/String): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.add|add(kotlin.String;kotlin.String){}[0]
|
||||||
|
final fun build(): coil3.network/NetworkHeaders // coil3.network/NetworkHeaders.Builder.build|build(){}[0]
|
||||||
|
final fun set(kotlin/String, kotlin.collections/List<kotlin/String>): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.set|set(kotlin.String;kotlin.collections.List<kotlin.String>){}[0]
|
||||||
|
final fun set(kotlin/String, kotlin/String): coil3.network/NetworkHeaders.Builder // coil3.network/NetworkHeaders.Builder.set|set(kotlin.String;kotlin.String){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Companion { // coil3.network/NetworkHeaders.Companion|null[0]
|
||||||
|
final val EMPTY // coil3.network/NetworkHeaders.Companion.EMPTY|{}EMPTY[0]
|
||||||
|
final fun <get-EMPTY>(): coil3.network/NetworkHeaders // coil3.network/NetworkHeaders.Companion.EMPTY.<get-EMPTY>|<get-EMPTY>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/NetworkRequest { // coil3.network/NetworkRequest|null[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ...) // coil3.network/NetworkRequest.<init>|<init>(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?){}[0]
|
||||||
|
constructor <init>(kotlin/String, kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ..., coil3/Extras = ...) // coil3.network/NetworkRequest.<init>|<init>(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?;coil3.Extras){}[0]
|
||||||
|
|
||||||
|
final val body // coil3.network/NetworkRequest.body|{}body[0]
|
||||||
|
final fun <get-body>(): coil3.network/NetworkRequestBody? // coil3.network/NetworkRequest.body.<get-body>|<get-body>(){}[0]
|
||||||
|
final val extras // coil3.network/NetworkRequest.extras|{}extras[0]
|
||||||
|
final fun <get-extras>(): coil3/Extras // coil3.network/NetworkRequest.extras.<get-extras>|<get-extras>(){}[0]
|
||||||
|
final val headers // coil3.network/NetworkRequest.headers|{}headers[0]
|
||||||
|
final fun <get-headers>(): coil3.network/NetworkHeaders // coil3.network/NetworkRequest.headers.<get-headers>|<get-headers>(){}[0]
|
||||||
|
final val method // coil3.network/NetworkRequest.method|{}method[0]
|
||||||
|
final fun <get-method>(): kotlin/String // coil3.network/NetworkRequest.method.<get-method>|<get-method>(){}[0]
|
||||||
|
final val url // coil3.network/NetworkRequest.url|{}url[0]
|
||||||
|
final fun <get-url>(): kotlin/String // coil3.network/NetworkRequest.url.<get-url>|<get-url>(){}[0]
|
||||||
|
|
||||||
|
final fun copy(kotlin/String = ..., kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ...): coil3.network/NetworkRequest // coil3.network/NetworkRequest.copy|copy(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?){}[0]
|
||||||
|
final fun copy(kotlin/String = ..., kotlin/String = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkRequestBody? = ..., coil3/Extras = ...): coil3.network/NetworkRequest // coil3.network/NetworkRequest.copy|copy(kotlin.String;kotlin.String;coil3.network.NetworkHeaders;coil3.network.NetworkRequestBody?;coil3.Extras){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.network/NetworkResponse { // coil3.network/NetworkResponse|null[0]
|
||||||
|
constructor <init>(kotlin/Int = ..., kotlin/Long = ..., kotlin/Long = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkResponseBody? = ..., kotlin/Any? = ...) // coil3.network/NetworkResponse.<init>|<init>(kotlin.Int;kotlin.Long;kotlin.Long;coil3.network.NetworkHeaders;coil3.network.NetworkResponseBody?;kotlin.Any?){}[0]
|
||||||
|
|
||||||
|
final val body // coil3.network/NetworkResponse.body|{}body[0]
|
||||||
|
final fun <get-body>(): coil3.network/NetworkResponseBody? // coil3.network/NetworkResponse.body.<get-body>|<get-body>(){}[0]
|
||||||
|
final val code // coil3.network/NetworkResponse.code|{}code[0]
|
||||||
|
final fun <get-code>(): kotlin/Int // coil3.network/NetworkResponse.code.<get-code>|<get-code>(){}[0]
|
||||||
|
final val delegate // coil3.network/NetworkResponse.delegate|{}delegate[0]
|
||||||
|
final fun <get-delegate>(): kotlin/Any? // coil3.network/NetworkResponse.delegate.<get-delegate>|<get-delegate>(){}[0]
|
||||||
|
final val headers // coil3.network/NetworkResponse.headers|{}headers[0]
|
||||||
|
final fun <get-headers>(): coil3.network/NetworkHeaders // coil3.network/NetworkResponse.headers.<get-headers>|<get-headers>(){}[0]
|
||||||
|
final val requestMillis // coil3.network/NetworkResponse.requestMillis|{}requestMillis[0]
|
||||||
|
final fun <get-requestMillis>(): kotlin/Long // coil3.network/NetworkResponse.requestMillis.<get-requestMillis>|<get-requestMillis>(){}[0]
|
||||||
|
final val responseMillis // coil3.network/NetworkResponse.responseMillis|{}responseMillis[0]
|
||||||
|
final fun <get-responseMillis>(): kotlin/Long // coil3.network/NetworkResponse.responseMillis.<get-responseMillis>|<get-responseMillis>(){}[0]
|
||||||
|
|
||||||
|
final fun copy(kotlin/Int = ..., kotlin/Long = ..., kotlin/Long = ..., coil3.network/NetworkHeaders = ..., coil3.network/NetworkResponseBody? = ..., kotlin/Any? = ...): coil3.network/NetworkResponse // coil3.network/NetworkResponse.copy|copy(kotlin.Int;kotlin.Long;kotlin.Long;coil3.network.NetworkHeaders;coil3.network.NetworkResponseBody?;kotlin.Any?){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object coil3.network/CacheNetworkResponse { // coil3.network/CacheNetworkResponse|null[0]
|
||||||
|
final fun readFrom(okio/BufferedSource): coil3.network/NetworkResponse // coil3.network/CacheNetworkResponse.readFrom|readFrom(okio.BufferedSource){}[0]
|
||||||
|
final fun writeTo(coil3.network/NetworkResponse, okio/BufferedSink) // coil3.network/CacheNetworkResponse.writeTo|writeTo(coil3.network.NetworkResponse;okio.BufferedSink){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.Extras.Key.Companion{}httpBody[0]
|
||||||
|
final fun (coil3/Extras.Key.Companion).<get-httpBody>(): coil3/Extras.Key<coil3.network/NetworkRequestBody?> // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.Extras.Key.Companion(){}[0]
|
||||||
|
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.request.ImageRequest{}httpBody[0]
|
||||||
|
final fun (coil3.request/ImageRequest).<get-httpBody>(): coil3.network/NetworkRequestBody? // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.request.ImageRequest(){}[0]
|
||||||
|
final val coil3.network/httpBody // coil3.network/httpBody|@coil3.request.Options{}httpBody[0]
|
||||||
|
final fun (coil3.request/Options).<get-httpBody>(): coil3.network/NetworkRequestBody? // coil3.network/httpBody.<get-httpBody>|<get-httpBody>@coil3.request.Options(){}[0]
|
||||||
|
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.Extras.Key.Companion{}httpHeaders[0]
|
||||||
|
final fun (coil3/Extras.Key.Companion).<get-httpHeaders>(): coil3/Extras.Key<coil3.network/NetworkHeaders> // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.Extras.Key.Companion(){}[0]
|
||||||
|
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.request.ImageRequest{}httpHeaders[0]
|
||||||
|
final fun (coil3.request/ImageRequest).<get-httpHeaders>(): coil3.network/NetworkHeaders // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.request.ImageRequest(){}[0]
|
||||||
|
final val coil3.network/httpHeaders // coil3.network/httpHeaders|@coil3.request.Options{}httpHeaders[0]
|
||||||
|
final fun (coil3.request/Options).<get-httpHeaders>(): coil3.network/NetworkHeaders // coil3.network/httpHeaders.<get-httpHeaders>|<get-httpHeaders>@coil3.request.Options(){}[0]
|
||||||
|
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.Extras.Key.Companion{}httpMethod[0]
|
||||||
|
final fun (coil3/Extras.Key.Companion).<get-httpMethod>(): coil3/Extras.Key<kotlin/String> // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.Extras.Key.Companion(){}[0]
|
||||||
|
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.request.ImageRequest{}httpMethod[0]
|
||||||
|
final fun (coil3.request/ImageRequest).<get-httpMethod>(): kotlin/String // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.request.ImageRequest(){}[0]
|
||||||
|
final val coil3.network/httpMethod // coil3.network/httpMethod|@coil3.request.Options{}httpMethod[0]
|
||||||
|
final fun (coil3.request/Options).<get-httpMethod>(): kotlin/String // coil3.network/httpMethod.<get-httpMethod>|<get-httpMethod>@coil3.request.Options(){}[0]
|
||||||
|
|
||||||
|
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpBody(coil3.network/NetworkRequestBody): coil3.request/ImageRequest.Builder // coil3.network/httpBody|httpBody@coil3.request.ImageRequest.Builder(coil3.network.NetworkRequestBody){}[0]
|
||||||
|
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpHeaders(coil3.network/NetworkHeaders): coil3.request/ImageRequest.Builder // coil3.network/httpHeaders|httpHeaders@coil3.request.ImageRequest.Builder(coil3.network.NetworkHeaders){}[0]
|
||||||
|
final fun (coil3.request/ImageRequest.Builder).coil3.network/httpMethod(kotlin/String): coil3.request/ImageRequest.Builder // coil3.network/httpMethod|httpMethod@coil3.request.ImageRequest.Builder(kotlin.String){}[0]
|
||||||
|
final fun coil3.network/ConnectivityChecker(coil3/PlatformContext): coil3.network/ConnectivityChecker // coil3.network/ConnectivityChecker|ConnectivityChecker(coil3.PlatformContext){}[0]
|
||||||
|
final fun coil3.network/NetworkRequestBody(okio/ByteString): coil3.network/NetworkRequestBody // coil3.network/NetworkRequestBody|NetworkRequestBody(okio.ByteString){}[0]
|
||||||
|
final fun coil3.network/NetworkResponseBody(okio/BufferedSource): coil3.network/NetworkResponseBody // coil3.network/NetworkResponseBody|NetworkResponseBody(okio.BufferedSource){}[0]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// :coil:coil-network-core - vendored VERBATIM from coil-kt/coil into src/vendor.
|
||||||
|
// Shared network-fetcher plumbing. Upstream: linux + apple, no mingwX64.
|
||||||
|
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
|
||||||
|
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
|
||||||
|
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil-network-core`.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies { api(project(":coil:coil-core")) }
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
val vNonAndroidMain = create("coilNonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
}
|
||||||
|
nativeMain { dependsOn(vNonAndroidMain) }
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# :coil:coil-network-core - shared network fetcher plumbing.
|
||||||
|
# Upstream: linux + apple, no mingwX64.
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-network-core/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/network/CacheNetworkResponse.kt -> src/vendor/common/kotlin/coil3/network/CacheNetworkResponse.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/CacheStrategy.kt -> src/vendor/common/kotlin/coil3/network/CacheStrategy.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/ConcurrentRequestStrategy.kt -> src/vendor/common/kotlin/coil3/network/ConcurrentRequestStrategy.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/common/kotlin/coil3/network/ConnectivityChecker.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/HttpException.kt -> src/vendor/common/kotlin/coil3/network/HttpException.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/NetworkClient.kt -> src/vendor/common/kotlin/coil3/network/NetworkClient.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/NetworkFetcher.kt -> src/vendor/common/kotlin/coil3/network/NetworkFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/NetworkHeaders.kt -> src/vendor/common/kotlin/coil3/network/NetworkHeaders.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/imageRequests.kt -> src/vendor/common/kotlin/coil3/network/imageRequests.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/internal/DefaultCacheStrategy.kt -> src/vendor/common/kotlin/coil3/network/internal/DefaultCacheStrategy.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/internal/SingleParameterLazy.kt -> src/vendor/common/kotlin/coil3/network/internal/SingleParameterLazy.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/internal/utils.kt -> src/vendor/common/kotlin/coil3/network/internal/utils.kt
|
||||||
|
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonAndroidMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/nonAndroid/kotlin/coil3/network/ConnectivityChecker.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/network/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/network/internal/utils.nonAndroid.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# androidMain (2)
|
||||||
|
# | androidMain/kotlin/coil3/network/ConnectivityChecker.kt -> src/vendor/android/kotlin/coil3/network/ConnectivityChecker.kt
|
||||||
|
# | androidMain/kotlin/coil3/network/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/network/internal/utils.android.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// 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.coil3:coil-network-ktor3>
|
||||||
|
final val coil3.network.ktor3.internal/initHook // coil3.network.ktor3.internal/initHook|{}initHook[0]
|
||||||
|
final fun <get-initHook>(): kotlin/Any // coil3.network.ktor3.internal/initHook.<get-initHook>|<get-initHook>(){}[0]
|
||||||
|
|
||||||
|
final fun (io.ktor.client/HttpClient).coil3.network.ktor3/asNetworkClient(): coil3.network/NetworkClient // coil3.network.ktor3/asNetworkClient|asNetworkClient@io.ktor.client.HttpClient(){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(io.ktor.client/HttpClient): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(io.ktor.client.HttpClient){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(io.ktor.client/HttpClient, coil3.network/ConcurrentRequestStrategy = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(io.ktor.client.HttpClient;coil3.network.ConcurrentRequestStrategy){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient> = ..., kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient> = ..., kotlin/Function0<coil3.network/CacheStrategy> = ..., kotlin/Function1<coil3/PlatformContext, coil3.network/ConnectivityChecker> = ..., kotlin/Function0<coil3.network/ConcurrentRequestStrategy> = ...): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>;kotlin.Function0<coil3.network.CacheStrategy>;kotlin.Function1<coil3.PlatformContext,coil3.network.ConnectivityChecker>;kotlin.Function0<coil3.network.ConcurrentRequestStrategy>){}[0]
|
||||||
|
final fun coil3.network.ktor3/KtorNetworkFetcherFactory(kotlin/Function0<io.ktor.client/HttpClient>): coil3.network/NetworkFetcher.Factory // coil3.network.ktor3/KtorNetworkFetcherFactory|KtorNetworkFetcherFactory(kotlin.Function0<io.ktor.client.HttpClient>){}[0]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// :coil:coil-network-ktor3 - vendored VERBATIM from coil-kt/coil into src/vendor.
|
||||||
|
// Ktor 3 network fetcher, on the port's existing ktor pin.
|
||||||
|
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
|
||||||
|
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
|
||||||
|
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil-network-ktor3`.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
api(project(":coil:coil-core"))
|
||||||
|
api(project(":coil:coil-network-core"))
|
||||||
|
api(libs.ktor.client.core)
|
||||||
|
}
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
val vNonAndroidMain = create("coilNonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
dependencies { implementation(libs.kotlinx.io.okio) }
|
||||||
|
}
|
||||||
|
nativeMain {
|
||||||
|
dependsOn(vNonAndroidMain)
|
||||||
|
kotlin.srcDir("src/vendor/native/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# :coil:coil-network-ktor3 - Ktor 3 network fetcher. Upstream: linux + apple, no mingwX64.
|
||||||
|
# Pairs with the port's existing ktor pin (the `ktor` entry in libs.versions.toml).
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-network-ktor3/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/network/ktor3/KtorNetworkFetcher.kt -> src/vendor/common/kotlin/coil3/network/ktor3/KtorNetworkFetcher.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.kt -> src/vendor/common/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.kt
|
||||||
|
# | commonMain/kotlin/coil3/network/ktor3/internal/utils.kt -> src/vendor/common/kotlin/coil3/network/ktor3/internal/utils.kt
|
||||||
|
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/network/ktor3/internal/utils.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/network/ktor3/internal/utils.nonJvmCommon.kt
|
||||||
|
nativeMain/kotlin/ -> src/vendor/native/kotlin/
|
||||||
|
# | nativeMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.native.kt -> src/vendor/native/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.native.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# jsMain (1)
|
||||||
|
# | jsMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.js.kt
|
||||||
|
# jvmCommonMain (1)
|
||||||
|
# | jvmCommonMain/kotlin/coil3/network/ktor3/internal/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/network/ktor3/internal/utils.jvmCommon.kt
|
||||||
|
# wasmJsMain (1)
|
||||||
|
# | wasmJsMain/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/network/ktor3/internal/KtorNetworkFetcherServiceLoaderTarget.wasmJs.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// 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.coil3:coil-svg>
|
||||||
|
abstract interface coil3.svg/Svg { // coil3.svg/Svg|null[0]
|
||||||
|
abstract val height // coil3.svg/Svg.height|{}height[0]
|
||||||
|
abstract fun <get-height>(): kotlin/Float // coil3.svg/Svg.height.<get-height>|<get-height>(){}[0]
|
||||||
|
abstract val width // coil3.svg/Svg.width|{}width[0]
|
||||||
|
abstract fun <get-width>(): kotlin/Float // coil3.svg/Svg.width.<get-width>|<get-width>(){}[0]
|
||||||
|
|
||||||
|
abstract var viewBox // coil3.svg/Svg.viewBox|{}viewBox[0]
|
||||||
|
abstract fun <get-viewBox>(): coil3.svg/Svg.ViewBox? // coil3.svg/Svg.viewBox.<get-viewBox>|<get-viewBox>(){}[0]
|
||||||
|
abstract fun <set-viewBox>(coil3.svg/Svg.ViewBox?) // coil3.svg/Svg.viewBox.<set-viewBox>|<set-viewBox>(coil3.svg.Svg.ViewBox?){}[0]
|
||||||
|
|
||||||
|
abstract fun asImage(kotlin/Int = ..., kotlin/Int = ...): coil3/Image // coil3.svg/Svg.asImage|asImage(kotlin.Int;kotlin.Int){}[0]
|
||||||
|
abstract fun height(kotlin/String) // coil3.svg/Svg.height|height(kotlin.String){}[0]
|
||||||
|
abstract fun options(coil3.request/Options) // coil3.svg/Svg.options|options(coil3.request.Options){}[0]
|
||||||
|
abstract fun width(kotlin/String) // coil3.svg/Svg.width|width(kotlin.String){}[0]
|
||||||
|
|
||||||
|
abstract fun interface Parser { // coil3.svg/Svg.Parser|null[0]
|
||||||
|
abstract fun parse(okio/BufferedSource): coil3.svg/Svg // coil3.svg/Svg.Parser.parse|parse(okio.BufferedSource){}[0]
|
||||||
|
|
||||||
|
final object Companion { // coil3.svg/Svg.Parser.Companion|null[0]
|
||||||
|
final val DEFAULT // coil3.svg/Svg.Parser.Companion.DEFAULT|{}DEFAULT[0]
|
||||||
|
final fun <get-DEFAULT>(): coil3.svg/Svg.Parser // coil3.svg/Svg.Parser.Companion.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ViewBox { // coil3.svg/Svg.ViewBox|null[0]
|
||||||
|
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // coil3.svg/Svg.ViewBox.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
||||||
|
|
||||||
|
final val bottom // coil3.svg/Svg.ViewBox.bottom|{}bottom[0]
|
||||||
|
final fun <get-bottom>(): kotlin/Float // coil3.svg/Svg.ViewBox.bottom.<get-bottom>|<get-bottom>(){}[0]
|
||||||
|
final val left // coil3.svg/Svg.ViewBox.left|{}left[0]
|
||||||
|
final fun <get-left>(): kotlin/Float // coil3.svg/Svg.ViewBox.left.<get-left>|<get-left>(){}[0]
|
||||||
|
final val right // coil3.svg/Svg.ViewBox.right|{}right[0]
|
||||||
|
final fun <get-right>(): kotlin/Float // coil3.svg/Svg.ViewBox.right.<get-right>|<get-right>(){}[0]
|
||||||
|
final val top // coil3.svg/Svg.ViewBox.top|{}top[0]
|
||||||
|
final fun <get-top>(): kotlin/Float // coil3.svg/Svg.ViewBox.top.<get-top>|<get-top>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): kotlin/Float // coil3.svg/Svg.ViewBox.component1|component1(){}[0]
|
||||||
|
final fun component2(): kotlin/Float // coil3.svg/Svg.ViewBox.component2|component2(){}[0]
|
||||||
|
final fun component3(): kotlin/Float // coil3.svg/Svg.ViewBox.component3|component3(){}[0]
|
||||||
|
final fun component4(): kotlin/Float // coil3.svg/Svg.ViewBox.component4|component4(){}[0]
|
||||||
|
final fun copy(kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ...): coil3.svg/Svg.ViewBox // coil3.svg/Svg.ViewBox.copy|copy(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // coil3.svg/Svg.ViewBox.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // coil3.svg/Svg.ViewBox.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // coil3.svg/Svg.ViewBox.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.svg/SvgDecoder : coil3.decode/Decoder { // coil3.svg/SvgDecoder|null[0]
|
||||||
|
constructor <init>(coil3.decode/ImageSource, coil3.request/Options, coil3.svg/Svg.Parser = ..., kotlin/Function1<coil3/PlatformContext, kotlin/Float> = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.<init>|<init>(coil3.decode.ImageSource;coil3.request.Options;coil3.svg.Svg.Parser;kotlin.Function1<coil3.PlatformContext,kotlin.Float>;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
constructor <init>(coil3.decode/ImageSource, coil3.request/Options, kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.<init>|<init>(coil3.decode.ImageSource;coil3.request.Options;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
|
||||||
|
final val density // coil3.svg/SvgDecoder.density|{}density[0]
|
||||||
|
final fun <get-density>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.density.<get-density>|<get-density>(){}[0]
|
||||||
|
final val parser // coil3.svg/SvgDecoder.parser|{}parser[0]
|
||||||
|
final fun <get-parser>(): coil3.svg/Svg.Parser // coil3.svg/SvgDecoder.parser.<get-parser>|<get-parser>(){}[0]
|
||||||
|
final val renderToBitmap // coil3.svg/SvgDecoder.renderToBitmap|{}renderToBitmap[0]
|
||||||
|
final fun <get-renderToBitmap>(): kotlin/Boolean // coil3.svg/SvgDecoder.renderToBitmap.<get-renderToBitmap>|<get-renderToBitmap>(){}[0]
|
||||||
|
final val scaleToDensity // coil3.svg/SvgDecoder.scaleToDensity|{}scaleToDensity[0]
|
||||||
|
final fun <get-scaleToDensity>(): kotlin/Boolean // coil3.svg/SvgDecoder.scaleToDensity.<get-scaleToDensity>|<get-scaleToDensity>(){}[0]
|
||||||
|
final val useViewBoundsAsIntrinsicSize // coil3.svg/SvgDecoder.useViewBoundsAsIntrinsicSize|{}useViewBoundsAsIntrinsicSize[0]
|
||||||
|
final fun <get-useViewBoundsAsIntrinsicSize>(): kotlin/Boolean // coil3.svg/SvgDecoder.useViewBoundsAsIntrinsicSize.<get-useViewBoundsAsIntrinsicSize>|<get-useViewBoundsAsIntrinsicSize>(){}[0]
|
||||||
|
|
||||||
|
final suspend fun decode(): coil3.decode/DecodeResult // coil3.svg/SvgDecoder.decode|decode(){}[0]
|
||||||
|
|
||||||
|
final class Factory : coil3.decode/Decoder.Factory { // coil3.svg/SvgDecoder.Factory|null[0]
|
||||||
|
constructor <init>(coil3.svg/Svg.Parser = ..., kotlin/Function1<coil3/PlatformContext, kotlin/Float> = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.Factory.<init>|<init>(coil3.svg.Svg.Parser;kotlin.Function1<coil3.PlatformContext,kotlin.Float>;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
constructor <init>(kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // coil3.svg/SvgDecoder.Factory.<init>|<init>(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
|
|
||||||
|
final val density // coil3.svg/SvgDecoder.Factory.density|{}density[0]
|
||||||
|
final fun <get-density>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Factory.density.<get-density>|<get-density>(){}[0]
|
||||||
|
final val parser // coil3.svg/SvgDecoder.Factory.parser|{}parser[0]
|
||||||
|
final fun <get-parser>(): coil3.svg/Svg.Parser // coil3.svg/SvgDecoder.Factory.parser.<get-parser>|<get-parser>(){}[0]
|
||||||
|
final val renderToBitmap // coil3.svg/SvgDecoder.Factory.renderToBitmap|{}renderToBitmap[0]
|
||||||
|
final fun <get-renderToBitmap>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.renderToBitmap.<get-renderToBitmap>|<get-renderToBitmap>(){}[0]
|
||||||
|
final val scaleToDensity // coil3.svg/SvgDecoder.Factory.scaleToDensity|{}scaleToDensity[0]
|
||||||
|
final fun <get-scaleToDensity>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.scaleToDensity.<get-scaleToDensity>|<get-scaleToDensity>(){}[0]
|
||||||
|
final val useViewBoundsAsIntrinsicSize // coil3.svg/SvgDecoder.Factory.useViewBoundsAsIntrinsicSize|{}useViewBoundsAsIntrinsicSize[0]
|
||||||
|
final fun <get-useViewBoundsAsIntrinsicSize>(): kotlin/Boolean // coil3.svg/SvgDecoder.Factory.useViewBoundsAsIntrinsicSize.<get-useViewBoundsAsIntrinsicSize>|<get-useViewBoundsAsIntrinsicSize>(){}[0]
|
||||||
|
|
||||||
|
final fun create(coil3.fetch/SourceFetchResult, coil3.request/Options, coil3/ImageLoader): coil3.decode/Decoder? // coil3.svg/SvgDecoder.Factory.create|create(coil3.fetch.SourceFetchResult;coil3.request.Options;coil3.ImageLoader){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Companion { // coil3.svg/SvgDecoder.Companion|null[0]
|
||||||
|
final val NO_DENSITY // coil3.svg/SvgDecoder.Companion.NO_DENSITY|{}NO_DENSITY[0]
|
||||||
|
final fun <get-NO_DENSITY>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Companion.NO_DENSITY.<get-NO_DENSITY>|<get-NO_DENSITY>(){}[0]
|
||||||
|
final val PLATFORM_DENSITY // coil3.svg/SvgDecoder.Companion.PLATFORM_DENSITY|{}PLATFORM_DENSITY[0]
|
||||||
|
final fun <get-PLATFORM_DENSITY>(): kotlin/Function1<coil3/PlatformContext, kotlin/Float> // coil3.svg/SvgDecoder.Companion.PLATFORM_DENSITY.<get-PLATFORM_DENSITY>|<get-PLATFORM_DENSITY>(){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class coil3.svg/SvgImage : coil3/Image { // coil3.svg/SvgImage|null[0]
|
||||||
|
constructor <init>(org.jetbrains.skia.svg/SVGDOM, kotlin/Int, kotlin/Int) // coil3.svg/SvgImage.<init>|<init>(org.jetbrains.skia.svg.SVGDOM;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
|
||||||
|
final val height // coil3.svg/SvgImage.height|{}height[0]
|
||||||
|
final fun <get-height>(): kotlin/Int // coil3.svg/SvgImage.height.<get-height>|<get-height>(){}[0]
|
||||||
|
final val shareable // coil3.svg/SvgImage.shareable|{}shareable[0]
|
||||||
|
final fun <get-shareable>(): kotlin/Boolean // coil3.svg/SvgImage.shareable.<get-shareable>|<get-shareable>(){}[0]
|
||||||
|
final val size // coil3.svg/SvgImage.size|{}size[0]
|
||||||
|
final fun <get-size>(): kotlin/Long // coil3.svg/SvgImage.size.<get-size>|<get-size>(){}[0]
|
||||||
|
final val svg // coil3.svg/SvgImage.svg|{}svg[0]
|
||||||
|
final fun <get-svg>(): org.jetbrains.skia.svg/SVGDOM // coil3.svg/SvgImage.svg.<get-svg>|<get-svg>(){}[0]
|
||||||
|
final val width // coil3.svg/SvgImage.width|{}width[0]
|
||||||
|
final fun <get-width>(): kotlin/Int // coil3.svg/SvgImage.width.<get-width>|<get-width>(){}[0]
|
||||||
|
|
||||||
|
final fun draw(org.jetbrains.skia/Canvas) // coil3.svg/SvgImage.draw|draw(org.jetbrains.skia.Canvas){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final val coil3.svg.internal/initHook // coil3.svg.internal/initHook|{}initHook[0]
|
||||||
|
final fun <get-initHook>(): kotlin/Any // coil3.svg.internal/initHook.<get-initHook>|<get-initHook>(){}[0]
|
||||||
|
final val coil3.svg/height // coil3.svg/height|@coil3.svg.Svg.ViewBox{}height[0]
|
||||||
|
final fun (coil3.svg/Svg.ViewBox).<get-height>(): kotlin/Float // coil3.svg/height.<get-height>|<get-height>@coil3.svg.Svg.ViewBox(){}[0]
|
||||||
|
final val coil3.svg/width // coil3.svg/width|@coil3.svg.Svg.ViewBox{}width[0]
|
||||||
|
final fun (coil3.svg/Svg.ViewBox).<get-width>(): kotlin/Float // coil3.svg/width.<get-width>|<get-width>@coil3.svg.Svg.ViewBox(){}[0]
|
||||||
|
|
||||||
|
final fun (coil3.decode/DecodeUtils).coil3.svg/isSvg(okio/BufferedSource): kotlin/Boolean // coil3.svg/isSvg|isSvg@coil3.decode.DecodeUtils(okio.BufferedSource){}[0]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// :coil:coil-svg - SVG decoding, vendored VERBATIM from coil-kt/coil into src/vendor.
|
||||||
|
// Upstream: linux + apple, no mingwX64. The non-android decoder goes through skiko's
|
||||||
|
// SVGDOM, so mingwX64 binds the bitsycore skiko FORK like :compose:ui:ui-graphics.
|
||||||
|
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core).
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies { api(project(":coil:coil-core")) }
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
// Two PARALLEL skiko branches (official for macOS/Linux, the bitsycore fork for
|
||||||
|
// mingwX64), each two levels deep so nonAndroid `expect`s and nativeMain
|
||||||
|
// `actual`s are not siblings. See coil/coil-core/build.gradle.kts for the why.
|
||||||
|
fun skikoBranch(inName: String, inSkiko: Any): org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet {
|
||||||
|
val vNonAndroid = create("${inName}NonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
dependencies { implementation(inSkiko) }
|
||||||
|
}
|
||||||
|
return create("${inName}NativeMain") {
|
||||||
|
dependsOn(vNonAndroid)
|
||||||
|
kotlin.srcDir("src/vendor/native/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val vOfficial = skikoBranch("coilSvgOfficial", libs.skiko)
|
||||||
|
get("macosArm64Main").dependsOn(vOfficial)
|
||||||
|
get("linuxX64Main").dependsOn(vOfficial)
|
||||||
|
get("linuxArm64Main").dependsOn(vOfficial)
|
||||||
|
|
||||||
|
if (vHostSupportsMingw) {
|
||||||
|
val vFork = skikoBranch(
|
||||||
|
"coilSvgFork",
|
||||||
|
"com.bitsycore.skiko:skiko:${providers.gradleProperty("skikoMingwVersion").getOrElse(libs.versions.skikoMingw.get())}",
|
||||||
|
)
|
||||||
|
get("mingwX64Main").dependsOn(vFork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# :coil:coil-svg - SVG decoding. Upstream: linux + apple, no mingwX64.
|
||||||
|
# The non-android decoder goes through skiko's SVGDOM.
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil-svg/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/svg/Svg.kt -> src/vendor/common/kotlin/coil3/svg/Svg.kt
|
||||||
|
# | commonMain/kotlin/coil3/svg/SvgDecoder.kt -> src/vendor/common/kotlin/coil3/svg/SvgDecoder.kt
|
||||||
|
# | commonMain/kotlin/coil3/svg/decodeUtils.kt -> src/vendor/common/kotlin/coil3/svg/decodeUtils.kt
|
||||||
|
# | commonMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.kt -> src/vendor/common/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.kt
|
||||||
|
# | commonMain/kotlin/coil3/svg/internal/parseSvg.kt -> src/vendor/common/kotlin/coil3/svg/internal/parseSvg.kt
|
||||||
|
# | commonMain/kotlin/coil3/svg/internal/utils.kt -> src/vendor/common/kotlin/coil3/svg/internal/utils.kt
|
||||||
|
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonAndroidMain/kotlin/coil3/svg/SvgImage.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/SvgImage.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/svg/internal/parseSvg.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/parseSvg.nonAndroid.kt
|
||||||
|
# | nonAndroidMain/kotlin/coil3/svg/internal/utils.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/utils.nonAndroid.kt
|
||||||
|
nonJvmCommonMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonJvmCommonMain/kotlin/coil3/svg/internal/utils.nonJvmCommon.kt -> src/vendor/nonAndroid/kotlin/coil3/svg/internal/utils.nonJvmCommon.kt
|
||||||
|
nativeMain/kotlin/ -> src/vendor/native/kotlin/
|
||||||
|
# | nativeMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.native.kt -> src/vendor/native/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.native.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# androidMain (4)
|
||||||
|
# | androidMain/kotlin/coil3/svg/SvgImage.android.kt -> src/vendor/android/kotlin/coil3/svg/SvgImage.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/svg/imageRequests.android.kt -> src/vendor/android/kotlin/coil3/svg/imageRequests.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/svg/internal/parseSvg.android.kt -> src/vendor/android/kotlin/coil3/svg/internal/parseSvg.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/svg/internal/utils.android.kt -> src/vendor/android/kotlin/coil3/svg/internal/utils.android.kt
|
||||||
|
# jsMain (1)
|
||||||
|
# | jsMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.js.kt -> src/vendor/js/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.js.kt
|
||||||
|
# jvmCommonMain (1)
|
||||||
|
# | jvmCommonMain/kotlin/coil3/svg/internal/utils.jvmCommon.kt -> src/vendor/jvmCommon/kotlin/coil3/svg/internal/utils.jvmCommon.kt
|
||||||
|
# wasmJsMain (1)
|
||||||
|
# | wasmJsMain/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.wasmJs.kt -> src/vendor/wasmJs/kotlin/coil3/svg/internal/SvgDecoderServiceLoaderTarget.wasmJs.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// 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.coil3:coil>
|
||||||
|
final object coil3/SingletonImageLoader { // coil3/SingletonImageLoader|null[0]
|
||||||
|
final fun get(coil3/PlatformContext): coil3/ImageLoader // coil3/SingletonImageLoader.get|get(coil3.PlatformContext){}[0]
|
||||||
|
final fun reset() // coil3/SingletonImageLoader.reset|reset(){}[0]
|
||||||
|
final fun setSafe(coil3/SingletonImageLoader.Factory) // coil3/SingletonImageLoader.setSafe|setSafe(coil3.SingletonImageLoader.Factory){}[0]
|
||||||
|
final fun setUnsafe(coil3/ImageLoader) // coil3/SingletonImageLoader.setUnsafe|setUnsafe(coil3.ImageLoader){}[0]
|
||||||
|
final fun setUnsafe(coil3/SingletonImageLoader.Factory) // coil3/SingletonImageLoader.setUnsafe|setUnsafe(coil3.SingletonImageLoader.Factory){}[0]
|
||||||
|
|
||||||
|
abstract fun interface Factory { // coil3/SingletonImageLoader.Factory|null[0]
|
||||||
|
abstract fun newImageLoader(coil3/PlatformContext): coil3/ImageLoader // coil3/SingletonImageLoader.Factory.newImageLoader|newImageLoader(coil3.PlatformContext){}[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// :coil:coil - vendored VERBATIM from coil-kt/coil into src/vendor.
|
||||||
|
// The singleton ImageLoader facade. Upstream: linux + apple, no mingwX64.
|
||||||
|
// Upstream's non* source sets sit ABOVE nativeMain here (see coil-core's build file
|
||||||
|
// and compose-fork.txt for why). Provenance = compose-fork.txt; never hand-edit
|
||||||
|
// src/vendor - change the manifest + `python scripts/compose-fork/sync.py coil/coil`.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||||
|
applyDefaultHierarchyTemplate()
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies { api(project(":coil:coil-core")) }
|
||||||
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
|
}
|
||||||
|
val vNonAndroidMain = create("coilNonAndroidMain") {
|
||||||
|
dependsOn(commonMain.get())
|
||||||
|
kotlin.srcDir("src/vendor/nonAndroid/kotlin")
|
||||||
|
}
|
||||||
|
nativeMain { dependsOn(vNonAndroidMain) }
|
||||||
|
}
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xexpect-actual-classes",
|
||||||
|
"-opt-in=coil3.annotation.InternalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=coil3.annotation.DelicateCoilApi",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# :coil:coil - the singleton ImageLoader facade. Upstream: linux + apple, no mingwX64.
|
||||||
|
# Upstream source sets collapse onto this port: it has NO android, jvm or js
|
||||||
|
# target, so nonAndroidMain / nonJvmCommonMain / nonJsCommonMain / nativeMain all
|
||||||
|
# apply to EVERY target here. They land in TWO trees, not one: the non* sets keep
|
||||||
|
# their own level (src/vendor/nonAndroid) ABOVE src/vendor/native, because some of
|
||||||
|
# them declare `expect`s whose `actual`s live in upstream nativeMain - collapsing
|
||||||
|
# both into one source set makes expect and actual siblings, which is an error. appleMain is macosArm64
|
||||||
|
# only and nonAppleMain is linux + mingw, so those two keep their own trees (the
|
||||||
|
# build file wires a `nonAppleMain` intermediate; appleMain comes from the default
|
||||||
|
# hierarchy template).
|
||||||
|
SET_REPO=https://github.com/coil-kt/coil@<COIL_REF>
|
||||||
|
|
||||||
|
SET_FOLDER=coil/src
|
||||||
|
|
||||||
|
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||||
|
# | commonMain/kotlin/coil3/SingletonImageLoader.kt -> src/vendor/common/kotlin/coil3/SingletonImageLoader.kt
|
||||||
|
nonAndroidMain/kotlin/ -> src/vendor/nonAndroid/kotlin/
|
||||||
|
# | nonAndroidMain/kotlin/coil3/SingletonImageLoader.nonAndroid.kt -> src/vendor/nonAndroid/kotlin/coil3/SingletonImageLoader.nonAndroid.kt
|
||||||
|
|
||||||
|
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||||
|
# androidMain (2)
|
||||||
|
# | androidMain/kotlin/coil3/SingletonImageLoader.android.kt -> src/vendor/android/kotlin/coil3/SingletonImageLoader.android.kt
|
||||||
|
# | androidMain/kotlin/coil3/singletonImageLoaders.android.kt -> src/vendor/android/kotlin/coil3/singletonImageLoaders.android.kt
|
||||||
|
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||||
+4
-10
@@ -1,22 +1,16 @@
|
|||||||
// :components-resources — the official Compose Multiplatform resources runtime
|
// :components-resources - the official Compose Multiplatform resources runtime
|
||||||
// (org.jetbrains.compose.components:components-resources), vendored because the
|
// (org.jetbrains.compose.components:components-resources), vendored because the
|
||||||
// Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte
|
// Maven artifact ships no mingwX64/linux klibs. Public API is byte-for-byte
|
||||||
// upstream (painterResource / stringResource / Font / qualifiers / Res codegen
|
// upstream (painterResource / stringResource / Font / qualifiers / Res codegen
|
||||||
// compatibility); the platform actuals are this port's: data.kres reading,
|
// compatibility); the platform actuals are this port's: data.kres reading,
|
||||||
// image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM
|
// image decode via :ui's Skia decoder, SDL locale/theme environment. Apps' JVM
|
||||||
// targets keep using the official Maven artifact — this module is native-only.
|
// targets keep using the official Maven artifact - this module is native-only.
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.multiplatform)
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
alias(libs.plugins.kotlin.plugin.compose)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
|
||||||
}
|
|
||||||
|
|
||||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -34,13 +28,13 @@ kotlin {
|
|||||||
// foundation (isSystemInDarkTheme in ResourceEnvironment) pulls the
|
// foundation (isSystemInDarkTheme in ResourceEnvironment) pulls the
|
||||||
// whole ui surface + loadComposeResourceBytes + the sdl3 cinterop
|
// whole ui surface + loadComposeResourceBytes + the sdl3 cinterop
|
||||||
// types transitively (all edges are api).
|
// types transitively (all edges are api).
|
||||||
api(project(":foundation"))
|
api(project(":compose:foundation:foundation"))
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
kotlin.srcDir("src/vendor/native/kotlin")
|
kotlin.srcDir("src/vendor/native/kotlin")
|
||||||
// src/nativeMain/kotlin also holds the Font / Image resource actuals —
|
// src/nativeMain/kotlin also holds the Font / Image resource actuals -
|
||||||
// pure project code that delegates to the project font registry
|
// pure project code that delegates to the project font registry
|
||||||
// (IconFont / NamedFont) and the :ui-graphics Skia decoder
|
// (IconFont / NamedFont) and the :ui-graphics Skia decoder
|
||||||
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
|
// (decodeEncodedImageBitmap). Skiko-free, so a single native set covers
|
||||||
+6
-6
@@ -1,9 +1,9 @@
|
|||||||
# Files vendored VERBATIM from JetBrains/compose-multiplatform (the umbrella repo —
|
# Files vendored VERBATIM from JetBrains/compose-multiplatform (the umbrella repo -
|
||||||
# NOT compose-multiplatform-core) into :components-resources. The official
|
# NOT compose-multiplatform-core) into :components-resources. The official
|
||||||
# org.jetbrains.compose.components:components-resources artifact publishes NO
|
# org.jetbrains.compose.components:components-resources artifact publishes NO
|
||||||
# mingwX64/linux variants, so the runtime library is vendored and the platform
|
# mingwX64/linux variants, so the runtime library is vendored and the platform
|
||||||
# actuals are project code:
|
# actuals are project code:
|
||||||
# src/nativeMain — ResourceReader (data.kres), getSystemEnvironment (SDL),
|
# src/nativeMain - ResourceReader (data.kres), getSystemEnvironment (SDL),
|
||||||
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
|
# pure-Kotlin DomXmlParser (upstream's is Darwin NSXMLParser),
|
||||||
# and the Font/Image resource actuals: renderer-agnostic
|
# and the Font/Image resource actuals: renderer-agnostic
|
||||||
# project code (image decode via the :ui-graphics Skia
|
# project code (image decode via the :ui-graphics Skia
|
||||||
@@ -43,16 +43,16 @@ blockingMain/kotlin/ -> src/vendor/native/kotlin/
|
|||||||
nativeMain/kotlin/ -> src/vendor/native/kotlin/
|
nativeMain/kotlin/ -> src/vendor/native/kotlin/
|
||||||
# | nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt
|
# | nativeMain/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageResources.native.kt
|
||||||
# | nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
|
# | nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
|
||||||
# Darwin-only (Foundation NSXMLParser) — replaced by a pure-Kotlin project parser
|
# Darwin-only (Foundation NSXMLParser) - replaced by a pure-Kotlin project parser
|
||||||
# in src/nativeMain so mingwX64/linux compile.
|
# in src/nativeMain so mingwX64/linux compile.
|
||||||
!nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
|
!nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt
|
||||||
|
|
||||||
# Portable pieces of upstream skikoMain (no Skia references — pure delegation /
|
# Portable pieces of upstream skikoMain (no Skia references - pure delegation /
|
||||||
# runtime-only) — every native target gets them.
|
# runtime-only) - every native target gets them.
|
||||||
skikoMain/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt
|
skikoMain/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ImageDecoders.skiko.kt
|
||||||
skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt
|
skikoMain/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt -> src/vendor/native/kotlin/org/jetbrains/compose/resources/ResourceReader.skiko.kt
|
||||||
|
|
||||||
# Skia-bound pieces — upstream's skikoMain actuals rely on Compose-Desktop Skia
|
# Skia-bound pieces - upstream's skikoMain actuals rely on Compose-Desktop Skia
|
||||||
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
|
# extensions (SystemFont / toComposeImageBitmap / nativeCanvas) this port's :ui
|
||||||
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
|
# doesn't expose. The module's own Font/Image actuals (src/nativeMain/kotlin/)
|
||||||
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
|
# use pure project APIs (IconFont / NamedFont / decodeEncodedImageBitmap)
|
||||||
+9
-3
@@ -1,5 +1,11 @@
|
|||||||
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
||||||
|
|
||||||
|
// VENDOR-REIMPL(COMPOSE_REF): components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @ v1.12.0
|
||||||
|
// Project REIMPLEMENTATION, not a derived copy: same package + signatures,
|
||||||
|
// different body (the port replaces upstream's skiko scene/platform layer).
|
||||||
|
// Upstream edits to the counterpart do NOT need reconciling here - re-check
|
||||||
|
// only if the expect/actual SIGNATURES change.
|
||||||
|
|
||||||
package org.jetbrains.compose.resources
|
package org.jetbrains.compose.resources
|
||||||
|
|
||||||
import org.jetbrains.compose.resources.InternalResourceApi
|
import org.jetbrains.compose.resources.InternalResourceApi
|
||||||
@@ -15,14 +21,14 @@ import com.compose.sdl.icons.IconFont
|
|||||||
import com.compose.sdl.text.NamedFont
|
import com.compose.sdl.text.NamedFont
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Font actuals — Skia text stack via the project font registry
|
// MARK: Font actuals - Skia text stack via the project font registry
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** The Skia text stack resolves fonts by FAMILY NAME through the project font
|
/** The Skia text stack resolves fonts by FAMILY NAME through the project font
|
||||||
registry (IconFont.registerIcon — the general byte-font table, not just
|
registry (IconFont.registerIcon - the general byte-font table, not just
|
||||||
icons). A font resource therefore loads its bytes through the resource
|
icons). A font resource therefore loads its bytes through the resource
|
||||||
reader, registers them under a per-resource family name, and returns a
|
reader, registers them under a per-resource family name, and returns a
|
||||||
[NamedFont] — `FontFamily(Font(Res.font.x))` then renders through the
|
[NamedFont] - `FontFamily(Font(Res.font.x))` then renders through the
|
||||||
standard project pipeline. Weight/style are carried on the NamedFont;
|
standard project pipeline. Weight/style are carried on the NamedFont;
|
||||||
variation axes apply at draw time via Skia's axis support. */
|
variation axes apply at draw time via Skia's axis support. */
|
||||||
|
|
||||||
+19
-13
@@ -1,5 +1,11 @@
|
|||||||
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
@file:OptIn(InternalResourceApi::class, ExperimentalResourceApi::class)
|
||||||
|
|
||||||
|
// VENDOR-REIMPL(COMPOSE_REF): components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/ImageResources.skiko.kt @ v1.12.0
|
||||||
|
// Project REIMPLEMENTATION, not a derived copy: same package + signatures,
|
||||||
|
// different body (the port replaces upstream's skiko scene/platform layer).
|
||||||
|
// Upstream edits to the counterpart do NOT need reconciling here - re-check
|
||||||
|
// only if the expect/actual SIGNATURES change.
|
||||||
|
|
||||||
package org.jetbrains.compose.resources
|
package org.jetbrains.compose.resources
|
||||||
|
|
||||||
import org.jetbrains.compose.resources.InternalResourceApi
|
import org.jetbrains.compose.resources.InternalResourceApi
|
||||||
@@ -19,7 +25,7 @@ import com.compose.sdl.graphics.svgIntrinsicSize
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Image actuals — Skia decode via the :ui-graphics hook
|
// MARK: Image actuals - Skia decode via the :ui-graphics hook
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Decoding goes through the :ui-graphics Skia hook the SDL backend registers
|
/** Decoding goes through the :ui-graphics Skia hook the SDL backend registers
|
||||||
@@ -28,7 +34,7 @@ import kotlin.math.roundToInt
|
|||||||
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 render backend initialised before painterResource ran?")
|
?: error("Image decode failed - is the render backend initialised before painterResource ran?")
|
||||||
|
|
||||||
/** SVG element = the raw document bytes; rendering is size-driven (see SvgPainter). */
|
/** SVG element = the raw document bytes; rendering is size-driven (see SvgPainter). */
|
||||||
internal actual class SvgElement(val bytes: ByteArray)
|
internal actual class SvgElement(val bytes: ByteArray)
|
||||||
@@ -45,20 +51,20 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
|
|||||||
|
|
||||||
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
|
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
|
||||||
|
|
||||||
private var fAlpha: Float = 1f
|
private var mAlpha: Float = 1f
|
||||||
private var fColorFilter: ColorFilter? = null
|
private var mColorFilter: ColorFilter? = null
|
||||||
private var fCacheKey: Long = -1L
|
private var mCacheKey: Long = -1L
|
||||||
private var fCacheBitmap: ImageBitmap? = null
|
private var mCacheBitmap: ImageBitmap? = null
|
||||||
|
|
||||||
override fun applyAlpha(alpha: Float): Boolean { fAlpha = alpha; return true }
|
override fun applyAlpha(alpha: Float): Boolean { mAlpha = alpha; return true }
|
||||||
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { fColorFilter = colorFilter; return true }
|
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { mColorFilter = colorFilter; return true }
|
||||||
|
|
||||||
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
||||||
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
|
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
|
||||||
if (vKey == fCacheKey && fCacheBitmap != null) return fCacheBitmap
|
if (vKey == mCacheKey && mCacheBitmap != null) return mCacheBitmap
|
||||||
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
|
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
|
||||||
fCacheKey = vKey
|
mCacheKey = vKey
|
||||||
fCacheBitmap = vBitmap
|
mCacheBitmap = vBitmap
|
||||||
return vBitmap
|
return vBitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +79,8 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
|
|||||||
srcSize = IntSize(vBitmap.width, vBitmap.height),
|
srcSize = IntSize(vBitmap.width, vBitmap.height),
|
||||||
dstOffset = IntOffset.Zero,
|
dstOffset = IntOffset.Zero,
|
||||||
dstSize = IntSize(vWpx, vHpx),
|
dstSize = IntSize(vWpx, vHpx),
|
||||||
alpha = fAlpha,
|
alpha = mAlpha,
|
||||||
colorFilter = fColorFilter,
|
colorFilter = mColorFilter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-3
@@ -8,13 +8,13 @@ import com.compose.sdl.res.preferredLocaleProvider
|
|||||||
import com.compose.sdl.res.systemThemeIsDarkProvider
|
import com.compose.sdl.res.systemThemeIsDarkProvider
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: ResourceEnvironment — platform-env-seam actual
|
// MARK: ResourceEnvironment - platform-env-seam actual
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Non-composable system environment for qualifier resolution (values-fr,
|
/** Non-composable system environment for qualifier resolution (values-fr,
|
||||||
drawable-dark, …): locale + theme come from the platform-env seams installed
|
drawable-dark, …): locale + theme come from the platform-env seams installed
|
||||||
by the SDL layer, so :components-resources carries no dependency on the sdl3
|
by the SDL layer, so :components-resources carries no dependency on the sdl3
|
||||||
cinterop. Density is reported as 1f — under this port's Option-B density flow
|
cinterop. Density is reported as 1f - under this port's Option-B density flow
|
||||||
layout runs in physical pixels and drawables are bundled at a single density,
|
layout runs in physical pixels and drawables are bundled at a single density,
|
||||||
so the mdpi bucket is always the right one. The COMPOSABLE path
|
so the mdpi bucket is always the right one. The COMPOSABLE path
|
||||||
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
|
(rememberResourceEnvironment) doesn't use this: it reads LocalDensity /
|
||||||
@@ -24,7 +24,7 @@ internal actual fun getSystemEnvironment(): ResourceEnvironment {
|
|||||||
val vDark = systemThemeIsDarkProvider?.invoke() ?: false
|
val vDark = systemThemeIsDarkProvider?.invoke() ?: false
|
||||||
return ResourceEnvironment(
|
return ResourceEnvironment(
|
||||||
language = LanguageQualifier(vLanguage),
|
language = LanguageQualifier(vLanguage),
|
||||||
// SDL locales carry language+country only — no script subtag.
|
// SDL locales carry language+country only - no script subtag.
|
||||||
script = ScriptQualifier(""),
|
script = ScriptQualifier(""),
|
||||||
region = RegionQualifier(vRegion),
|
region = RegionQualifier(vRegion),
|
||||||
theme = ThemeQualifier.selectByValue(isDark = vDark),
|
theme = ThemeQualifier.selectByValue(isDark = vDark),
|
||||||
+2
-2
@@ -8,11 +8,11 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
|
|||||||
import com.compose.sdl.loadComposeResourceBytes
|
import com.compose.sdl.loadComposeResourceBytes
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: ResourceReader — data.kres actual
|
// MARK: ResourceReader - data.kres actual
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** The platform reader for this port: every app ships its composeResources
|
/** The platform reader for this port: every app ships its composeResources
|
||||||
content inside data.kres (a STORED zip next to the executable — see the
|
content inside data.kres (a STORED zip next to the executable - see the
|
||||||
apps' Zip tasks), and :ui's ResourceIO serves entries by exact path with an
|
apps' Zip tasks), and :ui's ResourceIO serves entries by exact path with an
|
||||||
fseek+fread. The paths the generated Res accessors produce
|
fseek+fread. The paths the generated Res accessors produce
|
||||||
("composeResources/<package>/drawable/x.png") are stored verbatim in
|
("composeResources/<package>/drawable/x.png") are stored verbatim in
|
||||||
+56
-56
@@ -1,23 +1,23 @@
|
|||||||
package org.jetbrains.compose.resources.vector.xmldom
|
package org.jetbrains.compose.resources.vector.xmldom
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: DomXmlParser — pure-Kotlin replacement for upstream's Darwin parser
|
// MARK: DomXmlParser - pure-Kotlin replacement for upstream's Darwin parser
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/* Upstream's nativeMain DomXmlParser is built on Foundation's NSXMLParser and
|
/* Upstream's nativeMain DomXmlParser is built on Foundation's NSXMLParser and
|
||||||
only compiles on Darwin; this port also targets mingwX64/linux, so the same
|
only compiles on Darwin; this port also targets mingwX64/linux, so the same
|
||||||
`parse(xml): Element` entry point is implemented as a small recursive-descent
|
`parse(xml): Element` entry point is implemented as a small recursive-descent
|
||||||
XML parser instead (project file — the upstream one is excluded in
|
XML parser instead (project file - the upstream one is excluded in
|
||||||
compose-fork.txt).
|
compose-fork.txt).
|
||||||
|
|
||||||
Scope: exactly what XmlVectorParser + the resource string XMLs need —
|
Scope: exactly what XmlVectorParser + the resource string XMLs need -
|
||||||
elements, namespaced attributes (xmlns scoping, getAttributeNS,
|
elements, namespaced attributes (xmlns scoping, getAttributeNS,
|
||||||
lookupPrefix), text nodes (textContent), comments / prolog / DOCTYPE /
|
lookupPrefix), text nodes (textContent), comments / prolog / DOCTYPE /
|
||||||
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
|
// VENDOR-BASE(COMPOSE_REF): components/resources/library/src/nativeMain/kotlin/org/jetbrains/compose/resources/vector/xmldom/DomXmlParser.kt @ v1.12.1
|
||||||
// (fresh REIMPL, not a copy-edit — the base ref marks the upstream API it tracks)
|
// (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)
|
||||||
@@ -32,13 +32,13 @@ private class Attr(val prefix: String, val localName: String, val nsUri: String,
|
|||||||
val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName"
|
val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName"
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NodeListImpl(private val fNodes: List<Node>) : NodeList {
|
private class NodeListImpl(private val mNodes: List<Node>) : NodeList {
|
||||||
override fun item(i: Int): Node = fNodes[i]
|
override fun item(i: Int): Node = mNodes[i]
|
||||||
override val length: Int get() = fNodes.size
|
override val length: Int get() = mNodes.size
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextNodeImpl(private val fText: String) : Node {
|
private class TextNodeImpl(private val mText: String) : Node {
|
||||||
override val textContent: String? get() = fText
|
override val textContent: String? get() = mText
|
||||||
override val nodeName: String get() = "#text"
|
override val nodeName: String get() = "#text"
|
||||||
override val localName: String get() = ""
|
override val localName: String get() = ""
|
||||||
override val childNodes: NodeList = NodeListImpl(emptyList())
|
override val childNodes: NodeList = NodeListImpl(emptyList())
|
||||||
@@ -47,19 +47,19 @@ private class TextNodeImpl(private val fText: String) : Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ElementImpl(
|
private class ElementImpl(
|
||||||
private val fPrefix: String,
|
private val mPrefix: String,
|
||||||
override val localName: String,
|
override val localName: String,
|
||||||
override val namespaceURI: String,
|
override val namespaceURI: String,
|
||||||
private val fAttrs: List<Attr>,
|
private val mAttrs: List<Attr>,
|
||||||
// Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns).
|
// Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns).
|
||||||
private val fNsScope: List<Map<String, String>>,
|
private val mNsScope: List<Map<String, String>>,
|
||||||
) : Element {
|
) : Element {
|
||||||
var children: List<Node> = emptyList()
|
var children: List<Node> = emptyList()
|
||||||
|
|
||||||
override val nodeName: String get() = if (fPrefix.isEmpty()) localName else "$fPrefix:$localName"
|
override val nodeName: String get() = if (mPrefix.isEmpty()) localName else "$mPrefix:$localName"
|
||||||
override val childNodes: NodeList get() = NodeListImpl(children)
|
override val childNodes: NodeList get() = NodeListImpl(children)
|
||||||
|
|
||||||
// Concatenated descendant text — what DOM's textContent does.
|
// Concatenated descendant text - what DOM's textContent does.
|
||||||
override val textContent: String?
|
override val textContent: String?
|
||||||
get() = buildString {
|
get() = buildString {
|
||||||
for (vChild in children) {
|
for (vChild in children) {
|
||||||
@@ -72,7 +72,7 @@ private class ElementImpl(
|
|||||||
|
|
||||||
override fun lookupPrefix(namespaceURI: String): String {
|
override fun lookupPrefix(namespaceURI: String): String {
|
||||||
if (namespaceURI.isEmpty()) return ""
|
if (namespaceURI.isEmpty()) return ""
|
||||||
for (vScope in fNsScope) {
|
for (vScope in mNsScope) {
|
||||||
for ((vPrefix, vUri) in vScope) {
|
for ((vPrefix, vUri) in vScope) {
|
||||||
if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix
|
if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix
|
||||||
}
|
}
|
||||||
@@ -81,22 +81,22 @@ private class ElementImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
|
override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
|
||||||
fAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: ""
|
mAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: ""
|
||||||
|
|
||||||
override fun getAttribute(name: String): String =
|
override fun getAttribute(name: String): String =
|
||||||
fAttrs.firstOrNull { it.qualifiedName == name }?.value ?: ""
|
mAttrs.firstOrNull { it.qualifiedName == name }?.value ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// The parser — one pass over the string with a cursor.
|
// The parser - one pass over the string with a cursor.
|
||||||
private class XmlDomParser(private val fXml: String) {
|
private class XmlDomParser(private val mXml: String) {
|
||||||
private var fPos = 0
|
private var mPos = 0
|
||||||
|
|
||||||
fun skipProlog() {
|
fun skipProlog() {
|
||||||
while (true) {
|
while (true) {
|
||||||
skipWhitespace()
|
skipWhitespace()
|
||||||
when {
|
when {
|
||||||
lookingAt("") -> fPos++
|
lookingAt("") -> mPos++
|
||||||
lookingAt("<?") -> skipUntil("?>")
|
lookingAt("<?") -> skipUntil("?>")
|
||||||
lookingAt("<!--") -> skipUntil("-->")
|
lookingAt("<!--") -> skipUntil("-->")
|
||||||
lookingAt("<!DOCTYPE") -> skipDoctype()
|
lookingAt("<!DOCTYPE") -> skipDoctype()
|
||||||
@@ -112,16 +112,16 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
if (!lookingAt("<") || lookingAt("</")) return null
|
if (!lookingAt("<") || lookingAt("</")) return null
|
||||||
expect("<")
|
expect("<")
|
||||||
val vQName = readName()
|
val vQName = readName()
|
||||||
if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $fPos")
|
if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $mPos")
|
||||||
|
|
||||||
// Attributes (collect raw first — xmlns declarations shape the scope
|
// Attributes (collect raw first - xmlns declarations shape the scope
|
||||||
// that THIS element's own prefix resolves against).
|
// that THIS element's own prefix resolves against).
|
||||||
val vRawAttrs = ArrayList<Pair<String, String>>() // qualifiedName to value
|
val vRawAttrs = ArrayList<Pair<String, String>>() // qualifiedName to value
|
||||||
while (true) {
|
while (true) {
|
||||||
skipWhitespace()
|
skipWhitespace()
|
||||||
if (lookingAt("/>") || lookingAt(">")) break
|
if (lookingAt("/>") || lookingAt(">")) break
|
||||||
val vAttrName = readName()
|
val vAttrName = readName()
|
||||||
if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $fPos")
|
if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $mPos")
|
||||||
skipWhitespace(); expect("="); skipWhitespace()
|
skipWhitespace(); expect("="); skipWhitespace()
|
||||||
vRawAttrs.add(vAttrName to readQuotedValue())
|
vRawAttrs.add(vAttrName to readQuotedValue())
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope)
|
val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope)
|
||||||
|
|
||||||
if (lookingAt("/>")) {
|
if (lookingAt("/>")) {
|
||||||
fPos += 2
|
mPos += 2
|
||||||
return vElement
|
return vElement
|
||||||
}
|
}
|
||||||
expect(">")
|
expect(">")
|
||||||
@@ -169,9 +169,9 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
val vChildren = ArrayList<Node>()
|
val vChildren = ArrayList<Node>()
|
||||||
while (true) {
|
while (true) {
|
||||||
when {
|
when {
|
||||||
fPos >= fXml.length -> throw MalformedXMLException("unclosed <$vQName>")
|
mPos >= mXml.length -> throw MalformedXMLException("unclosed <$vQName>")
|
||||||
lookingAt("</") -> {
|
lookingAt("</") -> {
|
||||||
fPos += 2
|
mPos += 2
|
||||||
val vClose = readName()
|
val vClose = readName()
|
||||||
skipWhitespace(); expect(">")
|
skipWhitespace(); expect(">")
|
||||||
if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>")
|
if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>")
|
||||||
@@ -180,18 +180,18 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
}
|
}
|
||||||
lookingAt("<!--") -> skipUntil("-->")
|
lookingAt("<!--") -> skipUntil("-->")
|
||||||
lookingAt("<![CDATA[") -> {
|
lookingAt("<![CDATA[") -> {
|
||||||
val vEnd = fXml.indexOf("]]>", fPos + 9)
|
val vEnd = mXml.indexOf("]]>", mPos + 9)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unclosed CDATA")
|
if (vEnd < 0) throw MalformedXMLException("unclosed CDATA")
|
||||||
vChildren.add(TextNodeImpl(fXml.substring(fPos + 9, vEnd)))
|
vChildren.add(TextNodeImpl(mXml.substring(mPos + 9, vEnd)))
|
||||||
fPos = vEnd + 3
|
mPos = vEnd + 3
|
||||||
}
|
}
|
||||||
lookingAt("<?") -> skipUntil("?>")
|
lookingAt("<?") -> skipUntil("?>")
|
||||||
lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) }
|
lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) }
|
||||||
else -> {
|
else -> {
|
||||||
val vNext = fXml.indexOf('<', fPos).let { if (it < 0) fXml.length else it }
|
val vNext = mXml.indexOf('<', mPos).let { if (it < 0) mXml.length else it }
|
||||||
val vText = decodeEntities(fXml.substring(fPos, vNext))
|
val vText = decodeEntities(mXml.substring(mPos, vNext))
|
||||||
if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText))
|
if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText))
|
||||||
fPos = vNext
|
mPos = vNext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,28 +200,28 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
// ============
|
// ============
|
||||||
// Lexing helpers
|
// Lexing helpers
|
||||||
|
|
||||||
private fun lookingAt(inToken: String): Boolean = fXml.startsWith(inToken, fPos)
|
private fun lookingAt(inToken: String): Boolean = mXml.startsWith(inToken, mPos)
|
||||||
|
|
||||||
private fun expect(inToken: String) {
|
private fun expect(inToken: String) {
|
||||||
if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $fPos")
|
if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $mPos")
|
||||||
fPos += inToken.length
|
mPos += inToken.length
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipWhitespace() {
|
private fun skipWhitespace() {
|
||||||
while (fPos < fXml.length && fXml[fPos].isWhitespace()) fPos++
|
while (mPos < mXml.length && mXml[mPos].isWhitespace()) mPos++
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipUntil(inToken: String) {
|
private fun skipUntil(inToken: String) {
|
||||||
val vEnd = fXml.indexOf(inToken, fPos)
|
val vEnd = mXml.indexOf(inToken, mPos)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section")
|
if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section")
|
||||||
fPos = vEnd + inToken.length
|
mPos = vEnd + inToken.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DOCTYPE may nest an internal subset in [ ] — skip to the matching '>'. */
|
/* DOCTYPE may nest an internal subset in [ ] - skip to the matching '>'. */
|
||||||
private fun skipDoctype() {
|
private fun skipDoctype() {
|
||||||
var vDepth = 0
|
var vDepth = 0
|
||||||
while (fPos < fXml.length) {
|
while (mPos < mXml.length) {
|
||||||
val c = fXml[fPos++]
|
val c = mXml[mPos++]
|
||||||
if (c == '[') vDepth++
|
if (c == '[') vDepth++
|
||||||
else if (c == ']') vDepth--
|
else if (c == ']') vDepth--
|
||||||
else if (c == '>' && vDepth <= 0) return
|
else if (c == '>' && vDepth <= 0) return
|
||||||
@@ -230,24 +230,24 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun readName(): String {
|
private fun readName(): String {
|
||||||
val vStart = fPos
|
val vStart = mPos
|
||||||
while (fPos < fXml.length) {
|
while (mPos < mXml.length) {
|
||||||
val c = fXml[fPos]
|
val c = mXml[mPos]
|
||||||
if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break
|
if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break
|
||||||
fPos++
|
mPos++
|
||||||
}
|
}
|
||||||
return fXml.substring(vStart, fPos)
|
return mXml.substring(vStart, mPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readQuotedValue(): String {
|
private fun readQuotedValue(): String {
|
||||||
if (fPos >= fXml.length) throw MalformedXMLException("unterminated attribute value")
|
if (mPos >= mXml.length) throw MalformedXMLException("unterminated attribute value")
|
||||||
val vQuote = fXml[fPos]
|
val vQuote = mXml[mPos]
|
||||||
if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $fPos")
|
if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $mPos")
|
||||||
fPos++
|
mPos++
|
||||||
val vEnd = fXml.indexOf(vQuote, fPos)
|
val vEnd = mXml.indexOf(vQuote, mPos)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unterminated attribute value")
|
if (vEnd < 0) throw MalformedXMLException("unterminated attribute value")
|
||||||
val vRaw = fXml.substring(fPos, vEnd)
|
val vRaw = mXml.substring(mPos, vEnd)
|
||||||
fPos = vEnd + 1
|
mPos = vEnd + 1
|
||||||
return decodeEntities(vRaw)
|
return decodeEntities(vRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||||
|
|
||||||
// :animation-core — androidx.compose.animation.core.* vendored VERBATIM from
|
// :animation-core - androidx.compose.animation.core.* vendored VERBATIM from
|
||||||
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
|
// upstream. Renderer-agnostic (spring/tween specs, animatable, transition,
|
||||||
// vector math). Split out of :ui so :ui stays about renderer+ui glue only.
|
// vector math). Split out of :ui so :ui stays about renderer+ui glue only.
|
||||||
//
|
//
|
||||||
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
|
// Provenance = animation-core/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under animation-core/src/vendor/ — change the manifest and
|
// Never hand-edit files under animation-core/src/vendor/ - change the manifest and
|
||||||
// re-run `bash scripts/compose-fork/sync.sh :animation-core`.
|
// re-run `bash scripts/compose-fork/sync.sh :animation-core`.
|
||||||
//
|
//
|
||||||
// Publication artifactId (when set up): compose-desktop-native-animation-core.
|
// Publication artifactId (when set up): compose-desktop-native-animation-core.
|
||||||
@@ -30,7 +30,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":ui"))
|
api(project(":compose:ui:ui"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// :animation-graphics — androidx.compose.animation.graphics.*, vendored VERBATIM
|
// :animation-graphics - androidx.compose.animation.graphics.*, vendored VERBATIM
|
||||||
// from upstream into src/vendor/. Split out to mirror Compose Multiplatform's
|
// from upstream into src/vendor/. Split out to mirror Compose Multiplatform's
|
||||||
// module packaging (compose/animation/animation-graphics).
|
// module packaging (compose/animation/animation-graphics).
|
||||||
//
|
//
|
||||||
// Provenance = animation-graphics/compose-fork.txt + scripts/compose-fork/compose.properties.
|
// Provenance = animation-graphics/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under src/vendor/ — change the manifest and re-run
|
// Never hand-edit files under src/vendor/ - change the manifest and re-run
|
||||||
// `python scripts/compose-fork/sync.py compose/animation/animation-graphics`.
|
// `python scripts/compose-fork/sync.py compose/animation/animation-graphics`.
|
||||||
//
|
//
|
||||||
// Publication artifactId: desktop-animation-graphics.
|
// Publication artifactId: desktop-animation-graphics.
|
||||||
@@ -28,8 +28,8 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// animation.graphics.* uses animation.* / animation.core.* and the vector
|
// animation.graphics.* uses animation.* / animation.core.* and the vector
|
||||||
// graphics from ui (ui-graphics.vector, ui-util).
|
// graphics from ui (ui-graphics.vector, ui-util).
|
||||||
api(project(":animation"))
|
api(project(":compose:animation:animation"))
|
||||||
api(project(":ui"))
|
api(project(":compose:ui:ui"))
|
||||||
}
|
}
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// :animation — androidx.compose.animation.* (non-core), vendored VERBATIM from
|
// :animation - androidx.compose.animation.* (non-core), vendored VERBATIM from
|
||||||
// upstream into src/vendor/. Split out of :foundation to mirror Compose
|
// upstream into src/vendor/. Split out of :foundation to mirror Compose
|
||||||
// Multiplatform's module packaging.
|
// Multiplatform's module packaging.
|
||||||
//
|
//
|
||||||
// Provenance = animation/compose-fork.txt + scripts/compose-fork/compose.properties.
|
// Provenance = animation/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under src/vendor/ — change the manifest and re-run
|
// Never hand-edit files under src/vendor/ - change the manifest and re-run
|
||||||
// `python scripts/compose-fork/sync.py compose/animation/animation`.
|
// `python scripts/compose-fork/sync.py compose/animation/animation`.
|
||||||
//
|
//
|
||||||
// Publication artifactId: desktop-animation.
|
// Publication artifactId: desktop-animation.
|
||||||
@@ -28,8 +28,8 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// animation.* imports animation.core.* and foundation.layout.* (Box/Row/Column);
|
// animation.* imports animation.core.* and foundation.layout.* (Box/Row/Column);
|
||||||
// ui / runtime arrive transitively.
|
// ui / runtime arrive transitively.
|
||||||
api(project(":animation-core"))
|
api(project(":compose:animation:animation-core"))
|
||||||
api(project(":foundation-layout"))
|
api(project(":compose:foundation:foundation-layout"))
|
||||||
}
|
}
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -28,8 +28,8 @@ final var com.compose.sdl/useVirtualFrameTime // com.compose.sdl/useVirtualFrame
|
|||||||
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
|
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
|
||||||
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
|
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
|
||||||
|
|
||||||
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||||
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||||
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
|
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
|
||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
// :desktop-native-window — the module apps depend on. Owns nativeComposeWindow() (main loop,
|
// :desktop-native-window - the module apps depend on. Owns nativeComposeWindow() (main loop,
|
||||||
// recomposer lifecycle, event dispatch, Snapshot apply notifications).
|
// recomposer lifecycle, event dispatch, Snapshot apply notifications).
|
||||||
// Renderer selection happens entirely inside :ui via source-set wiring
|
// Renderer selection happens entirely inside :ui via source-set wiring
|
||||||
// (skikoRendererMain — Skia-only) — this module just calls
|
// (skikoRendererMain - Skia-only) - this module just calls
|
||||||
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
|
// `createRenderBackend(...)` and `rendererPreferredGpuMode()` from :ui and
|
||||||
// the right symbol resolves per target.
|
// the right symbol resolves per target.
|
||||||
// Publication artifactId (when set up): compose-desktop-native.
|
// Publication artifactId (when set up): compose-desktop-native.
|
||||||
@@ -23,7 +23,7 @@ kotlin {
|
|||||||
|
|
||||||
applyDefaultHierarchyTemplate()
|
applyDefaultHierarchyTemplate()
|
||||||
|
|
||||||
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib — no
|
// sdl3.* types are api-exposed via :ui → :sdl-core's cinterop klib - no
|
||||||
// separate sdl3 cinterop here.
|
// separate sdl3 cinterop here.
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -34,14 +34,14 @@ kotlin {
|
|||||||
// out of :ui (upstream Compose layout).
|
// out of :ui (upstream Compose layout).
|
||||||
// Material widgets used to be re-exported from :material here; the
|
// Material widgets used to be re-exported from :material here; the
|
||||||
// module was retired when :apidemo and :demo migrated to :material3.
|
// module was retired when :apidemo and :demo migrated to :material3.
|
||||||
// Apps that want Material 3 widgets pull `implementation(project(":material3"))`
|
// Apps that want Material 3 widgets pull `implementation(project(":compose:material3:material3"))`
|
||||||
// themselves (:material3 doesn't need to be `api`-exposed — the
|
// themselves (:material3 doesn't need to be `api`-exposed - the
|
||||||
// upstream vendored surface is stable and apps import it directly).
|
// upstream vendored surface is stable and apps import it directly).
|
||||||
api(project(":ui"))
|
api(project(":compose:ui:ui"))
|
||||||
api(project(":foundation"))
|
api(project(":compose:foundation:foundation"))
|
||||||
api(project(":animation-core"))
|
api(project(":compose:animation:animation-core"))
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
// setMain() / resetMain() — see Sdl3MainDispatcher.kt for usage.
|
// setMain() / resetMain() - see Sdl3MainDispatcher.kt for usage.
|
||||||
implementation(libs.kotlinx.coroutines.test)
|
implementation(libs.kotlinx.coroutines.test)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.AbstractApplier
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: App runtime internals
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
|
||||||
|
override fun exitApplication() { runtime.exitRequested = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registry + lifecycle for the live windows. Windows are created by the app
|
||||||
|
composition (Window()'s remember) and destroyed by the LOOP - DisposableEffect
|
||||||
|
onDispose only schedules, because teardown disposes a composition and that
|
||||||
|
must not run re-entrantly inside another composition's apply pass. */
|
||||||
|
internal class AppRuntime {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
val windows = mutableListOf<WindowInstance>()
|
||||||
|
private val destroyQueue = mutableListOf<WindowInstance>()
|
||||||
|
var exitRequested = false
|
||||||
|
var hadWindow = false
|
||||||
|
|
||||||
|
fun windowFor(inId: UInt): WindowInstance? =
|
||||||
|
if (inId == 0u) windows.firstOrNull()
|
||||||
|
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
|
||||||
|
|
||||||
|
fun markAllNeedFrame() {
|
||||||
|
for (vW in windows) vW.needsFrame = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWindow(
|
||||||
|
inTitle: String,
|
||||||
|
inWidth: Int,
|
||||||
|
inHeight: Int,
|
||||||
|
inGpu: GpuMode,
|
||||||
|
inIcon: AppWindowIcon?,
|
||||||
|
inAttrs: WindowAttributes,
|
||||||
|
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
|
||||||
|
inPreviewKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
|
||||||
|
inKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
|
||||||
|
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
|
||||||
|
): WindowInstance {
|
||||||
|
val vWindow = WindowInstance(
|
||||||
|
inTitle, inWidth, inHeight, inGpu, inIcon, inAttrs, inOnFrame,
|
||||||
|
inPreviewKeyHandler, inKeyHandler, inContent,
|
||||||
|
)
|
||||||
|
// A Window() was declared either way - the "exit when the last window
|
||||||
|
// is gone" rule must also fire when every declared window failed to
|
||||||
|
// initialise (otherwise the loop would spin forever with none).
|
||||||
|
hadWindow = true
|
||||||
|
if (vWindow.init(scope)) {
|
||||||
|
windows.add(vWindow)
|
||||||
|
} else {
|
||||||
|
println("nativeComposeApp: window '$inTitle' failed to initialise")
|
||||||
|
}
|
||||||
|
return vWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scheduleDestroy(inWindow: WindowInstance) {
|
||||||
|
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reapDestroyed() {
|
||||||
|
while (destroyQueue.isNotEmpty()) {
|
||||||
|
val vW = destroyQueue.removeLast()
|
||||||
|
windows.remove(vW)
|
||||||
|
vW.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op applier for the app-level composition - it emits no UI nodes, only
|
||||||
|
// side effects (each Window() manages a native window).
|
||||||
|
internal class UnitApplier : AbstractApplier<Unit>(Unit) {
|
||||||
|
override fun insertTopDown(index: Int, instance: Unit) {}
|
||||||
|
override fun insertBottomUp(index: Int, instance: Unit) {}
|
||||||
|
override fun remove(index: Int, count: Int) {}
|
||||||
|
override fun move(from: Int, to: Int, count: Int) {}
|
||||||
|
override fun onClear() {}
|
||||||
|
}
|
||||||
+2
-2
@@ -11,14 +11,14 @@ package com.compose.sdl
|
|||||||
*
|
*
|
||||||
* Each list holds data.kres resource paths of PRE-DECODED `.rgba` icon blobs
|
* Each list holds data.kres resource paths of PRE-DECODED `.rgba` icon blobs
|
||||||
* (produced by the bridge plugin's `icon { }` packaging, or by
|
* (produced by the bridge plugin's `icon { }` packaging, or by
|
||||||
* `scripts/make-app-icon.py rgba` — i.e. an 8-byte
|
* `scripts/make-app-icon.py rgba` - i.e. an 8-byte
|
||||||
* `[width u32-le][height u32-le]` header + straight-alpha RGBA pixels). List
|
* `[width u32-le][height u32-le]` header + straight-alpha RGBA pixels). List
|
||||||
* one path per size you bundle; the largest becomes the base and the rest are
|
* one path per size you bundle; the largest becomes the base and the rest are
|
||||||
* attached as alternate resolutions SDL chooses from (title bar vs taskbar vs
|
* attached as alternate resolutions SDL chooses from (title bar vs taskbar vs
|
||||||
* Alt-Tab), so the icon stays crisp at every size.
|
* Alt-Tab), so the icon stays crisp at every size.
|
||||||
*
|
*
|
||||||
* On Windows the runtime icon set here complements the icon EMBEDDED in the
|
* On Windows the runtime icon set here complements the icon EMBEDDED in the
|
||||||
* `.exe` (Explorer / pinned taskbar) — see the app / bridge-plugin Gradle setup.
|
* `.exe` (Explorer / pinned taskbar) - see the app / bridge-plugin Gradle setup.
|
||||||
*/
|
*/
|
||||||
class AppWindowIcon(
|
class AppWindowIcon(
|
||||||
val light: List<String>,
|
val light: List<String>,
|
||||||
+357
@@ -0,0 +1,357 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.runtime.Composition
|
||||||
|
import androidx.compose.runtime.Recomposer
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import sdl3.SDL_Delay
|
||||||
|
import sdl3.SDL_GetTicks
|
||||||
|
import sdl3.SDL_Quit
|
||||||
|
import sdl3.SDL_WaitEventTimeout
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Application entry - nativeComposeApp + Window()
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
Multi-window entry point, shaped like Compose Desktop's `application {}`:
|
||||||
|
|
||||||
|
nativeComposeApp {
|
||||||
|
var showTools by remember { mutableStateOf(false) }
|
||||||
|
Window(onCloseRequest = ::exitApplication, title = "Main") { MainUi(onTools = { showTools = true }) }
|
||||||
|
if (showTools) Window(onCloseRequest = { showTools = false }, title = "Tools") { ToolsUi() }
|
||||||
|
}
|
||||||
|
|
||||||
|
The app CONTENT is itself a composition (no UI tree - a Unit applier): each
|
||||||
|
`Window(...)` call materialises an SDL window + renderer + root host + its own
|
||||||
|
recomposer/composition, and leaving the composition (state flips to false)
|
||||||
|
tears the window down. One main loop pumps the shared SDL event queue and
|
||||||
|
routes events per SDL window id.
|
||||||
|
|
||||||
|
Per-window pumping order matters: the render-bridge globals
|
||||||
|
(currentImageLoader / viewport) are per-renderer, so the
|
||||||
|
loop installs a window's globals before recomposing / laying out / drawing it.
|
||||||
|
|
||||||
|
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
|
||||||
|
probes and apidemo ride it unchanged).
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ApplicationScope {
|
||||||
|
/** Requests the main loop to exit after the current iteration (all windows
|
||||||
|
are torn down on the way out). */
|
||||||
|
fun exitApplication()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Declares one native window for as long as this composable stays in the app
|
||||||
|
composition. `onCloseRequest` fires when the user asks the window to close
|
||||||
|
(OS close button, or `window.close()` from content) - remove the state that
|
||||||
|
composes this Window to actually close it, or call exitApplication(). */
|
||||||
|
@Composable
|
||||||
|
fun ApplicationScope.Window(
|
||||||
|
onCloseRequest: () -> Unit,
|
||||||
|
title: String = "ComposeNativeSDL3",
|
||||||
|
width: Int = 800,
|
||||||
|
height: Int = 600,
|
||||||
|
gpu: GpuMode = GpuMode.Auto,
|
||||||
|
icon: AppWindowIcon? = null,
|
||||||
|
undecorated: Boolean = false,
|
||||||
|
transparent: Boolean = false,
|
||||||
|
resizable: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
focusable: Boolean = true,
|
||||||
|
alwaysOnTop: Boolean = false,
|
||||||
|
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
||||||
|
content: @Composable ComposeWindowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val vScope = this as? ApplicationScopeImpl
|
||||||
|
?: error("Window() must be called inside nativeComposeApp { ... }")
|
||||||
|
val vContent = rememberUpdatedState(content)
|
||||||
|
val vClose = rememberUpdatedState(onCloseRequest)
|
||||||
|
val vPreviewKey = rememberUpdatedState(onPreviewKeyEvent)
|
||||||
|
val vKey = rememberUpdatedState(onKeyEvent)
|
||||||
|
val vAttrs = WindowAttributes(
|
||||||
|
undecorated = undecorated,
|
||||||
|
transparent = transparent,
|
||||||
|
resizable = resizable,
|
||||||
|
enabled = enabled,
|
||||||
|
focusable = focusable,
|
||||||
|
alwaysOnTop = alwaysOnTop,
|
||||||
|
)
|
||||||
|
val vWindow = remember {
|
||||||
|
vScope.runtime.createWindow(
|
||||||
|
inTitle = title,
|
||||||
|
inWidth = width,
|
||||||
|
inHeight = height,
|
||||||
|
inGpu = gpu,
|
||||||
|
inIcon = icon,
|
||||||
|
inAttrs = vAttrs,
|
||||||
|
inOnFrame = onFrame,
|
||||||
|
// Read through holders so updated lambdas propagate without
|
||||||
|
// recreating the window (same reason as the content holder below).
|
||||||
|
inPreviewKeyHandler = { vPreviewKey.value },
|
||||||
|
inKeyHandler = { vKey.value },
|
||||||
|
// The window composition reads the holder each recomposition, so
|
||||||
|
// updated content lambdas propagate without recreating the window.
|
||||||
|
inContent = { vContent.value },
|
||||||
|
).also { it.onCloseRequest = { vClose.value.invoke() } }
|
||||||
|
}
|
||||||
|
SideEffect {
|
||||||
|
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
|
||||||
|
}
|
||||||
|
// Re-apply only on a real change; `transparent` is creation-only (see
|
||||||
|
// WindowAttributes) so it is deliberately not re-applied here.
|
||||||
|
DisposableEffect(vAttrs) {
|
||||||
|
vWindow.applyAttributes(vAttrs)
|
||||||
|
onDispose { }
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots SDL + the Compose runtime, runs `content` as the application
|
||||||
|
composition, and drives every declared Window until exitApplication() or
|
||||||
|
the last window closes. */
|
||||||
|
fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||||
|
// Install the Main dispatcher BEFORE any window/owner exists: ComposeOwner
|
||||||
|
// captures Dispatchers.Main eagerly for its per-node coroutine scopes.
|
||||||
|
val mainDispatcher = Sdl3MainDispatcher()
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
Dispatchers.setMain(mainDispatcher)
|
||||||
|
|
||||||
|
val runtime = AppRuntime()
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
runtime.scope = this
|
||||||
|
|
||||||
|
// App-level composition: no UI tree - it only declares Window()s.
|
||||||
|
val appClock = Sdl3FrameClock()
|
||||||
|
val appRecomposer = Recomposer(coroutineContext + appClock)
|
||||||
|
// The clock must be in the collector's context - runRecomposeAndApplyChanges
|
||||||
|
// awaits parent frames through it.
|
||||||
|
val appRecomposeJob = launch(appClock) { appRecomposer.runRecomposeAndApplyChanges() }
|
||||||
|
val appComposition = Composition(UnitApplier(), appRecomposer)
|
||||||
|
val appScope = ApplicationScopeImpl(runtime)
|
||||||
|
|
||||||
|
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
|
||||||
|
// Only SCHEDULE a frame here; the apply is coalesced to once per loop
|
||||||
|
// iteration (top of loop + before each pump + before layout in
|
||||||
|
// renderFrame) instead of walking every snapshot observer on every
|
||||||
|
// individual state write. Mirrors upstream GlobalSnapshotManager,
|
||||||
|
// which schedules rather than applying inline.
|
||||||
|
runtime.markAllNeedFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
appComposition.setContent { appScope.content() }
|
||||||
|
// Compose the initial Window() declarations before entering the loop.
|
||||||
|
appClock.sendFrame(frameClockNanos())
|
||||||
|
yield()
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Main loop
|
||||||
|
var vGcLastTicks = SDL_GetTicks()
|
||||||
|
var vRenderedSinceGc = false
|
||||||
|
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()
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Events - one shared SDL queue, routed by window id.
|
||||||
|
val vEvents = pollEvents()
|
||||||
|
for (vEvent in vEvents) {
|
||||||
|
when (vEvent) {
|
||||||
|
is AppEvent.Quit -> {
|
||||||
|
// Platform quit = close request on every window; any
|
||||||
|
// veto (setOnCloseRequest returning false) keeps the
|
||||||
|
// app alive. No windows at all → plain exit.
|
||||||
|
if (runtime.windows.isEmpty()) runtime.exitRequested = true
|
||||||
|
else for (vW in runtime.windows.toList()) vW.requestClose()
|
||||||
|
}
|
||||||
|
is AppEvent.WindowClose -> runtime.windowFor(vEvent.windowId)?.requestClose()
|
||||||
|
is AppEvent.WindowResized -> runtime.windowFor(vEvent.windowId)?.onResizedEvent()
|
||||||
|
is AppEvent.RedrawNeeded -> runtime.windowFor(vEvent.windowId)?.let { it.needsFrame = true }
|
||||||
|
is AppEvent.SystemThemeChanged -> for (vW in runtime.windows) vW.onSystemThemeChanged()
|
||||||
|
is AppEvent.WindowActivation -> runtime.windowFor(vEvent.windowId)?.onActivationEvent(vEvent)
|
||||||
|
is AppEvent.Pointer -> runtime.windowFor(vEvent.windowId)?.onPointerEvent(vEvent)
|
||||||
|
is AppEvent.PointerExit -> runtime.windowFor(vEvent.windowId)?.onPointerExit()
|
||||||
|
is AppEvent.MouseWheel -> runtime.windowFor(vEvent.windowId)?.onWheelEvent(vEvent)
|
||||||
|
is AppEvent.Key -> runtime.windowFor(vEvent.windowId)?.onKeyEvent(vEvent)
|
||||||
|
is AppEvent.TextInput -> runtime.windowFor(vEvent.windowId)?.onTextInputEvent(vEvent)
|
||||||
|
is AppEvent.TextEditing -> runtime.windowFor(vEvent.windowId)?.onTextEditingEvent(vEvent)
|
||||||
|
is AppEvent.Drop -> runtime.windowFor(vEvent.windowId)?.onDropEvent(vEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mainDispatcher.drainPending()
|
||||||
|
FrameProfiler.phase("events")
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// App composition pump - Window()s may appear / disappear here.
|
||||||
|
Snapshot.sendApplyNotifications()
|
||||||
|
appClock.sendFrame(frameClockNanos())
|
||||||
|
yield()
|
||||||
|
runtime.reapDestroyed()
|
||||||
|
|
||||||
|
// Window content can flip window.close() / probes can end the app.
|
||||||
|
for (vW in runtime.windows.toList()) {
|
||||||
|
if (vW.facade.isCloseRequested && !vW.closeDispatched) {
|
||||||
|
vW.closeDispatched = true
|
||||||
|
vW.onCloseRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit when the last window is gone (after at least one existed).
|
||||||
|
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
|
||||||
|
if (runtime.exitRequested) break
|
||||||
|
FrameProfiler.phase("app")
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Per-window pump + render.
|
||||||
|
var vAnyRendered = false
|
||||||
|
var vAllVsync = true
|
||||||
|
// Non-vsync fallback cap: the shortest frame interval among the
|
||||||
|
// rendered non-vsync windows' displays (60Hz default). Only used when
|
||||||
|
// no window is vsync-paced (Software renderer / vsync unavailable).
|
||||||
|
var vFallbackDelayMs = 16u
|
||||||
|
val vAppPending = appRecomposer.hasPendingWork
|
||||||
|
for (vW in runtime.windows.toList()) {
|
||||||
|
vW.installGlobals()
|
||||||
|
vW.host.sendAnimationFrame(animationClockNanos())
|
||||||
|
mainDispatcher.drainPending()
|
||||||
|
Snapshot.sendApplyNotifications()
|
||||||
|
vW.frameClock.sendFrame(frameClockNanos())
|
||||||
|
yield()
|
||||||
|
FrameProfiler.phase("pump")
|
||||||
|
if (vW.shouldRender()) {
|
||||||
|
vW.renderFrame()
|
||||||
|
vAnyRendered = true
|
||||||
|
if (!vW.backend.vsyncEnabled) {
|
||||||
|
vAllVsync = false
|
||||||
|
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FrameProfiler.phase("render")
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
if (vAnyRendered) {
|
||||||
|
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
|
||||||
|
} else if (!vAppPending) {
|
||||||
|
// Nothing invalidated anywhere: block for events instead of
|
||||||
|
// spinning. A real input/redraw event wakes this immediately; the
|
||||||
|
// timeout only bounds how often async work (dispatcher/timers) is
|
||||||
|
// re-checked while truly idle, so keep it coarse to cut wakeups.
|
||||||
|
SDL_WaitEventTimeout(null, 100)
|
||||||
|
for (vW in runtime.windows) vW.resetFpsWindow()
|
||||||
|
} else {
|
||||||
|
// App composition has pending work but nothing rendered this
|
||||||
|
// iteration - yield briefly instead of spinning at full speed
|
||||||
|
// until the composition settles into a renderable state.
|
||||||
|
SDL_Delay(1u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Teardown
|
||||||
|
snapshotHandle.dispose()
|
||||||
|
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
|
||||||
|
runtime.reapDestroyed()
|
||||||
|
com.compose.sdl.graphics.NativeReleaseQueue.drain()
|
||||||
|
appComposition.dispose()
|
||||||
|
appRecomposer.cancel()
|
||||||
|
appRecomposeJob.cancelAndJoin()
|
||||||
|
}
|
||||||
|
|
||||||
|
mainDispatcher.close()
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
|
||||||
|
SDL_Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Single-window compatibility wrapper - the pre-multi-window entry point.
|
||||||
|
Closing the window exits the app, exactly as before. */
|
||||||
|
fun nativeComposeWindow(
|
||||||
|
title: String = "ComposeNativeSDL3",
|
||||||
|
width: Int = 800,
|
||||||
|
height: Int = 600,
|
||||||
|
gpu: GpuMode = GpuMode.Auto,
|
||||||
|
icon: AppWindowIcon? = null,
|
||||||
|
undecorated: Boolean = false,
|
||||||
|
transparent: Boolean = false,
|
||||||
|
resizable: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
focusable: Boolean = true,
|
||||||
|
alwaysOnTop: Boolean = false,
|
||||||
|
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
||||||
|
content: @Composable ComposeWindowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
nativeComposeApp {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = { exitApplication() },
|
||||||
|
title = title,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
gpu = gpu,
|
||||||
|
icon = icon,
|
||||||
|
undecorated = undecorated,
|
||||||
|
transparent = transparent,
|
||||||
|
resizable = resizable,
|
||||||
|
enabled = enabled,
|
||||||
|
focusable = focusable,
|
||||||
|
alwaysOnTop = alwaysOnTop,
|
||||||
|
onPreviewKeyEvent = onPreviewKeyEvent,
|
||||||
|
onKeyEvent = onKeyEvent,
|
||||||
|
onFrame = onFrame,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-29
@@ -9,65 +9,65 @@ import sdl3.SDL_GetTicks
|
|||||||
// MARK: FrameProfiler
|
// MARK: FrameProfiler
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** CDN_PROFILE=1 — per-phase timings, printed every ~2s of rendered frames.
|
/** 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)
|
A named-phase SINGLETON so both the main loop (events / app / pump / render)
|
||||||
AND renderFrame's sub-steps (render.layout / render.draw / render.present)
|
AND renderFrame's sub-steps (render.layout / render.draw / render.present)
|
||||||
report into one line. `mark()` resets the stopwatch; `phase(name)` charges
|
report into one line. `mark()` resets the stopwatch; `phase(name)` charges
|
||||||
the elapsed since the last mark/phase to that name. Measure first, optimize
|
the elapsed since the last mark/phase to that name. Measure first, optimize
|
||||||
second — see ROADMAP.md. */
|
second - see ROADMAP.md. */
|
||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
internal object FrameProfiler {
|
internal object FrameProfiler {
|
||||||
// null until first checked; then true/false for the run's lifetime.
|
// null until first checked; then true/false for the run's lifetime.
|
||||||
private var fEnabled: Boolean? = null
|
private var mEnabled: Boolean? = null
|
||||||
// Output file — resolved once from CDN_PROFILE. Writing to a file (not
|
// Output file - resolved once from CDN_PROFILE. Writing to a file (not
|
||||||
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
|
// 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
|
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
|
||||||
// the cwd; CDN_PROFILE=<path> → that path.
|
// the cwd; CDN_PROFILE=<path> → that path.
|
||||||
private var fPath: String = "cdn_profile.log"
|
private var mPath: String = "cdn_profile.log"
|
||||||
val enabled: Boolean
|
val enabled: Boolean
|
||||||
get() = fEnabled ?: run {
|
get() = mEnabled ?: run {
|
||||||
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
||||||
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv
|
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) mPath = vEnv
|
||||||
(vEnv != null).also { fEnabled = it }
|
(vEnv != null).also { mEnabled = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
|
private val mFreq = SDL_GetPerformanceFrequency().toDouble()
|
||||||
// Insertion-ordered so the printed line follows the call order.
|
// Insertion-ordered so the printed line follows the call order.
|
||||||
private val fSum = LinkedHashMap<String, Double>()
|
private val mSum = LinkedHashMap<String, Double>()
|
||||||
private val fMax = LinkedHashMap<String, Double>()
|
private val mMax = LinkedHashMap<String, Double>()
|
||||||
private var fFrames = 0
|
private var mFrames = 0
|
||||||
private var fLastPrintMs = SDL_GetTicks()
|
private var mLastPrintMs = SDL_GetTicks()
|
||||||
private var fMark = 0uL
|
private var mMark = 0uL
|
||||||
|
|
||||||
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() }
|
fun mark() { if (enabled) mMark = SDL_GetPerformanceCounter() }
|
||||||
|
|
||||||
fun phase(inName: String) {
|
fun phase(inName: String) {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
val vNow = SDL_GetPerformanceCounter()
|
val vNow = SDL_GetPerformanceCounter()
|
||||||
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
|
val vMs = (vNow - mMark).toDouble() * 1000.0 / mFreq
|
||||||
fSum[inName] = (fSum[inName] ?: 0.0) + vMs
|
mSum[inName] = (mSum[inName] ?: 0.0) + vMs
|
||||||
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs
|
if (vMs > (mMax[inName] ?: 0.0)) mMax[inName] = vMs
|
||||||
fMark = vNow
|
mMark = vNow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun frameDone(inRendered: Boolean) {
|
fun frameDone(inRendered: Boolean) {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
if (inRendered) fFrames++
|
if (inRendered) mFrames++
|
||||||
val vNowMs = SDL_GetTicks()
|
val vNowMs = SDL_GetTicks()
|
||||||
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
|
if (vNowMs - mLastPrintMs >= 2000u && mFrames > 0) {
|
||||||
val vParts = fSum.keys.map { vName ->
|
val vParts = mSum.keys.map { vName ->
|
||||||
val vAvg = (fSum[vName] ?: 0.0) / fFrames
|
val vAvg = (mSum[vName] ?: 0.0) / mFrames
|
||||||
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
"$vName=${(vAvg * 100).toInt() / 100.0}/${((mMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
||||||
}
|
}
|
||||||
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + "\n"
|
val vLine = "[profile] frames=$mFrames avg/max " + vParts.joinToString(" ") + "\n"
|
||||||
val vFile = platform.posix.fopen(fPath, "a")
|
val vFile = platform.posix.fopen(mPath, "a")
|
||||||
if (vFile != null) {
|
if (vFile != null) {
|
||||||
platform.posix.fputs(vLine, vFile)
|
platform.posix.fputs(vLine, vFile)
|
||||||
platform.posix.fclose(vFile)
|
platform.posix.fclose(vFile)
|
||||||
}
|
}
|
||||||
fSum.clear(); fMax.clear()
|
mSum.clear(); mMax.clear()
|
||||||
fFrames = 0
|
mFrames = 0
|
||||||
fLastPrintMs = vNowMs
|
mLastPrintMs = vNowMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import kotlinx.cinterop.COpaquePointer
|
||||||
|
import kotlinx.cinterop.reinterpret
|
||||||
|
import kotlinx.cinterop.pointed
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import sdl3.SDL_GetCurrentDisplayMode
|
||||||
|
import sdl3.SDL_GetDisplayForWindow
|
||||||
|
import sdl3.SDL_GetTicks
|
||||||
|
import sdl3.SDL_GetTicksNS
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Deterministic frame timing + probe/screenshot support
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/** 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. */
|
||||||
|
internal 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
|
||||||
|
|
||||||
|
internal fun advanceVirtualFrame() {
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
|
||||||
|
internal 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.
|
||||||
|
internal 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".
|
||||||
|
internal 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
|
||||||
|
|
||||||
|
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
||||||
|
their native memory (see the main loop's native-memory nudge). */
|
||||||
|
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||||
|
internal fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
||||||
|
|
||||||
|
/** Frame interval (ms) of the window's current display, for the non-vsync
|
||||||
|
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
|
||||||
|
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
|
||||||
|
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
|
internal fun displayFrameDelayMs(window: COpaquePointer?): UInt {
|
||||||
|
val vWindow = window ?: return 16u
|
||||||
|
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
|
||||||
|
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
|
||||||
|
val vHz = vMode.pointed.refresh_rate
|
||||||
|
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
|
||||||
|
}
|
||||||
+3
-3
@@ -6,7 +6,7 @@ import sdl3.SDL_GetTicksNS
|
|||||||
|
|
||||||
/** MonotonicFrameClock for the SDL main loop, with upstream frame semantics:
|
/** MonotonicFrameClock for the SDL main loop, with upstream frame semantics:
|
||||||
|
|
||||||
- withFrameNanos ALWAYS suspends until the NEXT sendFrame — never completes
|
- withFrameNanos ALWAYS suspends until the NEXT sendFrame - never completes
|
||||||
with a stale frame. Deferred animations (AnimatedVisibility's enter/exit
|
with a stale frame. Deferred animations (AnimatedVisibility's enter/exit
|
||||||
size + fade, SharedTransition bounds) are registered during the
|
size + fade, SharedTransition bounds) are registered during the
|
||||||
measure/draw pass AFTER the recomposition that starts a Transition; the
|
measure/draw pass AFTER the recomposition that starts a Transition; the
|
||||||
@@ -20,7 +20,7 @@ import sdl3.SDL_GetTicksNS
|
|||||||
|
|
||||||
Both come from delegating to the runtime's BroadcastFrameClock, which is
|
Both come from delegating to the runtime's BroadcastFrameClock, which is
|
||||||
exactly upstream's dispatch structure. */
|
exactly upstream's dispatch structure. */
|
||||||
internal class SDL3FrameClock : MonotonicFrameClock {
|
internal class Sdl3FrameClock : MonotonicFrameClock {
|
||||||
private val broadcast = BroadcastFrameClock()
|
private val broadcast = BroadcastFrameClock()
|
||||||
|
|
||||||
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
||||||
@@ -31,7 +31,7 @@ internal class SDL3FrameClock : MonotonicFrameClock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Coroutines (recomposer / composition animations) currently suspended in withFrameNanos
|
// Coroutines (recomposer / composition animations) currently suspended in withFrameNanos
|
||||||
// awaiting the next sendFrame — the "an animation is still running" half of the window's
|
// awaiting the next sendFrame - the "an animation is still running" half of the window's
|
||||||
// quiescence signal (see WindowInstance.hasInvalidations).
|
// quiescence signal (see WindowInstance.hasInvalidations).
|
||||||
val hasAwaiters: Boolean get() = broadcast.hasAwaiters
|
val hasAwaiters: Boolean get() = broadcast.hasAwaiters
|
||||||
}
|
}
|
||||||
+11
-11
@@ -12,7 +12,7 @@ import sdl3.SDL_GetCurrentThreadID
|
|||||||
|
|
||||||
/** Single-threaded Main dispatcher driven by the SDL3 main loop. Kotlin/Native
|
/** Single-threaded Main dispatcher driven by the SDL3 main loop. Kotlin/Native
|
||||||
ships no Dispatchers.Main on Linux/Windows targets (and the Darwin one
|
ships no Dispatchers.Main on Linux/Windows targets (and the Darwin one
|
||||||
posts to GCD's main queue, which only drains when the run loop pumps —
|
posts to GCD's main queue, which only drains when the run loop pumps -
|
||||||
our SDL_Delay-based loop doesn't reliably pump it). nativeComposeWindow
|
our SDL_Delay-based loop doesn't reliably pump it). nativeComposeWindow
|
||||||
installs this dispatcher via kotlinx.coroutines.test.setMain at startup
|
installs this dispatcher via kotlinx.coroutines.test.setMain at startup
|
||||||
and resets it on shutdown, so app code can withContext(Dispatchers.Main)
|
and resets it on shutdown, so app code can withContext(Dispatchers.Main)
|
||||||
@@ -24,28 +24,28 @@ import sdl3.SDL_GetCurrentThreadID
|
|||||||
(Dispatchers.IO, Default, etc.) are safe.
|
(Dispatchers.IO, Default, etc.) are safe.
|
||||||
|
|
||||||
`immediate` is a REAL immediate dispatcher: its isDispatchNeeded() returns
|
`immediate` is a REAL immediate dispatcher: its isDispatchNeeded() returns
|
||||||
false when already on the SDL main thread, so work runs inline — matching
|
false when already on the SDL main thread, so work runs inline - matching
|
||||||
Android's Main.immediate / Swing's EDT semantics. This is load-bearing:
|
Android's Main.immediate / Swing's EDT semantics. This is load-bearing:
|
||||||
androidx.lifecycle's KMP LifecycleRegistry enforces main-thread access by
|
androidx.lifecycle's KMP LifecycleRegistry enforces main-thread access by
|
||||||
round-tripping through Dispatchers.Main.immediate (MainDispatcherChecker);
|
round-tripping through Dispatchers.Main.immediate (MainDispatcherChecker);
|
||||||
with a queue-only Main that trip can never complete from the main thread
|
with a queue-only Main that trip can never complete from the main thread
|
||||||
while the loop is inside setContent, deadlocking the app at 0% CPU (this
|
while the loop is inside setContent, deadlocking the app at 0% CPU (this
|
||||||
froze Navigation3's NavDisplay — rememberLifecycleOwner — on mingwX64,
|
froze Navigation3's NavDisplay - rememberLifecycleOwner - on mingwX64,
|
||||||
see NAV_FIX.md). The BASE dispatcher intentionally KEEPS always-queue
|
see NAV_FIX.md). The BASE dispatcher intentionally KEEPS always-queue
|
||||||
semantics so LaunchedEffect / recomposer ordering is unchanged: state
|
semantics so LaunchedEffect / recomposer ordering is unchanged: state
|
||||||
writes land at the loop's drainPending(), observable on the same frame. */
|
writes land at the loop's drainPending(), observable on the same frame. */
|
||||||
internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
||||||
|
|
||||||
private val fQueue = Channel<Runnable>(Channel.UNLIMITED)
|
private val mQueue = Channel<Runnable>(Channel.UNLIMITED)
|
||||||
|
|
||||||
// The SDL main thread — the dispatcher is constructed by nativeComposeApp
|
// The SDL main thread - the dispatcher is constructed by nativeComposeApp
|
||||||
// on the thread that then runs the loop.
|
// on the thread that then runs the loop.
|
||||||
private val fMainThreadId = SDL_GetCurrentThreadID()
|
private val mMainThreadId = SDL_GetCurrentThreadID()
|
||||||
|
|
||||||
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher()
|
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher()
|
||||||
|
|
||||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||||
fQueue.trySend(block)
|
mQueue.trySend(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drains all pending tasks. Called from the SDL3 main loop once per
|
/** Drains all pending tasks. Called from the SDL3 main loop once per
|
||||||
@@ -54,7 +54,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
composition. */
|
composition. */
|
||||||
fun drainPending() {
|
fun drainPending() {
|
||||||
while (true) {
|
while (true) {
|
||||||
val vTask = fQueue.tryReceive().getOrNull() ?: break
|
val vTask = mQueue.tryReceive().getOrNull() ?: break
|
||||||
runCatching { vTask.run() }.onFailure { vErr ->
|
runCatching { vTask.run() }.onFailure { vErr ->
|
||||||
println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}")
|
println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}")
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
/** Closes the queue. After this, dispatch() drops new posts silently.
|
/** Closes the queue. After this, dispatch() drops new posts silently.
|
||||||
Called from nativeComposeWindow as part of shutdown. */
|
Called from nativeComposeWindow as part of shutdown. */
|
||||||
fun close() {
|
fun close() {
|
||||||
fQueue.close()
|
mQueue.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Sdl3MainDispatcher"
|
override fun toString(): String = "Sdl3MainDispatcher"
|
||||||
@@ -75,10 +75,10 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
override val immediate: MainCoroutineDispatcher get() = this
|
override val immediate: MainCoroutineDispatcher get() = this
|
||||||
|
|
||||||
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
|
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
|
||||||
SDL_GetCurrentThreadID() != fMainThreadId
|
SDL_GetCurrentThreadID() != mMainThreadId
|
||||||
|
|
||||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||||
fQueue.trySend(block)
|
mQueue.trySend(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Sdl3MainDispatcher.immediate"
|
override fun toString(): String = "Sdl3MainDispatcher.immediate"
|
||||||
+5
-5
@@ -13,11 +13,11 @@ import androidx.lifecycle.enableSavedStateHandles
|
|||||||
default ViewModel factory), so viewModel(), SavedStateHandle and
|
default ViewModel factory), so viewModel(), SavedStateHandle and
|
||||||
rememberSaveable-backed registries all resolve against the WINDOW scope.
|
rememberSaveable-backed registries all resolve against the WINDOW scope.
|
||||||
|
|
||||||
The lifecycle registry uses createUnsafe (no main-thread enforcement) —
|
The lifecycle registry uses createUnsafe (no main-thread enforcement) -
|
||||||
same as the root owner this replaces; the SDL loop is single-threaded
|
same as the root owner this replaces; the SDL loop is single-threaded
|
||||||
anyway. RESUMED from construction; destroy() moves to DESTROYED and clears
|
anyway. RESUMED from construction; destroy() moves to DESTROYED and clears
|
||||||
the ViewModelStore (onCleared runs). SavedState restores from nothing (no
|
the ViewModelStore (onCleared runs). SavedState restores from nothing (no
|
||||||
process-death persistence on desktop — upstream desktop passes null too). */
|
process-death persistence on desktop - upstream desktop passes null too). */
|
||||||
internal class WindowArchitectureOwner :
|
internal class WindowArchitectureOwner :
|
||||||
androidx.lifecycle.LifecycleOwner,
|
androidx.lifecycle.LifecycleOwner,
|
||||||
androidx.lifecycle.ViewModelStoreOwner,
|
androidx.lifecycle.ViewModelStoreOwner,
|
||||||
@@ -41,13 +41,13 @@ internal class WindowArchitectureOwner :
|
|||||||
init {
|
init {
|
||||||
savedStateController.performAttach()
|
savedStateController.performAttach()
|
||||||
savedStateController.performRestore(null)
|
savedStateController.performRestore(null)
|
||||||
// SavedStateHandle support for WINDOW-scoped ViewModels — must run while
|
// SavedStateHandle support for WINDOW-scoped ViewModels - must run while
|
||||||
// the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer
|
// the lifecycle is still ≤ CREATED; upstream desktop's ComposeContainer
|
||||||
// calls this at the same point. With it, `viewModel { ... }` against the
|
// calls this at the same point. With it, `viewModel { ... }` against the
|
||||||
// window owner (the activityViewModels() analog) can take a
|
// window owner (the activityViewModels() analog) can take a
|
||||||
// SavedStateHandle instead of needing a saved-state-less child owner.
|
// SavedStateHandle instead of needing a saved-state-less child owner.
|
||||||
enableSavedStateHandles()
|
enableSavedStateHandles()
|
||||||
// CREATED (not RESUMED) until the first composition is done — code that
|
// CREATED (not RESUMED) until the first composition is done - code that
|
||||||
// runs enableSavedStateHandles() during composition (nav3's decorators,
|
// runs enableSavedStateHandles() during composition (nav3's decorators,
|
||||||
// rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and
|
// rememberViewModelStoreOwner, …) requires INITIALIZED/CREATED, and
|
||||||
// upstream desktop windows likewise compose first and resume after.
|
// upstream desktop windows likewise compose first and resume after.
|
||||||
@@ -55,7 +55,7 @@ internal class WindowArchitectureOwner :
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Focus/visibility-driven state (see WindowInstance.onActivationEvent).
|
/** Focus/visibility-driven state (see WindowInstance.onActivationEvent).
|
||||||
Ignored once destroyed — a stray SDL event during teardown must not
|
Ignored once destroyed - a stray SDL event during teardown must not
|
||||||
resurrect the registry. */
|
resurrect the registry. */
|
||||||
fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) {
|
fun setLifecycleState(inState: androidx.lifecycle.Lifecycle.State) {
|
||||||
if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED &&
|
if (lifecycle.currentState != androidx.lifecycle.Lifecycle.State.DESTROYED &&
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: WindowAttributes
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Compose Desktop `Window()` attributes this port supports, bundled so they
|
||||||
|
travel as one value from the composable down to [WindowInstance] instead of as
|
||||||
|
six positional booleans.
|
||||||
|
|
||||||
|
[transparent] is creation-only: SDL needs "SDL_WINDOW_TRANSPARENT" at
|
||||||
|
"SDL_CreateWindow" time and offers no setter, so changing it after the window
|
||||||
|
exists has no effect. The rest are re-applied whenever "Window()" recomposes
|
||||||
|
with a new value.
|
||||||
|
*/
|
||||||
|
internal data class WindowAttributes(
|
||||||
|
/** Hides the title bar and border (`SDL_WINDOW_BORDERLESS`). */
|
||||||
|
val undecorated: Boolean = false,
|
||||||
|
/** Per-pixel alpha for the window surface. CREATION-ONLY - see above. */
|
||||||
|
val transparent: Boolean = false,
|
||||||
|
/** Whether the user can resize the window. */
|
||||||
|
val resizable: Boolean = true,
|
||||||
|
/** False drops input (pointer / key / text / wheel / drop) while the window
|
||||||
|
keeps rendering - Compose Desktop's `enabled = false` behaviour. */
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
/** False stops the window taking keyboard focus when clicked or raised. */
|
||||||
|
val focusable: Boolean = true,
|
||||||
|
/** Keeps the window above other windows. */
|
||||||
|
val alwaysOnTop: Boolean = false,
|
||||||
|
)
|
||||||
+72
-486
@@ -7,19 +7,14 @@ package com.compose.sdl
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.AbstractApplier
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Composition
|
import androidx.compose.runtime.Composition
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Recomposer
|
import androidx.compose.runtime.Recomposer
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.runtime.snapshots.Snapshot
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
|
||||||
import androidx.compose.ui.input.pointer.PointerButton
|
import androidx.compose.ui.input.pointer.PointerButton
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import com.compose.sdl.node.ComposeRootHost
|
import com.compose.sdl.node.ComposeRootHost
|
||||||
@@ -27,457 +22,17 @@ import com.compose.sdl.res.currentImageLoader
|
|||||||
import com.compose.sdl.window.LocalPopupHost
|
import com.compose.sdl.window.LocalPopupHost
|
||||||
import com.compose.sdl.window.PopupLayer
|
import com.compose.sdl.window.PopupLayer
|
||||||
import com.compose.sdl.window.createPopupHostState
|
import com.compose.sdl.window.createPopupHostState
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.test.resetMain
|
|
||||||
import kotlinx.coroutines.test.setMain
|
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
import kotlinx.cinterop.reinterpret
|
|
||||||
import kotlinx.cinterop.alloc
|
import kotlinx.cinterop.alloc
|
||||||
import kotlinx.cinterop.ptr
|
import kotlinx.cinterop.ptr
|
||||||
import kotlinx.cinterop.COpaquePointer
|
import kotlinx.cinterop.reinterpret
|
||||||
import kotlinx.cinterop.pointed
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import sdl3.SDL_Delay
|
import kotlinx.coroutines.Job
|
||||||
import sdl3.SDL_GetCurrentDisplayMode
|
import kotlinx.coroutines.launch
|
||||||
import sdl3.SDL_GetDisplayForWindow
|
|
||||||
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_SetWindowTitle
|
import sdl3.SDL_SetWindowTitle
|
||||||
import sdl3.SDL_WaitEventTimeout
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Application entry — nativeComposeApp + Window()
|
// MARK: WindowInstance - one SDL window + renderer + composition
|
||||||
// ==================
|
|
||||||
|
|
||||||
/**
|
|
||||||
Multi-window entry point, shaped like Compose Desktop's `application {}`:
|
|
||||||
|
|
||||||
nativeComposeApp {
|
|
||||||
var showTools by remember { mutableStateOf(false) }
|
|
||||||
Window(onCloseRequest = ::exitApplication, title = "Main") { MainUi(onTools = { showTools = true }) }
|
|
||||||
if (showTools) Window(onCloseRequest = { showTools = false }, title = "Tools") { ToolsUi() }
|
|
||||||
}
|
|
||||||
|
|
||||||
The app CONTENT is itself a composition (no UI tree — a Unit applier): each
|
|
||||||
`Window(...)` call materialises an SDL window + renderer + root host + its own
|
|
||||||
recomposer/composition, and leaving the composition (state flips to false)
|
|
||||||
tears the window down. One main loop pumps the shared SDL event queue and
|
|
||||||
routes events per SDL window id.
|
|
||||||
|
|
||||||
Per-window pumping order matters: the render-bridge globals
|
|
||||||
(currentImageLoader / viewport) are per-renderer, so the
|
|
||||||
loop installs a window's globals before recomposing / laying out / drawing it.
|
|
||||||
|
|
||||||
`nativeComposeWindow(...)` remains as the single-window wrapper (all demo
|
|
||||||
probes and apidemo ride it unchanged).
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ApplicationScope {
|
|
||||||
/** Requests the main loop to exit after the current iteration (all windows
|
|
||||||
are torn down on the way out). */
|
|
||||||
fun exitApplication()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Declares one native window for as long as this composable stays in the app
|
|
||||||
composition. `onCloseRequest` fires when the user asks the window to close
|
|
||||||
(OS close button, or `window.close()` from content) — remove the state that
|
|
||||||
composes this Window to actually close it, or call exitApplication(). */
|
|
||||||
@Composable
|
|
||||||
fun ApplicationScope.Window(
|
|
||||||
onCloseRequest: () -> Unit,
|
|
||||||
title: String = "ComposeNativeSDL3",
|
|
||||||
width: Int = 800,
|
|
||||||
height: Int = 600,
|
|
||||||
gpu: GpuMode = GpuMode.Auto,
|
|
||||||
icon: AppWindowIcon? = null,
|
|
||||||
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
|
||||||
content: @Composable ComposeWindowScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
val vScope = this as? ApplicationScopeImpl
|
|
||||||
?: error("Window() must be called inside nativeComposeApp { ... }")
|
|
||||||
val vContent = rememberUpdatedState(content)
|
|
||||||
val vClose = rememberUpdatedState(onCloseRequest)
|
|
||||||
val vWindow = remember {
|
|
||||||
vScope.runtime.createWindow(
|
|
||||||
inTitle = title,
|
|
||||||
inWidth = width,
|
|
||||||
inHeight = height,
|
|
||||||
inGpu = gpu,
|
|
||||||
inIcon = icon,
|
|
||||||
inOnFrame = onFrame,
|
|
||||||
// The window composition reads the holder each recomposition, so
|
|
||||||
// updated content lambdas propagate without recreating the window.
|
|
||||||
inContent = { vContent.value },
|
|
||||||
).also { it.onCloseRequest = { vClose.value.invoke() } }
|
|
||||||
}
|
|
||||||
SideEffect {
|
|
||||||
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
|
|
||||||
}
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots SDL + the Compose runtime, runs `content` as the application
|
|
||||||
composition, and drives every declared Window until exitApplication() or
|
|
||||||
the last window closes. */
|
|
||||||
fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|
||||||
// Install the Main dispatcher BEFORE any window/owner exists: ComposeOwner
|
|
||||||
// captures Dispatchers.Main eagerly for its per-node coroutine scopes.
|
|
||||||
val mainDispatcher = Sdl3MainDispatcher()
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
Dispatchers.setMain(mainDispatcher)
|
|
||||||
|
|
||||||
val runtime = AppRuntime()
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
runtime.scope = this
|
|
||||||
|
|
||||||
// App-level composition: no UI tree — it only declares Window()s.
|
|
||||||
val appClock = SDL3FrameClock()
|
|
||||||
val appRecomposer = Recomposer(coroutineContext + appClock)
|
|
||||||
// The clock must be in the collector's context — runRecomposeAndApplyChanges
|
|
||||||
// awaits parent frames through it.
|
|
||||||
val appRecomposeJob = launch(appClock) { appRecomposer.runRecomposeAndApplyChanges() }
|
|
||||||
val appComposition = Composition(UnitApplier(), appRecomposer)
|
|
||||||
val appScope = ApplicationScopeImpl(runtime)
|
|
||||||
|
|
||||||
val snapshotHandle = Snapshot.registerGlobalWriteObserver {
|
|
||||||
// Only SCHEDULE a frame here; the apply is coalesced to once per loop
|
|
||||||
// iteration (top of loop + before each pump + before layout in
|
|
||||||
// renderFrame) instead of walking every snapshot observer on every
|
|
||||||
// individual state write. Mirrors upstream GlobalSnapshotManager,
|
|
||||||
// which schedules rather than applying inline.
|
|
||||||
runtime.markAllNeedFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
appComposition.setContent { appScope.content() }
|
|
||||||
// Compose the initial Window() declarations before entering the loop.
|
|
||||||
appClock.sendFrame(frameClockNanos())
|
|
||||||
yield()
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Main loop
|
|
||||||
var vGcLastTicks = SDL_GetTicks()
|
|
||||||
var vRenderedSinceGc = false
|
|
||||||
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()
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Events — one shared SDL queue, routed by window id.
|
|
||||||
val vEvents = pollEvents()
|
|
||||||
for (vEvent in vEvents) {
|
|
||||||
when (vEvent) {
|
|
||||||
is AppEvent.Quit -> {
|
|
||||||
// Platform quit = close request on every window; any
|
|
||||||
// veto (setOnCloseRequest returning false) keeps the
|
|
||||||
// app alive. No windows at all → plain exit.
|
|
||||||
if (runtime.windows.isEmpty()) runtime.exitRequested = true
|
|
||||||
else for (vW in runtime.windows.toList()) vW.requestClose()
|
|
||||||
}
|
|
||||||
is AppEvent.WindowClose -> runtime.windowFor(vEvent.windowId)?.requestClose()
|
|
||||||
is AppEvent.WindowResized -> runtime.windowFor(vEvent.windowId)?.onResizedEvent()
|
|
||||||
is AppEvent.RedrawNeeded -> runtime.windowFor(vEvent.windowId)?.let { it.needsFrame = true }
|
|
||||||
is AppEvent.SystemThemeChanged -> for (vW in runtime.windows) vW.onSystemThemeChanged()
|
|
||||||
is AppEvent.WindowActivation -> runtime.windowFor(vEvent.windowId)?.onActivationEvent(vEvent)
|
|
||||||
is AppEvent.Pointer -> runtime.windowFor(vEvent.windowId)?.onPointerEvent(vEvent)
|
|
||||||
is AppEvent.PointerExit -> runtime.windowFor(vEvent.windowId)?.onPointerExit()
|
|
||||||
is AppEvent.MouseWheel -> runtime.windowFor(vEvent.windowId)?.onWheelEvent(vEvent)
|
|
||||||
is AppEvent.Key -> runtime.windowFor(vEvent.windowId)?.onKeyEvent(vEvent)
|
|
||||||
is AppEvent.TextInput -> runtime.windowFor(vEvent.windowId)?.onTextInputEvent(vEvent)
|
|
||||||
is AppEvent.TextEditing -> runtime.windowFor(vEvent.windowId)?.onTextEditingEvent(vEvent)
|
|
||||||
is AppEvent.Drop -> runtime.windowFor(vEvent.windowId)?.onDropEvent(vEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mainDispatcher.drainPending()
|
|
||||||
FrameProfiler.phase("events")
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// App composition pump — Window()s may appear / disappear here.
|
|
||||||
Snapshot.sendApplyNotifications()
|
|
||||||
appClock.sendFrame(frameClockNanos())
|
|
||||||
yield()
|
|
||||||
runtime.reapDestroyed()
|
|
||||||
|
|
||||||
// Window content can flip window.close() / probes can end the app.
|
|
||||||
for (vW in runtime.windows.toList()) {
|
|
||||||
if (vW.facade.isCloseRequested && !vW.closeDispatched) {
|
|
||||||
vW.closeDispatched = true
|
|
||||||
vW.onCloseRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit when the last window is gone (after at least one existed).
|
|
||||||
if (runtime.hadWindow && runtime.windows.isEmpty()) runtime.exitRequested = true
|
|
||||||
if (runtime.exitRequested) break
|
|
||||||
FrameProfiler.phase("app")
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Per-window pump + render.
|
|
||||||
var vAnyRendered = false
|
|
||||||
var vAllVsync = true
|
|
||||||
// Non-vsync fallback cap: the shortest frame interval among the
|
|
||||||
// rendered non-vsync windows' displays (60Hz default). Only used when
|
|
||||||
// no window is vsync-paced (Software renderer / vsync unavailable).
|
|
||||||
var vFallbackDelayMs = 16u
|
|
||||||
val vAppPending = appRecomposer.hasPendingWork
|
|
||||||
for (vW in runtime.windows.toList()) {
|
|
||||||
vW.installGlobals()
|
|
||||||
vW.host.sendAnimationFrame(animationClockNanos())
|
|
||||||
mainDispatcher.drainPending()
|
|
||||||
Snapshot.sendApplyNotifications()
|
|
||||||
vW.frameClock.sendFrame(frameClockNanos())
|
|
||||||
yield()
|
|
||||||
FrameProfiler.phase("pump")
|
|
||||||
if (vW.shouldRender()) {
|
|
||||||
vW.renderFrame()
|
|
||||||
vAnyRendered = true
|
|
||||||
if (!vW.backend.vsyncEnabled) {
|
|
||||||
vAllVsync = false
|
|
||||||
vFallbackDelayMs = minOf(vFallbackDelayMs, displayFrameDelayMs(vW.backend.window))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FrameProfiler.phase("render")
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
if (vAnyRendered) {
|
|
||||||
SDL_Delay(if (vAllVsync) 1u else vFallbackDelayMs)
|
|
||||||
} else if (!vAppPending) {
|
|
||||||
// Nothing invalidated anywhere: block for events instead of
|
|
||||||
// spinning. A real input/redraw event wakes this immediately; the
|
|
||||||
// timeout only bounds how often async work (dispatcher/timers) is
|
|
||||||
// re-checked while truly idle, so keep it coarse to cut wakeups.
|
|
||||||
SDL_WaitEventTimeout(null, 100)
|
|
||||||
for (vW in runtime.windows) vW.resetFpsWindow()
|
|
||||||
} else {
|
|
||||||
// App composition has pending work but nothing rendered this
|
|
||||||
// iteration — yield briefly instead of spinning at full speed
|
|
||||||
// until the composition settles into a renderable state.
|
|
||||||
SDL_Delay(1u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Teardown
|
|
||||||
snapshotHandle.dispose()
|
|
||||||
for (vW in runtime.windows.toList()) runtime.scheduleDestroy(vW)
|
|
||||||
runtime.reapDestroyed()
|
|
||||||
com.compose.sdl.graphics.NativeReleaseQueue.drain()
|
|
||||||
appComposition.dispose()
|
|
||||||
appRecomposer.cancel()
|
|
||||||
appRecomposeJob.cancelAndJoin()
|
|
||||||
}
|
|
||||||
|
|
||||||
mainDispatcher.close()
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
Dispatchers.resetMain()
|
|
||||||
|
|
||||||
SDL_Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// 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
|
|
||||||
|
|
||||||
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
|
||||||
their native memory (see the main loop's native-memory nudge). */
|
|
||||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
|
||||||
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
|
||||||
|
|
||||||
/** Frame interval (ms) of the window's current display, for the non-vsync
|
|
||||||
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
|
|
||||||
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
|
|
||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
|
||||||
private fun displayFrameDelayMs(window: COpaquePointer?): UInt {
|
|
||||||
val vWindow = window ?: return 16u
|
|
||||||
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
|
|
||||||
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
|
|
||||||
val vHz = vMode.pointed.refresh_rate
|
|
||||||
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Single-window compatibility wrapper — the pre-multi-window entry point.
|
|
||||||
Closing the window exits the app, exactly as before. */
|
|
||||||
fun nativeComposeWindow(
|
|
||||||
title: String = "ComposeNativeSDL3",
|
|
||||||
width: Int = 800,
|
|
||||||
height: Int = 600,
|
|
||||||
gpu: GpuMode = GpuMode.Auto,
|
|
||||||
icon: AppWindowIcon? = null,
|
|
||||||
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
|
||||||
content: @Composable ComposeWindowScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
nativeComposeApp {
|
|
||||||
Window(
|
|
||||||
onCloseRequest = { exitApplication() },
|
|
||||||
title = title,
|
|
||||||
width = width,
|
|
||||||
height = height,
|
|
||||||
gpu = gpu,
|
|
||||||
icon = icon,
|
|
||||||
onFrame = onFrame,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: App runtime internals
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
|
|
||||||
override fun exitApplication() { runtime.exitRequested = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Registry + lifecycle for the live windows. Windows are created by the app
|
|
||||||
composition (Window()'s remember) and destroyed by the LOOP — DisposableEffect
|
|
||||||
onDispose only schedules, because teardown disposes a composition and that
|
|
||||||
must not run re-entrantly inside another composition's apply pass. */
|
|
||||||
internal class AppRuntime {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
val windows = mutableListOf<WindowInstance>()
|
|
||||||
private val destroyQueue = mutableListOf<WindowInstance>()
|
|
||||||
var exitRequested = false
|
|
||||||
var hadWindow = false
|
|
||||||
|
|
||||||
fun windowFor(inId: UInt): WindowInstance? =
|
|
||||||
if (inId == 0u) windows.firstOrNull()
|
|
||||||
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
|
|
||||||
|
|
||||||
fun markAllNeedFrame() {
|
|
||||||
for (vW in windows) vW.needsFrame = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createWindow(
|
|
||||||
inTitle: String,
|
|
||||||
inWidth: Int,
|
|
||||||
inHeight: Int,
|
|
||||||
inGpu: GpuMode,
|
|
||||||
inIcon: AppWindowIcon?,
|
|
||||||
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
|
|
||||||
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
|
|
||||||
): WindowInstance {
|
|
||||||
val vWindow = WindowInstance(inTitle, inWidth, inHeight, inGpu, inIcon, inOnFrame, inContent)
|
|
||||||
// A Window() was declared either way — the "exit when the last window
|
|
||||||
// is gone" rule must also fire when every declared window failed to
|
|
||||||
// initialise (otherwise the loop would spin forever with none).
|
|
||||||
hadWindow = true
|
|
||||||
if (vWindow.init(scope)) {
|
|
||||||
windows.add(vWindow)
|
|
||||||
} else {
|
|
||||||
println("nativeComposeApp: window '$inTitle' failed to initialise")
|
|
||||||
}
|
|
||||||
return vWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scheduleDestroy(inWindow: WindowInstance) {
|
|
||||||
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reapDestroyed() {
|
|
||||||
while (destroyQueue.isNotEmpty()) {
|
|
||||||
val vW = destroyQueue.removeLast()
|
|
||||||
windows.remove(vW)
|
|
||||||
vW.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No-op applier for the app-level composition — it emits no UI nodes, only
|
|
||||||
// side effects (each Window() manages a native window).
|
|
||||||
private class UnitApplier : AbstractApplier<Unit>(Unit) {
|
|
||||||
override fun insertTopDown(index: Int, instance: Unit) {}
|
|
||||||
override fun insertBottomUp(index: Int, instance: Unit) {}
|
|
||||||
override fun remove(index: Int, count: Int) {}
|
|
||||||
override fun move(from: Int, to: Int, count: Int) {}
|
|
||||||
override fun onClear() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: WindowInstance — one SDL window + renderer + composition
|
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
internal class WindowInstance(
|
internal class WindowInstance(
|
||||||
@@ -486,7 +41,12 @@ internal class WindowInstance(
|
|||||||
inHeight: Int,
|
inHeight: Int,
|
||||||
inGpu: GpuMode,
|
inGpu: GpuMode,
|
||||||
inIcon: AppWindowIcon?,
|
inIcon: AppWindowIcon?,
|
||||||
|
private val attrs: WindowAttributes,
|
||||||
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
|
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
|
||||||
|
// Window-level key handlers, read through holders so Window() recompositions
|
||||||
|
// with new lambdas take effect without recreating the window.
|
||||||
|
private val previewKeyHandler: () -> ((KeyEvent) -> Boolean),
|
||||||
|
private val keyHandler: () -> ((KeyEvent) -> Boolean),
|
||||||
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
|
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
|
||||||
) {
|
) {
|
||||||
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
|
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
|
||||||
@@ -496,7 +56,7 @@ internal class WindowInstance(
|
|||||||
private val initialHeight = inHeight
|
private val initialHeight = inHeight
|
||||||
private val icon = inIcon
|
private val icon = inIcon
|
||||||
// Assigned by init() from the first backend that comes up (see makeBackend()).
|
// Assigned by init() from the first backend that comes up (see makeBackend()).
|
||||||
lateinit var backend: SDL3Backend
|
lateinit var backend: Sdl3Backend
|
||||||
private set
|
private set
|
||||||
private var renderBackend: RenderBackend? = null
|
private var renderBackend: RenderBackend? = null
|
||||||
lateinit var host: ComposeRootHost
|
lateinit var host: ComposeRootHost
|
||||||
@@ -505,7 +65,7 @@ internal class WindowInstance(
|
|||||||
private set
|
private set
|
||||||
private var popupHost: com.compose.sdl.window.PopupHostState? = null
|
private var popupHost: com.compose.sdl.window.PopupHostState? = null
|
||||||
|
|
||||||
val frameClock = SDL3FrameClock()
|
val frameClock = Sdl3FrameClock()
|
||||||
private var recomposer: Recomposer? = null
|
private var recomposer: Recomposer? = null
|
||||||
private var recomposeJob: Job? = null
|
private var recomposeJob: Job? = null
|
||||||
private var composition: Composition? = null
|
private var composition: Composition? = null
|
||||||
@@ -537,7 +97,7 @@ internal class WindowInstance(
|
|||||||
private val backNavigationInput = BackNavigationInput()
|
private val backNavigationInput = BackNavigationInput()
|
||||||
|
|
||||||
// WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore +
|
// WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore +
|
||||||
// SavedStateRegistry) — the same trio upstream desktop's
|
// SavedStateRegistry) - the same trio upstream desktop's
|
||||||
// DefaultArchitectureComponentsOwner supplies through the window
|
// DefaultArchitectureComponentsOwner supplies through the window
|
||||||
// PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt).
|
// PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt).
|
||||||
// viewModel(), SavedStateHandle plumbing and navigation3's
|
// viewModel(), SavedStateHandle plumbing and navigation3's
|
||||||
@@ -563,19 +123,24 @@ internal class WindowInstance(
|
|||||||
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Shown / restored / focus-gained also invalidate the contents — keep
|
// Shown / restored / focus-gained also invalidate the contents - keep
|
||||||
// the pre-lifecycle RedrawNeeded behaviour of these SDL events.
|
// the pre-lifecycle RedrawNeeded behaviour of these SDL events.
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates + initialises an SDL3Backend for [mode] and its RenderBackend. On
|
// Creates + initialises an Sdl3Backend for [mode] and its RenderBackend. On
|
||||||
// success commits `backend` and returns the renderer; else tears everything
|
// success commits `backend` and returns the renderer; else tears everything
|
||||||
// down and returns null so the caller can retry with a different mode.
|
// down and returns null so the caller can retry with a different mode.
|
||||||
private fun makeBackend(mode: GpuMode): RenderBackend? {
|
private fun makeBackend(mode: GpuMode): RenderBackend? {
|
||||||
val vBackend = SDL3Backend(
|
val vBackend = Sdl3Backend(
|
||||||
initialTitle, initialWidth, initialHeight, gpuMode = mode,
|
initialTitle, initialWidth, initialHeight, gpuMode = mode,
|
||||||
iconLightResourcePaths = icon?.light ?: emptyList(),
|
iconLightResourcePaths = icon?.light ?: emptyList(),
|
||||||
iconDarkResourcePaths = icon?.dark ?: emptyList(),
|
iconDarkResourcePaths = icon?.dark ?: emptyList(),
|
||||||
|
undecorated = attrs.undecorated,
|
||||||
|
transparent = attrs.transparent,
|
||||||
|
resizable = attrs.resizable,
|
||||||
|
alwaysOnTop = attrs.alwaysOnTop,
|
||||||
|
focusable = attrs.focusable,
|
||||||
)
|
)
|
||||||
if (!vBackend.init()) {
|
if (!vBackend.init()) {
|
||||||
vBackend.destroy(inQuitSdl = false)
|
vBackend.destroy(inQuitSdl = false)
|
||||||
@@ -597,9 +162,9 @@ internal class WindowInstance(
|
|||||||
// can't come up (RDP / headless / missing GL driver), fall back once to
|
// can't come up (RDP / headless / missing GL driver), fall back once to
|
||||||
// CPU raster (Software) so the window still opens instead of failing.
|
// CPU raster (Software) so the window still opens instead of failing.
|
||||||
var vRender = makeBackend(gpuMode)
|
var vRender = makeBackend(gpuMode)
|
||||||
if (vRender == null && gpuMode is GpuMode.Skia) {
|
if (vRender == null && gpuMode.isGpuAccelerated) {
|
||||||
println("GPU renderer ($gpuMode) unavailable — falling back to CPU raster (Software)")
|
println("GPU renderer ($gpuMode) unavailable - falling back to CPU raster (Software)")
|
||||||
gpuMode = GpuMode.Software
|
gpuMode = GpuMode.CpuRaster
|
||||||
vRender = makeBackend(gpuMode)
|
vRender = makeBackend(gpuMode)
|
||||||
}
|
}
|
||||||
if (vRender == null) {
|
if (vRender == null) {
|
||||||
@@ -611,10 +176,13 @@ 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
|
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame
|
||||||
// even when nothing else (recompose / relayout) is pending — retained layers
|
// even when nothing else (recompose / relayout) is pending - retained layers
|
||||||
// need this so a draw-only state change still repaints.
|
// need this so a draw-only state change still repaints.
|
||||||
host.setInvalidationCallback { needsFrame = true }
|
host.setInvalidationCallback { needsFrame = true }
|
||||||
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
||||||
|
// Mirror the creation flags into the facade so isResizable & co. read true
|
||||||
|
// from the start and a later Window() update only fires on a real change.
|
||||||
|
applyAttributes(attrs)
|
||||||
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
||||||
|
|
||||||
// The render-bridge globals must point at THIS window's renderer while
|
// The render-bridge globals must point at THIS window's renderer while
|
||||||
@@ -622,7 +190,7 @@ internal class WindowInstance(
|
|||||||
// setContent below).
|
// setContent below).
|
||||||
installGlobals()
|
installGlobals()
|
||||||
|
|
||||||
// Effect context for this window's composition — the screenshot flag injects the
|
// Effect context for this window's composition - the screenshot flag injects the
|
||||||
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
|
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
|
||||||
var vEffectContext = inScope.coroutineContext + frameClock
|
var vEffectContext = inScope.coroutineContext + frameClock
|
||||||
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
|
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
|
||||||
@@ -646,7 +214,7 @@ internal class WindowInstance(
|
|||||||
LocalComposeNativeWindow provides facade,
|
LocalComposeNativeWindow provides facade,
|
||||||
LocalPopupHost provides vPopupHost,
|
LocalPopupHost provides vPopupHost,
|
||||||
// Upstream vendored CompositionLocals.kt declares each of these as
|
// Upstream vendored CompositionLocals.kt declares each of these as
|
||||||
// `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` — reading
|
// `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` - reading
|
||||||
// one without a Provider throws. Seed them all from the ComposeOwner.
|
// one without a Provider throws. Seed them all from the ComposeOwner.
|
||||||
androidx.compose.ui.platform.LocalDensity provides host.density,
|
androidx.compose.ui.platform.LocalDensity provides host.density,
|
||||||
androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection,
|
androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection,
|
||||||
@@ -663,7 +231,7 @@ internal class WindowInstance(
|
|||||||
androidx.compose.ui.platform.LocalFontFamilyResolver provides
|
androidx.compose.ui.platform.LocalFontFamilyResolver provides
|
||||||
com.compose.sdl.text.font.projectFontFamilyResolver,
|
com.compose.sdl.text.font.projectFontFamilyResolver,
|
||||||
androidx.compose.ui.platform.LocalUriHandler provides vUriHandler,
|
androidx.compose.ui.platform.LocalUriHandler provides vUriHandler,
|
||||||
// Desktop windows have no system bars / notch / IME insets — the
|
// Desktop windows have no system bars / notch / IME insets - the
|
||||||
// interface's all-zero defaults are exactly right.
|
// interface's all-zero defaults are exactly right.
|
||||||
androidx.compose.ui.platform.LocalPlatformWindowInsets provides
|
androidx.compose.ui.platform.LocalPlatformWindowInsets provides
|
||||||
object : androidx.compose.ui.platform.PlatformWindowInsets {},
|
object : androidx.compose.ui.platform.PlatformWindowInsets {},
|
||||||
@@ -681,7 +249,7 @@ internal class WindowInstance(
|
|||||||
androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner,
|
androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner,
|
||||||
androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner,
|
androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner,
|
||||||
androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner,
|
androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner,
|
||||||
// Runtime-level host defaults — this window's navigation-event
|
// Runtime-level host defaults - this window's navigation-event
|
||||||
// owner (SearchBar / sheets back plumbing). The viewmodel store
|
// owner (SearchBar / sheets back plumbing). The viewmodel store
|
||||||
// is provided through the plain LocalViewModelStoreOwner above
|
// is provided through the plain LocalViewModelStoreOwner above
|
||||||
// instead: the google lifecycle artifacts this port ships have no
|
// instead: the google lifecycle artifacts this port ships have no
|
||||||
@@ -710,7 +278,7 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// First composition done (setContent is synchronous) — promote the
|
// First composition done (setContent is synchronous) - promote the
|
||||||
// window lifecycle from CREATED to the focus/visibility-derived state.
|
// window lifecycle from CREATED to the focus/visibility-derived state.
|
||||||
// Composition itself runs at CREATED so enableSavedStateHandles()
|
// Composition itself runs at CREATED so enableSavedStateHandles()
|
||||||
// callers see a legal state, mirroring upstream desktop's
|
// callers see a legal state, mirroring upstream desktop's
|
||||||
@@ -748,24 +316,25 @@ internal class WindowInstance(
|
|||||||
// Event handling (loop-routed, per window)
|
// Event handling (loop-routed, per window)
|
||||||
|
|
||||||
fun onPointerEvent(inEvent: AppEvent.Pointer) {
|
fun onPointerEvent(inEvent: AppEvent.Pointer) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
val vType = when (inEvent.event.type) {
|
val vType = when (inEvent.type) {
|
||||||
PointerEventType.Press -> 1
|
PointerEventType.Press -> 1
|
||||||
PointerEventType.Release -> 2
|
PointerEventType.Release -> 2
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
val vBtn = when (inEvent.event.button) {
|
val vBtn = when (inEvent.button) {
|
||||||
PointerButton.Secondary -> 1
|
PointerButton.Secondary -> 1
|
||||||
PointerButton.Tertiary -> 2
|
PointerButton.Tertiary -> 2
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
// SDL3 delivers mouse coords in logical points on HiDPI — multiply by
|
// SDL3 delivers mouse coords in logical points on HiDPI - multiply by
|
||||||
// DPR so hit-testing lands in the pixel space layout uses.
|
// DPR so hit-testing lands in the pixel space layout uses.
|
||||||
val vDpr = backend.pixelDensity
|
val vDpr = backend.pixelDensity
|
||||||
val vPx = inEvent.event.x * vDpr
|
val vPx = inEvent.x * vDpr
|
||||||
val vPy = inEvent.event.y * vDpr
|
val vPy = inEvent.y * vDpr
|
||||||
if (inEvent.event.type == PointerEventType.Press) {
|
if (inEvent.type == PointerEventType.Press) {
|
||||||
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
||||||
}
|
}
|
||||||
host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong())
|
host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong())
|
||||||
@@ -778,6 +347,7 @@ internal class WindowInstance(
|
|||||||
synthetic hover re-dispatch and fire one Exit at the last position so a
|
synthetic hover re-dispatch and fire one Exit at the last position so a
|
||||||
hovered widget clears its highlight instead of sticking forever. */
|
hovered widget clears its highlight instead of sticking forever. */
|
||||||
fun onPointerExit() {
|
fun onPointerExit() {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
if (hasMousePos) {
|
if (hasMousePos) {
|
||||||
host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong())
|
host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong())
|
||||||
@@ -786,6 +356,7 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onWheelEvent(inEvent: AppEvent.MouseWheel) {
|
fun onWheelEvent(inEvent: AppEvent.MouseWheel) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
val vDpr = backend.pixelDensity
|
val vDpr = backend.pixelDensity
|
||||||
@@ -793,20 +364,36 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onKeyEvent(inEvent: AppEvent.Key) {
|
fun onKeyEvent(inEvent: AppEvent.Key) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
// Focused chain → window-level shortcuts → back navigation (Escape).
|
// Compose Desktop's order: window preview → focused content → window
|
||||||
if (!host.dispatchKeyEvent(inEvent.event) && !facade.dispatchKeyShortcut(inEvent.event)) {
|
// onKeyEvent → app shortcut handler → back navigation (Escape).
|
||||||
|
if (previewKeyHandler().invoke(inEvent.event)) return
|
||||||
|
if (host.dispatchKeyEvent(inEvent.event)) return
|
||||||
|
if (keyHandler().invoke(inEvent.event)) return
|
||||||
|
if (!facade.dispatchKeyShortcut(inEvent.event)) {
|
||||||
backNavigationInput.onKeyEvent(inEvent.event)
|
backNavigationInput.onKeyEvent(inEvent.event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Applies the mutable window attributes. Called at init from the creation
|
||||||
|
flags and again whenever Window() sees one of them change. */
|
||||||
|
fun applyAttributes(inAttrs: WindowAttributes) {
|
||||||
|
facade.setUndecorated(inAttrs.undecorated)
|
||||||
|
facade.setResizable(inAttrs.resizable)
|
||||||
|
facade.setAlwaysOnTop(inAttrs.alwaysOnTop)
|
||||||
|
facade.setFocusable(inAttrs.focusable)
|
||||||
|
facade.setEnabled(inAttrs.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun onTextInputEvent(inEvent: AppEvent.TextInput) {
|
fun onTextInputEvent(inEvent: AppEvent.TextInput) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
// A focused text field runs an IME session (ComposeOwner.textInputSession):
|
// A focused text field runs an IME session (ComposeOwner.textInputSession):
|
||||||
// commit through it so the text REPLACES any active composition. Falls back
|
// commit through it so the text REPLACES any active composition. Falls back
|
||||||
// to synthesising typed KeyEvents when no field is focused — SDL key events
|
// to synthesising typed KeyEvents when no field is focused - SDL key events
|
||||||
// carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed
|
// carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed
|
||||||
// text is the only layout-correct character source.
|
// text is the only layout-correct character source.
|
||||||
if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) {
|
if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) {
|
||||||
@@ -815,6 +402,7 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTextEditingEvent(inEvent: AppEvent.TextEditing) {
|
fun onTextEditingEvent(inEvent: AppEvent.TextEditing) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
// IME preedit: show the composing (underlined) region in the focused field.
|
// IME preedit: show the composing (underlined) region in the focused field.
|
||||||
@@ -844,6 +432,7 @@ internal class WindowInstance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDropEvent(inEvent: AppEvent.Drop) {
|
fun onDropEvent(inEvent: AppEvent.Drop) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
needsFrame = true
|
needsFrame = true
|
||||||
installGlobals()
|
installGlobals()
|
||||||
// SDL fires drop coords in logical points at DPR-1; scale to the pixel
|
// SDL fires drop coords in logical points at DPR-1; scale to the pixel
|
||||||
@@ -865,7 +454,7 @@ internal class WindowInstance(
|
|||||||
facade.onResized()
|
facade.onResized()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** OS light/dark theme changed — re-pick the theme-appropriate window icon
|
/** OS light/dark theme changed - re-pick the theme-appropriate window icon
|
||||||
(no-op if this window has no icon configured or the choice is unchanged). */
|
(no-op if this window has no icon configured or the choice is unchanged). */
|
||||||
fun onSystemThemeChanged() {
|
fun onSystemThemeChanged() {
|
||||||
backend.applyThemeIcon()
|
backend.applyThemeIcon()
|
||||||
@@ -889,7 +478,7 @@ internal class WindowInstance(
|
|||||||
|
|
||||||
/** True while this window still has pending work: a state/layout/draw invalidation
|
/** True while this window still has pending work: a state/layout/draw invalidation
|
||||||
(needsFrame), recomposer work, or a composition/node animation awaiting the next
|
(needsFrame), recomposer work, or a composition/node animation awaiting the next
|
||||||
frame. Sampled by windowHasInvalidations() from onFrame probes — the quiescence
|
frame. Sampled by windowHasInvalidations() from onFrame probes - the quiescence
|
||||||
signal for render-to-settle screenshot capture. */
|
signal for render-to-settle screenshot capture. */
|
||||||
fun hasInvalidations(): Boolean =
|
fun hasInvalidations(): Boolean =
|
||||||
needsFrame || recomposer?.hasPendingWork == true ||
|
needsFrame || recomposer?.hasPendingWork == true ||
|
||||||
@@ -905,13 +494,13 @@ internal class WindowInstance(
|
|||||||
// Charge the GPU back-buffer acquire to its own phase. On Metal this is
|
// Charge the GPU back-buffer acquire to its own phase. On Metal this is
|
||||||
// where nextDrawable() blocks on vsync (the natural frame pacing), so
|
// where nextDrawable() blocks on vsync (the natural frame pacing), so
|
||||||
// folding it into "layout" made the profiler read the vsync wait as
|
// folding it into "layout" made the profiler read the vsync wait as
|
||||||
// layout cost — a ~6-7ms phantom that hid the real (tiny) layout time.
|
// layout cost - a ~6-7ms phantom that hid the real (tiny) layout time.
|
||||||
FrameProfiler.phase(" acquire")
|
FrameProfiler.phase(" acquire")
|
||||||
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
||||||
// Deliver any state written by this iteration's frame-clock continuations
|
// Deliver any state written by this iteration's frame-clock continuations
|
||||||
// (withFrameNanos animations — notably smooth wheel scrolling) BEFORE we lay
|
// (withFrameNanos animations - notably smooth wheel scrolling) BEFORE we lay
|
||||||
// out. Those writes happen in the per-window pump's sendFrame()/yield(), which
|
// out. Those writes happen in the per-window pump's sendFrame()/yield(), which
|
||||||
// is AFTER the loop's last sendApplyNotifications() — so without this the
|
// is AFTER the loop's last sendApplyNotifications() - so without this the
|
||||||
// relayout they trigger wasn't registered until the next frame, and the frame
|
// relayout they trigger wasn't registered until the next frame, and the frame
|
||||||
// drew last frame's positions. That one-frame trail read as "the clip is a
|
// drew last frame's positions. That one-frame trail read as "the clip is a
|
||||||
// frame late" when scrolling (content briefly drawn past the viewport edge).
|
// frame late" when scrolling (content briefly drawn past the viewport edge).
|
||||||
@@ -938,14 +527,14 @@ internal class WindowInstance(
|
|||||||
FrameProfiler.phase(" present")
|
FrameProfiler.phase(" present")
|
||||||
frameIndex++
|
frameIndex++
|
||||||
|
|
||||||
// FPS — instantaneous inter-frame rate, EMA-smoothed, title refreshed
|
// FPS - instantaneous inter-frame rate, EMA-smoothed, title refreshed
|
||||||
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
|
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
|
||||||
// than waiting for a full second of unbroken rendering to accumulate
|
// than waiting for a full second of unbroken rendering to accumulate
|
||||||
// (the old fixed-window counter never got its first update: bursty
|
// (the old fixed-window counter never got its first update: bursty
|
||||||
// interaction kept idling before 1s elapsed, and the idle-skip reset the
|
// interaction kept idling before 1s elapsed, and the idle-skip reset the
|
||||||
// window each time — so FPS only appeared during a long enough page
|
// 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
|
// 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
|
// idle (the gap isn't a real frame interval), so it's not sampled - the
|
||||||
// title just holds the last active rate while idle.
|
// title just holds the last active rate while idle.
|
||||||
val vNowMs = SDL_GetTicks()
|
val vNowMs = SDL_GetTicks()
|
||||||
val vDt = (vNowMs - fpsLastFrameMs).toInt()
|
val vDt = (vNowMs - fpsLastFrameMs).toInt()
|
||||||
@@ -984,6 +573,3 @@ internal class WindowInstance(
|
|||||||
backend.destroy(inQuitSdl = false)
|
backend.destroy(inQuitSdl = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WindowArchitectureOwner -> WindowArchitectureOwner.kt
|
|
||||||
// BackNavigationInput / dispatchTypedText -> WindowInputHelpers.kt
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// :foundation-layout — androidx.compose.foundation.layout.*, vendored VERBATIM
|
// :foundation-layout - androidx.compose.foundation.layout.*, vendored VERBATIM
|
||||||
// from upstream into src/vendor/. Split out of :foundation to mirror Compose
|
// from upstream into src/vendor/. Split out of :foundation to mirror Compose
|
||||||
// Multiplatform's module packaging.
|
// Multiplatform's module packaging.
|
||||||
//
|
//
|
||||||
// Provenance = foundation-layout/compose-fork.txt + scripts/compose-fork/compose.properties.
|
// Provenance = foundation-layout/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under src/vendor/ — change the manifest and re-run
|
// Never hand-edit files under src/vendor/ - change the manifest and re-run
|
||||||
// `python scripts/compose-fork/sync.py compose/foundation/foundation-layout`.
|
// `python scripts/compose-fork/sync.py compose/foundation/foundation-layout`.
|
||||||
//
|
//
|
||||||
// Publication artifactId: desktop-foundation-layout.
|
// Publication artifactId: desktop-foundation-layout.
|
||||||
@@ -27,7 +27,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
// foundation.layout.* only needs ui (+ runtime/collection transitively).
|
// foundation.layout.* only needs ui (+ runtime/collection transitively).
|
||||||
api(project(":ui"))
|
api(project(":compose:ui:ui"))
|
||||||
}
|
}
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -778,6 +778,17 @@ abstract interface androidx.compose.foundation.text/TextAutoSize { // androidx.c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract interface androidx.compose.foundation.v2/ScrollbarAdapter { // androidx.compose.foundation.v2/ScrollbarAdapter|null[0]
|
||||||
|
abstract val contentSize // androidx.compose.foundation.v2/ScrollbarAdapter.contentSize|{}contentSize[0]
|
||||||
|
abstract fun <get-contentSize>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.contentSize.<get-contentSize>|<get-contentSize>(){}[0]
|
||||||
|
abstract val scrollOffset // androidx.compose.foundation.v2/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
|
||||||
|
abstract fun <get-scrollOffset>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
|
||||||
|
abstract val viewportSize // androidx.compose.foundation.v2/ScrollbarAdapter.viewportSize|{}viewportSize[0]
|
||||||
|
abstract fun <get-viewportSize>(): kotlin/Double // androidx.compose.foundation.v2/ScrollbarAdapter.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
|
||||||
|
|
||||||
|
abstract suspend fun scrollTo(kotlin/Double) // androidx.compose.foundation.v2/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Double){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
abstract interface androidx.compose.foundation/BasicTooltipState { // androidx.compose.foundation/BasicTooltipState|null[0]
|
abstract interface androidx.compose.foundation/BasicTooltipState { // androidx.compose.foundation/BasicTooltipState|null[0]
|
||||||
abstract val isPersistent // androidx.compose.foundation/BasicTooltipState.isPersistent|{}isPersistent[0]
|
abstract val isPersistent // androidx.compose.foundation/BasicTooltipState.isPersistent|{}isPersistent[0]
|
||||||
abstract fun <get-isPersistent>(): kotlin/Boolean // androidx.compose.foundation/BasicTooltipState.isPersistent.<get-isPersistent>|<get-isPersistent>(){}[0]
|
abstract fun <get-isPersistent>(): kotlin/Boolean // androidx.compose.foundation/BasicTooltipState.isPersistent.<get-isPersistent>|<get-isPersistent>(){}[0]
|
||||||
@@ -830,15 +841,12 @@ abstract interface androidx.compose.foundation/ScrollIndicatorState { // android
|
|||||||
abstract fun <get-viewportSize>(): kotlin/Int // androidx.compose.foundation/ScrollIndicatorState.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
|
abstract fun <get-viewportSize>(): kotlin/Int // androidx.compose.foundation/ScrollIndicatorState.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface com.compose.sdl.scrollbar/ScrollbarAdapter { // com.compose.sdl.scrollbar/ScrollbarAdapter|null[0]
|
abstract interface androidx.compose.foundation/ScrollbarAdapter { // androidx.compose.foundation/ScrollbarAdapter|null[0]
|
||||||
abstract val contentSize // com.compose.sdl.scrollbar/ScrollbarAdapter.contentSize|{}contentSize[0]
|
abstract val scrollOffset // androidx.compose.foundation/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
|
||||||
abstract fun <get-contentSize>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.contentSize.<get-contentSize>|<get-contentSize>(){}[0]
|
abstract fun <get-scrollOffset>(): kotlin/Float // androidx.compose.foundation/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
|
||||||
abstract val scrollOffset // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollOffset|{}scrollOffset[0]
|
|
||||||
abstract fun <get-scrollOffset>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollOffset.<get-scrollOffset>|<get-scrollOffset>(){}[0]
|
|
||||||
abstract val viewportSize // com.compose.sdl.scrollbar/ScrollbarAdapter.viewportSize|{}viewportSize[0]
|
|
||||||
abstract fun <get-viewportSize>(): kotlin/Double // com.compose.sdl.scrollbar/ScrollbarAdapter.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
|
|
||||||
|
|
||||||
abstract suspend fun scrollTo(kotlin/Double) // com.compose.sdl.scrollbar/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Double){}[0]
|
abstract fun maxScrollOffset(kotlin/Int): kotlin/Float // androidx.compose.foundation/ScrollbarAdapter.maxScrollOffset|maxScrollOffset(kotlin.Int){}[0]
|
||||||
|
abstract suspend fun scrollTo(kotlin/Int, kotlin/Float) // androidx.compose.foundation/ScrollbarAdapter.scrollTo|scrollTo(kotlin.Int;kotlin.Float){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface <#A: out kotlin/Any?> androidx.compose.foundation.lazy.layout/IntervalList { // androidx.compose.foundation.lazy.layout/IntervalList|null[0]
|
sealed interface <#A: out kotlin/Any?> androidx.compose.foundation.lazy.layout/IntervalList { // androidx.compose.foundation.lazy.layout/IntervalList|null[0]
|
||||||
@@ -1829,21 +1837,32 @@ final class androidx.compose.foundation/ScrollState : androidx.compose.foundatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl.scrollbar/ScrollbarStyle { // com.compose.sdl.scrollbar/ScrollbarStyle|null[0]
|
final class androidx.compose.foundation/ScrollbarStyle { // androidx.compose.foundation/ScrollbarStyle|null[0]
|
||||||
constructor <init>(androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Shape, kotlin/Int, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // com.compose.sdl.scrollbar/ScrollbarStyle.<init>|<init>(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
|
constructor <init>(androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Shape, kotlin/Int, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // androidx.compose.foundation/ScrollbarStyle.<init>|<init>(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
|
||||||
|
|
||||||
final val hoverColor // com.compose.sdl.scrollbar/ScrollbarStyle.hoverColor|{}hoverColor[0]
|
final val hoverColor // androidx.compose.foundation/ScrollbarStyle.hoverColor|{}hoverColor[0]
|
||||||
final fun <get-hoverColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.scrollbar/ScrollbarStyle.hoverColor.<get-hoverColor>|<get-hoverColor>(){}[0]
|
final fun <get-hoverColor>(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.hoverColor.<get-hoverColor>|<get-hoverColor>(){}[0]
|
||||||
final val hoverDurationMillis // com.compose.sdl.scrollbar/ScrollbarStyle.hoverDurationMillis|{}hoverDurationMillis[0]
|
final val hoverDurationMillis // androidx.compose.foundation/ScrollbarStyle.hoverDurationMillis|{}hoverDurationMillis[0]
|
||||||
final fun <get-hoverDurationMillis>(): kotlin/Int // com.compose.sdl.scrollbar/ScrollbarStyle.hoverDurationMillis.<get-hoverDurationMillis>|<get-hoverDurationMillis>(){}[0]
|
final fun <get-hoverDurationMillis>(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.hoverDurationMillis.<get-hoverDurationMillis>|<get-hoverDurationMillis>(){}[0]
|
||||||
final val minimalHeight // com.compose.sdl.scrollbar/ScrollbarStyle.minimalHeight|{}minimalHeight[0]
|
final val minimalHeight // androidx.compose.foundation/ScrollbarStyle.minimalHeight|{}minimalHeight[0]
|
||||||
final fun <get-minimalHeight>(): androidx.compose.ui.unit/Dp // com.compose.sdl.scrollbar/ScrollbarStyle.minimalHeight.<get-minimalHeight>|<get-minimalHeight>(){}[0]
|
final fun <get-minimalHeight>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.minimalHeight.<get-minimalHeight>|<get-minimalHeight>(){}[0]
|
||||||
final val shape // com.compose.sdl.scrollbar/ScrollbarStyle.shape|{}shape[0]
|
final val shape // androidx.compose.foundation/ScrollbarStyle.shape|{}shape[0]
|
||||||
final fun <get-shape>(): androidx.compose.ui.graphics/Shape // com.compose.sdl.scrollbar/ScrollbarStyle.shape.<get-shape>|<get-shape>(){}[0]
|
final fun <get-shape>(): androidx.compose.ui.graphics/Shape // androidx.compose.foundation/ScrollbarStyle.shape.<get-shape>|<get-shape>(){}[0]
|
||||||
final val thickness // com.compose.sdl.scrollbar/ScrollbarStyle.thickness|{}thickness[0]
|
final val thickness // androidx.compose.foundation/ScrollbarStyle.thickness|{}thickness[0]
|
||||||
final fun <get-thickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.scrollbar/ScrollbarStyle.thickness.<get-thickness>|<get-thickness>(){}[0]
|
final fun <get-thickness>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.thickness.<get-thickness>|<get-thickness>(){}[0]
|
||||||
final val unhoverColor // com.compose.sdl.scrollbar/ScrollbarStyle.unhoverColor|{}unhoverColor[0]
|
final val unhoverColor // androidx.compose.foundation/ScrollbarStyle.unhoverColor|{}unhoverColor[0]
|
||||||
final fun <get-unhoverColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.scrollbar/ScrollbarStyle.unhoverColor.<get-unhoverColor>|<get-unhoverColor>(){}[0]
|
final fun <get-unhoverColor>(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.unhoverColor.<get-unhoverColor>|<get-unhoverColor>(){}[0]
|
||||||
|
|
||||||
|
final fun component1(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.component1|component1(){}[0]
|
||||||
|
final fun component2(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/ScrollbarStyle.component2|component2(){}[0]
|
||||||
|
final fun component3(): androidx.compose.ui.graphics/Shape // androidx.compose.foundation/ScrollbarStyle.component3|component3(){}[0]
|
||||||
|
final fun component4(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.component4|component4(){}[0]
|
||||||
|
final fun component5(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.component5|component5(){}[0]
|
||||||
|
final fun component6(): androidx.compose.ui.graphics/Color // androidx.compose.foundation/ScrollbarStyle.component6|component6(){}[0]
|
||||||
|
final fun copy(androidx.compose.ui.unit/Dp = ..., androidx.compose.ui.unit/Dp = ..., androidx.compose.ui.graphics/Shape = ..., kotlin/Int = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): androidx.compose.foundation/ScrollbarStyle // androidx.compose.foundation/ScrollbarStyle.copy|copy(androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Shape;kotlin.Int;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
|
||||||
|
final fun equals(kotlin/Any?): kotlin/Boolean // androidx.compose.foundation/ScrollbarStyle.equals|equals(kotlin.Any?){}[0]
|
||||||
|
final fun hashCode(): kotlin/Int // androidx.compose.foundation/ScrollbarStyle.hashCode|hashCode(){}[0]
|
||||||
|
final fun toString(): kotlin/String // androidx.compose.foundation/ScrollbarStyle.toString|toString(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final value class androidx.compose.foundation.lazy.grid/GridItemSpan { // androidx.compose.foundation.lazy.grid/GridItemSpan|null[0]
|
final value class androidx.compose.foundation.lazy.grid/GridItemSpan { // androidx.compose.foundation.lazy.grid/GridItemSpan|null[0]
|
||||||
@@ -2071,13 +2090,6 @@ final object androidx.compose.foundation/MarqueeDefaults { // androidx.compose.f
|
|||||||
final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0]
|
final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object com.compose.sdl.icons/IconDefaults { // com.compose.sdl.icons/IconDefaults|null[0]
|
|
||||||
final val DefaultIconSize // com.compose.sdl.icons/IconDefaults.DefaultIconSize|{}DefaultIconSize[0]
|
|
||||||
final fun <get-DefaultIconSize>(): androidx.compose.ui.unit/Dp // com.compose.sdl.icons/IconDefaults.DefaultIconSize.<get-DefaultIconSize>|<get-DefaultIconSize>(){}[0]
|
|
||||||
final val LocalContentColor // com.compose.sdl.icons/IconDefaults.LocalContentColor|{}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.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]
|
||||||
@@ -2197,10 +2209,15 @@ final val androidx.compose.foundation.text/androidx_compose_foundation_text_Keyb
|
|||||||
final val androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop|#static{}androidx_compose_foundation_text_KeyboardOptions$stableprop[0]
|
final val androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_KeyboardOptions$stableprop|#static{}androidx_compose_foundation_text_KeyboardOptions$stableprop[0]
|
||||||
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop|#static{}androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop[0]
|
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop|#static{}androidx_compose_foundation_text_TextAutoSizeDefaults$stableprop[0]
|
||||||
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop|#static{}androidx_compose_foundation_text_TextFieldScrollState$stableprop[0]
|
final val androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop // androidx.compose.foundation.text/androidx_compose_foundation_text_TextFieldScrollState$stableprop|#static{}androidx_compose_foundation_text_TextFieldScrollState$stableprop[0]
|
||||||
|
final val androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop // androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop|#static{}androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop[0]
|
||||||
|
final val androidx.compose.foundation.v2/maxScrollOffset // androidx.compose.foundation.v2/maxScrollOffset|@androidx.compose.foundation.v2.ScrollbarAdapter{}maxScrollOffset[0]
|
||||||
|
final fun (androidx.compose.foundation.v2/ScrollbarAdapter).<get-maxScrollOffset>(): kotlin/Double // androidx.compose.foundation.v2/maxScrollOffset.<get-maxScrollOffset>|<get-maxScrollOffset>@androidx.compose.foundation.v2.ScrollbarAdapter(){}[0]
|
||||||
final val androidx.compose.foundation/LocalIndication // androidx.compose.foundation/LocalIndication|{}LocalIndication[0]
|
final val androidx.compose.foundation/LocalIndication // androidx.compose.foundation/LocalIndication|{}LocalIndication[0]
|
||||||
final fun <get-LocalIndication>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/Indication> // androidx.compose.foundation/LocalIndication.<get-LocalIndication>|<get-LocalIndication>(){}[0]
|
final fun <get-LocalIndication>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/Indication> // androidx.compose.foundation/LocalIndication.<get-LocalIndication>|<get-LocalIndication>(){}[0]
|
||||||
final val androidx.compose.foundation/LocalOverscrollFactory // androidx.compose.foundation/LocalOverscrollFactory|{}LocalOverscrollFactory[0]
|
final val androidx.compose.foundation/LocalOverscrollFactory // androidx.compose.foundation/LocalOverscrollFactory|{}LocalOverscrollFactory[0]
|
||||||
final fun <get-LocalOverscrollFactory>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/OverscrollFactory?> // androidx.compose.foundation/LocalOverscrollFactory.<get-LocalOverscrollFactory>|<get-LocalOverscrollFactory>(){}[0]
|
final fun <get-LocalOverscrollFactory>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/OverscrollFactory?> // androidx.compose.foundation/LocalOverscrollFactory.<get-LocalOverscrollFactory>|<get-LocalOverscrollFactory>(){}[0]
|
||||||
|
final val androidx.compose.foundation/LocalScrollbarStyle // androidx.compose.foundation/LocalScrollbarStyle|{}LocalScrollbarStyle[0]
|
||||||
|
final fun <get-LocalScrollbarStyle>(): androidx.compose.runtime/ProvidableCompositionLocal<androidx.compose.foundation/ScrollbarStyle> // androidx.compose.foundation/LocalScrollbarStyle.<get-LocalScrollbarStyle>|<get-LocalScrollbarStyle>(){}[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop|#static{}androidx_compose_foundation_BasicTooltipDefaults$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop|#static{}androidx_compose_foundation_BasicTooltipDefaults$stableprop[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop|#static{}androidx_compose_foundation_BorderStroke$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop|#static{}androidx_compose_foundation_BorderStroke$stableprop[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop|#static{}androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop|#static{}androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop[0]
|
||||||
@@ -2208,10 +2225,7 @@ final val androidx.compose.foundation/androidx_compose_foundation_ComposeFoundat
|
|||||||
final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop|#static{}androidx_compose_foundation_MarqueeDefaults$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop|#static{}androidx_compose_foundation_MarqueeDefaults$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_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 androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop|#static{}androidx_compose_foundation_ScrollbarStyle$stableprop[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 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.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0]
|
final val com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0]
|
||||||
|
|
||||||
final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0]
|
final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0]
|
||||||
@@ -2585,18 +2599,29 @@ final fun androidx.compose.foundation.text/androidx_compose_foundation_text_Text
|
|||||||
final fun androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState|rememberTextFieldHorizontalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldHorizontalScrollState|rememberTextFieldHorizontalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation.text/rememberTextFieldScrollState(androidx.compose.foundation.gestures/Orientation, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldScrollState|rememberTextFieldScrollState(androidx.compose.foundation.gestures.Orientation;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation.text/rememberTextFieldScrollState(androidx.compose.foundation.gestures/Orientation, kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldScrollState|rememberTextFieldScrollState(androidx.compose.foundation.gestures.Orientation;kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation.text/rememberTextFieldVerticalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldVerticalScrollState|rememberTextFieldVerticalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation.text/rememberTextFieldVerticalScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation.text/TextFieldScrollState // androidx.compose.foundation.text/rememberTextFieldVerticalScrollState|rememberTextFieldVerticalScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter(): kotlin/Int // androidx.compose.foundation.v2/androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter|androidx_compose_foundation_v2_LazyLineContentAdapter_VisibleLine$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/BasicTooltipBox(androidx.compose.ui.window/PopupPositionProvider, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.foundation/BasicTooltipState, androidx.compose.ui/Modifier?, kotlin/Boolean, kotlin/Boolean, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/BasicTooltipBox|BasicTooltipBox(androidx.compose.ui.window.PopupPositionProvider;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.foundation.BasicTooltipState;androidx.compose.ui.Modifier?;kotlin.Boolean;kotlin.Boolean;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/BasicTooltipState(kotlin/Boolean = ..., kotlin/Boolean = ..., androidx.compose.foundation/MutatorMutex = ...): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/BasicTooltipState|BasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex){}[0]
|
final fun androidx.compose.foundation/BasicTooltipState(kotlin/Boolean = ..., kotlin/Boolean = ..., androidx.compose.foundation/MutatorMutex = ...): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/BasicTooltipState|BasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex){}[0]
|
||||||
final fun androidx.compose.foundation/BorderStroke(androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color): androidx.compose.foundation/BorderStroke // androidx.compose.foundation/BorderStroke|BorderStroke(androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color){}[0]
|
final fun androidx.compose.foundation/BorderStroke(androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color): androidx.compose.foundation/BorderStroke // androidx.compose.foundation/BorderStroke|BorderStroke(androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color){}[0]
|
||||||
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/String, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.String;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Canvas(androidx.compose.ui/Modifier, kotlin/String, kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.foundation/Canvas|Canvas(androidx.compose.ui.Modifier;kotlin.String;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/HorizontalScrollbar(androidx.compose.foundation.v2/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/HorizontalScrollbar|HorizontalScrollbar(androidx.compose.foundation.v2.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/HorizontalScrollbar(androidx.compose.foundation/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/HorizontalScrollbar|HorizontalScrollbar(androidx.compose.foundation.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.painter/Painter, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.painter.Painter;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.painter/Painter, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.painter.Painter;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.vector/ImageVector, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.vector.ImageVector;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics.vector/ImageVector, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.vector.ImageVector;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/Image(androidx.compose.ui.graphics/ImageBitmap, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui/Alignment?, androidx.compose.ui.layout/ContentScale?, kotlin/Float, androidx.compose.ui.graphics/ColorFilter?, androidx.compose.ui.graphics/FilterQuality?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/Image|Image(androidx.compose.ui.graphics.ImageBitmap;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.Alignment?;androidx.compose.ui.layout.ContentScale?;kotlin.Float;androidx.compose.ui.graphics.ColorFilter?;androidx.compose.ui.graphics.FilterQuality?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/MarqueeSpacing(androidx.compose.ui.unit/Dp): androidx.compose.foundation/MarqueeSpacing // androidx.compose.foundation/MarqueeSpacing|MarqueeSpacing(androidx.compose.ui.unit.Dp){}[0]
|
final fun androidx.compose.foundation/MarqueeSpacing(androidx.compose.ui.unit/Dp): androidx.compose.foundation/MarqueeSpacing // androidx.compose.foundation/MarqueeSpacing|MarqueeSpacing(androidx.compose.ui.unit.Dp){}[0]
|
||||||
|
final fun androidx.compose.foundation/OldScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/OldScrollbarAdapter|OldScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState){}[0]
|
||||||
|
final fun androidx.compose.foundation/OldScrollbarAdapter(androidx.compose.foundation/ScrollState): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/OldScrollbarAdapter|OldScrollbarAdapter(androidx.compose.foundation.ScrollState){}[0]
|
||||||
|
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.lazy.grid/LazyGridState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.lazy.grid.LazyGridState){}[0]
|
||||||
|
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState){}[0]
|
||||||
|
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState){}[0]
|
||||||
|
final fun androidx.compose.foundation/ScrollbarAdapter(androidx.compose.foundation/ScrollState): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/ScrollbarAdapter|ScrollbarAdapter(androidx.compose.foundation.ScrollState){}[0]
|
||||||
|
final fun androidx.compose.foundation/VerticalScrollbar(androidx.compose.foundation.v2/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/VerticalScrollbar|VerticalScrollbar(androidx.compose.foundation.v2.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/VerticalScrollbar(androidx.compose.foundation/ScrollbarAdapter, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.foundation/ScrollbarStyle?, androidx.compose.foundation.interaction/MutableInteractionSource?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // androidx.compose.foundation/VerticalScrollbar|VerticalScrollbar(androidx.compose.foundation.ScrollbarAdapter;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.foundation.ScrollbarStyle?;androidx.compose.foundation.interaction.MutableInteractionSource?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter|androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter|androidx_compose_foundation_BasicTooltipDefaults$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter|androidx_compose_foundation_BorderStroke$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_BorderStroke$stableprop_getter|androidx_compose_foundation_BorderStroke$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter|androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter|androidx_compose_foundation_CombinedClickableNode_DoubleKeyClickState$stableprop_getter(){}[0]
|
||||||
@@ -2604,20 +2629,21 @@ final fun androidx.compose.foundation/androidx_compose_foundation_ComposeFoundat
|
|||||||
final fun androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter|androidx_compose_foundation_MarqueeDefaults$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MarqueeDefaults$stableprop_getter|androidx_compose_foundation_MarqueeDefaults$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter|androidx_compose_foundation_MutatorMutex$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop_getter|androidx_compose_foundation_MutatorMutex$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter|androidx_compose_foundation_ScrollState$stableprop_getter(){}[0]
|
final fun androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop_getter|androidx_compose_foundation_ScrollState$stableprop_getter(){}[0]
|
||||||
|
final fun androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop_getter(): kotlin/Int // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop_getter|androidx_compose_foundation_ScrollbarStyle$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.foundation/checkScrollableContainerConstraints(androidx.compose.ui.unit/Constraints, androidx.compose.foundation.gestures/Orientation) // androidx.compose.foundation/checkScrollableContainerConstraints|checkScrollableContainerConstraints(androidx.compose.ui.unit.Constraints;androidx.compose.foundation.gestures.Orientation){}[0]
|
final fun androidx.compose.foundation/checkScrollableContainerConstraints(androidx.compose.ui.unit/Constraints, androidx.compose.foundation.gestures/Orientation) // androidx.compose.foundation/checkScrollableContainerConstraints|checkScrollableContainerConstraints(androidx.compose.ui.unit.Constraints;androidx.compose.foundation.gestures.Orientation){}[0]
|
||||||
|
final fun androidx.compose.foundation/defaultScrollbarStyle(): androidx.compose.foundation/ScrollbarStyle // androidx.compose.foundation/defaultScrollbarStyle|defaultScrollbarStyle(){}[0]
|
||||||
final fun androidx.compose.foundation/isSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.foundation/isSystemInDarkTheme|isSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/isSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.foundation/isSystemInDarkTheme|isSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean, kotlin/Boolean, androidx.compose.foundation/MutatorMutex?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/rememberBasicTooltipState|rememberBasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/rememberBasicTooltipState(kotlin/Boolean, kotlin/Boolean, androidx.compose.foundation/MutatorMutex?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): androidx.compose.foundation/BasicTooltipState // androidx.compose.foundation/rememberBasicTooltipState|rememberBasicTooltipState(kotlin.Boolean;kotlin.Boolean;androidx.compose.foundation.MutatorMutex?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/rememberOldScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/rememberOldScrollbarAdapter|rememberOldScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/rememberOldScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation/ScrollbarAdapter // androidx.compose.foundation/rememberOldScrollbarAdapter|rememberOldScrollbarAdapter(androidx.compose.foundation.ScrollState;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/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 androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.lazy.grid/LazyGridState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.grid.LazyGridState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.lazy/LazyListState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.lazy.LazyListState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
|
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;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/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.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/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/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.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]
|
||||||
final fun com.compose.sdl.widgets/HorizontalSplitPane(androidx.compose.ui/Modifier?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.widgets/HorizontalSplitPane|HorizontalSplitPane(androidx.compose.ui.Modifier?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.widgets/HorizontalSplitPane(androidx.compose.ui/Modifier?, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.unit/Dp, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.widgets/HorizontalSplitPane|HorizontalSplitPane(androidx.compose.ui.Modifier?;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.unit.Dp;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||||
|
|
||||||
// :foundation — androidx.compose.foundation.* AND androidx.compose.animation.*
|
// :foundation - androidx.compose.foundation.* AND androidx.compose.animation.*
|
||||||
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
|
// (non-core) vendored VERBATIM from upstream. Sits on :ui (which still owns
|
||||||
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
|
// androidx.compose.ui.*, the Skia renderer, cinterops) + :animation-core.
|
||||||
//
|
//
|
||||||
@@ -14,12 +14,12 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
|||||||
// module boundary is coalesced.
|
// module boundary is coalesced.
|
||||||
//
|
//
|
||||||
// Provenance = foundation/compose-fork.txt + scripts/compose-fork/compose.properties.
|
// Provenance = foundation/compose-fork.txt + scripts/compose-fork/compose.properties.
|
||||||
// Never hand-edit files under foundation/src/vendor/ — change the manifest and
|
// Never hand-edit files under foundation/src/vendor/ - change the manifest and
|
||||||
// re-run `bash scripts/compose-fork/sync.sh :foundation`.
|
// re-run `bash scripts/compose-fork/sync.sh :foundation`.
|
||||||
//
|
//
|
||||||
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
|
// Note on DarkTheme: foundation declares `internal expect fun _isSystemInDarkTheme()`,
|
||||||
// and its actual lives here as a plain nativeMain kt file (see
|
// and its actual lives here as a plain nativeMain kt file (see
|
||||||
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) — a single
|
// src/nativeMain/kotlin/androidx/compose/foundation/DarkTheme.native.kt) - a single
|
||||||
// stub returning false. The upstream skiko per-renderer actuals were dropped
|
// stub returning false. The upstream skiko per-renderer actuals were dropped
|
||||||
// when :foundation was split from :ui: expect/actual must live in the same
|
// when :foundation was split from :ui: expect/actual must live in the same
|
||||||
// module, and duplicating the skikoRenderer source-set hierarchy on
|
// module, and duplicating the skikoRenderer source-set hierarchy on
|
||||||
@@ -48,10 +48,10 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
kotlin.srcDir("src/vendor/common/kotlin")
|
kotlin.srcDir("src/vendor/common/kotlin")
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":ui"))
|
api(project(":compose:ui:ui"))
|
||||||
api(project(":animation-core"))
|
api(project(":compose:animation:animation-core"))
|
||||||
api(project(":animation"))
|
api(project(":compose:animation:animation"))
|
||||||
api(project(":foundation-layout"))
|
api(project(":compose:foundation:foundation-layout"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeMain {
|
nativeMain {
|
||||||
|
|||||||
@@ -356,7 +356,6 @@ commonMain/kotlin/ -> src/vendor/common/kotlin/
|
|||||||
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
|
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
|
||||||
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt
|
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionColors.kt
|
||||||
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt
|
# | commonMain/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt -> src/vendor/common/kotlin/androidx/compose/foundation/text/selection/TextSelectionDelegate.kt
|
||||||
!commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionLayout.kt
|
|
||||||
|
|
||||||
macosMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.macos.kt -> src/vendor/native/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.native.kt
|
macosMain/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.macos.kt -> src/vendor/native/kotlin/androidx/compose/foundation/text/selection/DefaultTextSelectionColors.native.kt
|
||||||
|
|
||||||
|
|||||||
-810
@@ -1,810 +0,0 @@
|
|||||||
@file:Suppress("OPTIONAL_DECLARATION_USAGE_IN_NON_COMMON_SOURCE", "LESS_VISIBLE_TYPE_ACCESS_IN_INLINE")
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2023 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// MANUAL VENDOR of upstream compose/foundation/foundation/src/commonMain/kotlin/
|
|
||||||
// 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
|
|
||||||
// infoList index to the valid range. Upstream assumes that if the drag position never
|
|
||||||
// resolves ON or BEFORE any selectable, at least one slot is anchored on a text (odd).
|
|
||||||
// That assumption breaks when a mouse-down starts a NEW selection AFTER every selectable
|
|
||||||
// in the container — both slots stay UNASSIGNED and get defaulted to lastSlot = 2N by
|
|
||||||
// SelectionLayoutBuilder.build(), so startSlot / 2 == infoList.size (OOB). Repro:
|
|
||||||
// apidemo response viewer, drag-select in the empty area below the last body block.
|
|
||||||
|
|
||||||
package androidx.compose.foundation.text.selection
|
|
||||||
|
|
||||||
import androidx.collection.LongIntMap
|
|
||||||
import androidx.collection.LongObjectMap
|
|
||||||
import androidx.collection.MutableLongIntMap
|
|
||||||
import androidx.collection.buildLongObjectMap
|
|
||||||
import androidx.collection.emptyLongObjectMap
|
|
||||||
import androidx.collection.longObjectMapOf
|
|
||||||
import androidx.collection.mutableLongIntMapOf
|
|
||||||
import androidx.collection.mutableLongObjectMapOf
|
|
||||||
import androidx.compose.foundation.internal.checkPrecondition
|
|
||||||
import androidx.compose.foundation.text.selection.Direction.AFTER
|
|
||||||
import androidx.compose.foundation.text.selection.Direction.BEFORE
|
|
||||||
import androidx.compose.foundation.text.selection.Direction.ON
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.layout.LayoutCoordinates
|
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selection data around how the pointer relates to the actual positions of the Text components.
|
|
||||||
*
|
|
||||||
* # Explanation of Slots
|
|
||||||
*
|
|
||||||
* The Slot value is meant to sit either *on* an index or *between* indices. The former means the
|
|
||||||
* pointer is on a `Text` (like slot value `1` and index `0` below). The latter means the pointer is
|
|
||||||
* not on a `Text`, but between `Text`s (like slot value `0` or `2` below). So a slot value of `2`
|
|
||||||
* means that the pointer is between the `Text`s at index `0` and `1`, perhaps in padding or a
|
|
||||||
* non-selectable `Text`.
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* slot value 0 1 2 3 4 5 6 7 8 9 10
|
|
||||||
* info index 0 1 2 3 4
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## Mappings:
|
|
||||||
* The `X` represents an impossible slot assignment The `|`, `/`, and `\` represent a slot mapping.
|
|
||||||
*
|
|
||||||
* ### Mapping minimum slot:
|
|
||||||
* ```
|
|
||||||
* slot value 0 1 2 3 4 5 6 7 8 9 10
|
|
||||||
* \ | \ | \ | \ | \ | X
|
|
||||||
* info index 0 1 2 3 4
|
|
||||||
*
|
|
||||||
* min-slot index = slot / 2
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Minimum slot cannot be after the final `Text` (index `4`).
|
|
||||||
*
|
|
||||||
* ### Mapping maximum slot:
|
|
||||||
* ```
|
|
||||||
* slot value 0 1 2 3 4 5 6 7 8 9 10
|
|
||||||
* X | / | / | / | / | /
|
|
||||||
* info index 0 1 2 3 4
|
|
||||||
* max-slot index = (slot - 1) / 2
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Maximum slot cannot be before the first `Text` (index `0`).
|
|
||||||
*
|
|
||||||
* ## Assertions
|
|
||||||
* * The non-dragging slot should always be directly on a text (odd) because the non-dragging handle
|
|
||||||
* must be anchored somewhere.
|
|
||||||
* * Because of this, we can determine that if `startSlot == endSlot` then it also follows that
|
|
||||||
* `startSlot` and `endSlot` are even.
|
|
||||||
*/
|
|
||||||
internal interface SelectionLayout {
|
|
||||||
/** The number of [SelectableInfo]s in this [SelectionLayout]. */
|
|
||||||
val size: Int
|
|
||||||
|
|
||||||
/** The slot of the start anchor. */
|
|
||||||
val startSlot: Int
|
|
||||||
|
|
||||||
/** The slot of the end anchor. */
|
|
||||||
val endSlot: Int
|
|
||||||
|
|
||||||
/** The [CrossStatus] of this layout as determined by the slot/offsets. */
|
|
||||||
val crossStatus: CrossStatus
|
|
||||||
|
|
||||||
/** The [SelectableInfo] that the start is on. */
|
|
||||||
val startInfo: SelectableInfo
|
|
||||||
|
|
||||||
/** The [SelectableInfo] that the end is on. */
|
|
||||||
val endInfo: SelectableInfo
|
|
||||||
|
|
||||||
/** The [SelectableInfo] that the start/end is on as determined by [isStartHandle]. */
|
|
||||||
val currentInfo: SelectableInfo
|
|
||||||
|
|
||||||
/** The [SelectableInfo] for the first selectable on the screen. */
|
|
||||||
val firstInfo: SelectableInfo
|
|
||||||
|
|
||||||
/** The [SelectableInfo] for the last selectable on the screen. */
|
|
||||||
val lastInfo: SelectableInfo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a function on every [SelectableInfo] between [firstInfo] and [lastInfo] (not including
|
|
||||||
* [firstInfo]/[lastInfo]).
|
|
||||||
*/
|
|
||||||
fun forEachMiddleInfo(block: (SelectableInfo) -> Unit)
|
|
||||||
|
|
||||||
/** Whether the start or end anchor is currently being moved. */
|
|
||||||
val isStartHandle: Boolean
|
|
||||||
|
|
||||||
/** The previous [Selection] that we are modifying. */
|
|
||||||
val previousSelection: Selection?
|
|
||||||
|
|
||||||
/** Returns the [SelectableInfo] of the selectable with the given id. */
|
|
||||||
fun infoForSelectable(selectableId: Long): SelectableInfo?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this layout, compared to another layout, has any relevant changes that would require
|
|
||||||
* recomputing selection.
|
|
||||||
*
|
|
||||||
* @param other the selection layout to check for changes compared to this one
|
|
||||||
*/
|
|
||||||
fun shouldRecomputeSelection(other: SelectionLayout?): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits a selection into a Map of selectable ID to Selections limited to that selectable ID.
|
|
||||||
*
|
|
||||||
* @param selection The selection to turn into subSelections
|
|
||||||
*/
|
|
||||||
fun createSubSelections(selection: Selection): LongObjectMap<Selection>
|
|
||||||
}
|
|
||||||
|
|
||||||
private class MultiSelectionLayout(
|
|
||||||
val selectableIdToInfoListIndex: LongIntMap,
|
|
||||||
val infoList: List<SelectableInfo>,
|
|
||||||
override val startSlot: Int,
|
|
||||||
override val endSlot: Int,
|
|
||||||
override val isStartHandle: Boolean,
|
|
||||||
override val previousSelection: Selection?,
|
|
||||||
) : SelectionLayout {
|
|
||||||
init {
|
|
||||||
checkPrecondition(infoList.size > 1) {
|
|
||||||
"MultiSelectionLayout requires an infoList size greater than 1, was ${infoList.size}."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _infoBySelectableId: LongObjectMap<SelectableInfo>? = null
|
|
||||||
private val infoBySelectableId: LongObjectMap<SelectableInfo>
|
|
||||||
get() {
|
|
||||||
var result = _infoBySelectableId
|
|
||||||
if (result == null) {
|
|
||||||
result = buildLongObjectMap { infoList.fastForEach { put(it.selectableId, it) } }
|
|
||||||
_infoBySelectableId = result
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Most of these properties are unused unless shouldRecomputeSelection returns true,
|
|
||||||
// hence why getters are used everywhere.
|
|
||||||
|
|
||||||
override val size
|
|
||||||
get() = infoList.size
|
|
||||||
|
|
||||||
override val crossStatus: CrossStatus
|
|
||||||
get() =
|
|
||||||
when {
|
|
||||||
startSlot < endSlot -> CrossStatus.NOT_CROSSED
|
|
||||||
startSlot > endSlot -> CrossStatus.CROSSED
|
|
||||||
// FORK CHANGE: clamp when the gesture starts past every selectable, both slots
|
|
||||||
// default to lastSlot = 2 * infoList.size, so slot / 2 would be out of bounds.
|
|
||||||
else -> infoList[(startSlot / 2).coerceAtMost(infoList.lastIndex)].rawCrossStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
override val startInfo: SelectableInfo
|
|
||||||
get() = infoList[startOrEndSlotToIndex(startSlot, isStartSlot = true)]
|
|
||||||
|
|
||||||
override val endInfo: SelectableInfo
|
|
||||||
get() = infoList[startOrEndSlotToIndex(endSlot, isStartSlot = false)]
|
|
||||||
|
|
||||||
override val currentInfo: SelectableInfo
|
|
||||||
get() = if (isStartHandle) startInfo else endInfo
|
|
||||||
|
|
||||||
override val firstInfo: SelectableInfo
|
|
||||||
get() = if (crossStatus == CrossStatus.CROSSED) endInfo else startInfo
|
|
||||||
|
|
||||||
override val lastInfo: SelectableInfo
|
|
||||||
get() = if (crossStatus == CrossStatus.CROSSED) startInfo else endInfo
|
|
||||||
|
|
||||||
override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
|
|
||||||
val minIndex = getInfoListIndexBySelectableId(firstInfo.selectableId)
|
|
||||||
val maxIndex = getInfoListIndexBySelectableId(lastInfo.selectableId)
|
|
||||||
if (minIndex + 1 >= maxIndex) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i in minIndex + 1 until maxIndex) {
|
|
||||||
block(infoList[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun infoForSelectable(selectableId: Long): SelectableInfo? {
|
|
||||||
return infoBySelectableId[selectableId]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
|
|
||||||
previousSelection == null ||
|
|
||||||
other == null ||
|
|
||||||
other !is MultiSelectionLayout ||
|
|
||||||
isStartHandle != other.isStartHandle ||
|
|
||||||
startSlot != other.startSlot ||
|
|
||||||
endSlot != other.endSlot ||
|
|
||||||
shouldAnyInfoRecomputeSelection(other)
|
|
||||||
|
|
||||||
private fun shouldAnyInfoRecomputeSelection(other: MultiSelectionLayout): Boolean {
|
|
||||||
if (size != other.size) return true
|
|
||||||
for (i in infoList.indices) {
|
|
||||||
val thisInfo = infoList[i]
|
|
||||||
val otherInfo = other.infoList[i]
|
|
||||||
if (thisInfo.shouldRecomputeSelection(otherInfo)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createSubSelections(selection: Selection): LongObjectMap<Selection> =
|
|
||||||
createSubSelections(
|
|
||||||
selection = selection,
|
|
||||||
sortedItems = infoList,
|
|
||||||
getId = { it.selectableId },
|
|
||||||
getSelectAll = { info, _ ->
|
|
||||||
if (selection.handlesCrossed) {
|
|
||||||
info.makeSingleLayoutSelection(info.textLength, 0)
|
|
||||||
} else {
|
|
||||||
info.makeSingleLayoutSelection(0, info.textLength)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
createBoundarySelection = { info, isStart, offset, _ ->
|
|
||||||
val isCrossed = selection.handlesCrossed
|
|
||||||
if (isStart) {
|
|
||||||
if (isCrossed) {
|
|
||||||
info.makeSingleLayoutSelection(start = offset, end = 0)
|
|
||||||
} else {
|
|
||||||
info.makeSingleLayoutSelection(start = offset, end = info.textLength)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (isCrossed) {
|
|
||||||
info.makeSingleLayoutSelection(start = info.textLength, end = offset)
|
|
||||||
} else {
|
|
||||||
info.makeSingleLayoutSelection(start = 0, end = offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toString(): String =
|
|
||||||
"MultiSelectionLayout(isStartHandle=$isStartHandle, " +
|
|
||||||
"startPosition=${(startSlot + 1).toFloat() / 2}, " +
|
|
||||||
"endPosition=${(endSlot + 1).toFloat() / 2}, " +
|
|
||||||
"crossed=$crossStatus, " +
|
|
||||||
"infos=${
|
|
||||||
buildString {
|
|
||||||
append("[\n\t")
|
|
||||||
var first = true
|
|
||||||
infoList
|
|
||||||
.fastForEachIndexed { index, info ->
|
|
||||||
if (first) {
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
append(",\n\t")
|
|
||||||
}
|
|
||||||
append("${(index + 1)} -> $info")
|
|
||||||
}
|
|
||||||
append("\n]")
|
|
||||||
}
|
|
||||||
})"
|
|
||||||
|
|
||||||
private fun startOrEndSlotToIndex(slot: Int, isStartSlot: Boolean): Int =
|
|
||||||
slotToIndex(
|
|
||||||
slot = slot,
|
|
||||||
isMinimumSlot =
|
|
||||||
when (crossStatus) {
|
|
||||||
// collapsed: doesn't matter whether true or false, it will result in the same
|
|
||||||
// index
|
|
||||||
CrossStatus.COLLAPSED -> true
|
|
||||||
CrossStatus.NOT_CROSSED -> isStartSlot
|
|
||||||
CrossStatus.CROSSED -> !isStartSlot
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun slotToIndex(slot: Int, isMinimumSlot: Boolean): Int {
|
|
||||||
val slotAdjustment = if (isMinimumSlot) 0 else 1
|
|
||||||
// FORK CHANGE: clamp when the gesture starts past every selectable, slots default
|
|
||||||
// to lastSlot = 2 * infoList.size, so slot / 2 would be out of bounds.
|
|
||||||
return ((slot - slotAdjustment) / 2).coerceIn(0, infoList.lastIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getInfoListIndexBySelectableId(id: Long): Int =
|
|
||||||
try {
|
|
||||||
selectableIdToInfoListIndex[id]
|
|
||||||
} catch (e: NoSuchElementException) {
|
|
||||||
throw IllegalStateException("Invalid selectableId: $id", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a selection layout that has only one slot.
|
|
||||||
*
|
|
||||||
* @param isStartHandle whether this is the start or end anchor
|
|
||||||
* @param previousSelection the previous selection
|
|
||||||
* @param info the single [SelectableInfo]
|
|
||||||
*/
|
|
||||||
private class SingleSelectionLayout(
|
|
||||||
override val isStartHandle: Boolean,
|
|
||||||
override val startSlot: Int,
|
|
||||||
override val endSlot: Int,
|
|
||||||
override val previousSelection: Selection?,
|
|
||||||
private val info: SelectableInfo,
|
|
||||||
) : SelectionLayout {
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_SLOT = 1
|
|
||||||
const val DEFAULT_SELECTABLE_ID = 1L
|
|
||||||
}
|
|
||||||
|
|
||||||
override val size
|
|
||||||
get() = 1
|
|
||||||
|
|
||||||
override val crossStatus: CrossStatus
|
|
||||||
get() =
|
|
||||||
when {
|
|
||||||
startSlot < endSlot -> CrossStatus.NOT_CROSSED
|
|
||||||
startSlot > endSlot -> CrossStatus.CROSSED
|
|
||||||
else -> info.rawCrossStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
override val startInfo: SelectableInfo
|
|
||||||
get() = info
|
|
||||||
|
|
||||||
override val endInfo: SelectableInfo
|
|
||||||
get() = info
|
|
||||||
|
|
||||||
override val currentInfo: SelectableInfo
|
|
||||||
get() = info
|
|
||||||
|
|
||||||
override val firstInfo: SelectableInfo
|
|
||||||
get() = info
|
|
||||||
|
|
||||||
override val lastInfo: SelectableInfo
|
|
||||||
get() = info
|
|
||||||
|
|
||||||
override fun forEachMiddleInfo(block: (SelectableInfo) -> Unit) {
|
|
||||||
// there are no middle infos, so do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun infoForSelectable(selectableId: Long): SelectableInfo? {
|
|
||||||
return if (selectableId == info.selectableId) info else null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldRecomputeSelection(other: SelectionLayout?): Boolean =
|
|
||||||
previousSelection == null ||
|
|
||||||
other == null ||
|
|
||||||
other !is SingleSelectionLayout ||
|
|
||||||
startSlot != other.startSlot ||
|
|
||||||
endSlot != other.endSlot ||
|
|
||||||
isStartHandle != other.isStartHandle ||
|
|
||||||
info.shouldRecomputeSelection(other.info)
|
|
||||||
|
|
||||||
override fun createSubSelections(selection: Selection): LongObjectMap<Selection> {
|
|
||||||
val finalSelection =
|
|
||||||
selection.run {
|
|
||||||
// uncross handles if necessary
|
|
||||||
if (
|
|
||||||
(!handlesCrossed && start.offset > end.offset) ||
|
|
||||||
(handlesCrossed && start.offset <= end.offset)
|
|
||||||
) {
|
|
||||||
copy(handlesCrossed = !handlesCrossed)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return longObjectMapOf(info.selectableId, finalSelection)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String =
|
|
||||||
"SingleSelectionLayout(isStartHandle=$isStartHandle, crossed=$crossStatus, info=\n\t$info)"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a selection layout that has only one slot.
|
|
||||||
*
|
|
||||||
* This is intended for TextField, where multiple selectables is of no concern.
|
|
||||||
*
|
|
||||||
* @param layoutResult the [TextLayoutResult] for the text field
|
|
||||||
* @param rawStartHandleOffset the index of the start handle
|
|
||||||
* @param rawEndHandleOffset the index of the end handle
|
|
||||||
* @param rawPreviousHandleOffset the previous handle offset based on [isStartHandle], or
|
|
||||||
* [UNASSIGNED_SLOT] if none
|
|
||||||
* @param previousSelectionRange the previous selection
|
|
||||||
* @param isStartOfSelection whether this is the start of a selection gesture (no previous context)
|
|
||||||
* @param isStartHandle whether this is the start or end anchor
|
|
||||||
*/
|
|
||||||
internal fun getTextFieldSelectionLayout(
|
|
||||||
layoutResult: TextLayoutResult,
|
|
||||||
rawStartHandleOffset: Int,
|
|
||||||
rawEndHandleOffset: Int,
|
|
||||||
rawPreviousHandleOffset: Int,
|
|
||||||
previousSelectionRange: TextRange,
|
|
||||||
isStartOfSelection: Boolean,
|
|
||||||
isStartHandle: Boolean,
|
|
||||||
): SelectionLayout =
|
|
||||||
SingleSelectionLayout(
|
|
||||||
isStartHandle = isStartHandle,
|
|
||||||
startSlot = SingleSelectionLayout.DEFAULT_SLOT,
|
|
||||||
endSlot = SingleSelectionLayout.DEFAULT_SLOT,
|
|
||||||
previousSelection =
|
|
||||||
if (isStartOfSelection) null
|
|
||||||
else
|
|
||||||
Selection(
|
|
||||||
start =
|
|
||||||
Selection.AnchorInfo(
|
|
||||||
layoutResult.getTextDirectionForOffset(previousSelectionRange.start),
|
|
||||||
previousSelectionRange.start,
|
|
||||||
SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
|
|
||||||
),
|
|
||||||
end =
|
|
||||||
Selection.AnchorInfo(
|
|
||||||
layoutResult.getTextDirectionForOffset(previousSelectionRange.end),
|
|
||||||
previousSelectionRange.end,
|
|
||||||
SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
|
|
||||||
),
|
|
||||||
handlesCrossed = previousSelectionRange.reversed,
|
|
||||||
),
|
|
||||||
info =
|
|
||||||
SelectableInfo(
|
|
||||||
selectableId = SingleSelectionLayout.DEFAULT_SELECTABLE_ID,
|
|
||||||
slot = SingleSelectionLayout.DEFAULT_SLOT,
|
|
||||||
rawStartHandleOffset = rawStartHandleOffset,
|
|
||||||
rawEndHandleOffset = rawEndHandleOffset,
|
|
||||||
textLayoutResult = layoutResult,
|
|
||||||
rawPreviousHandleOffset = rawPreviousHandleOffset,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Whether something is crossed as determined by the position of the start/end. */
|
|
||||||
internal enum class CrossStatus {
|
|
||||||
/** The start comes after the end. */
|
|
||||||
CROSSED,
|
|
||||||
|
|
||||||
/** The start comes before the end. */
|
|
||||||
NOT_CROSSED,
|
|
||||||
|
|
||||||
/** The start is the same as the end. */
|
|
||||||
COLLAPSED,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Slot has not been assigned yet */
|
|
||||||
internal const val UNASSIGNED_SLOT = -1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A builder for [SelectionLayout] that ensures the data structures and slots are properly
|
|
||||||
* constructed.
|
|
||||||
*
|
|
||||||
* @param previousHandlePosition the previous handle position matching the handle directed to by
|
|
||||||
* [isStartHandle]
|
|
||||||
* @param containerCoordinates the coordinates of the [SelectionContainer] for converting
|
|
||||||
* [SelectionContainer] coordinates to their respective [Selectable] coordinates
|
|
||||||
* @param isStartHandle whether the currently pressed/clicked handle is the start
|
|
||||||
* @param previousLayout The [SelectionLayout] created when [currentPosition] was
|
|
||||||
* [previousHandlePosition]. When non-null, it is used to obtain
|
|
||||||
* [SelectableInfo.rawPreviousHandleOffset] instead of re-computing it from
|
|
||||||
* [previousHandlePosition]. Note that this is not just an optimization. When selectables are
|
|
||||||
* moved (due to drag-to-scroll, for example) inside the selection container, it becomes
|
|
||||||
* impossible to correctly deduce [SelectableInfo.rawPreviousHandleOffset] from
|
|
||||||
* [previousHandlePosition] (because to do that, the "previous" layout coordinates are needed, but
|
|
||||||
* are not available).
|
|
||||||
* @param selectableIdOrderingComparator determines the ordering of selectables by their IDs
|
|
||||||
* @param allowSelectionBetweenSelectables whether selection in the "empty" area where there are no
|
|
||||||
* selectables is allowed.
|
|
||||||
*/
|
|
||||||
internal class SelectionLayoutBuilder(
|
|
||||||
val currentPosition: Offset,
|
|
||||||
val previousHandlePosition: Offset,
|
|
||||||
val containerCoordinates: LayoutCoordinates,
|
|
||||||
val isStartHandle: Boolean,
|
|
||||||
val previousSelection: Selection?,
|
|
||||||
val previousLayout: SelectionLayout?,
|
|
||||||
val selectableIdOrderingComparator: Comparator<Long>,
|
|
||||||
val allowSelectionBetweenSelectables: Boolean = false,
|
|
||||||
) {
|
|
||||||
private val selectableIdToInfoListIndex: MutableLongIntMap = mutableLongIntMapOf()
|
|
||||||
private val infoList: MutableList<SelectableInfo> = mutableListOf()
|
|
||||||
private var startSlot: Int = UNASSIGNED_SLOT
|
|
||||||
private var endSlot: Int = UNASSIGNED_SLOT
|
|
||||||
private var currentSlot: Int = UNASSIGNED_SLOT
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finishes building the [SelectionLayout] and returns it.
|
|
||||||
*
|
|
||||||
* @return the [SelectionLayout] or null if no [SelectableInfo]s were added.
|
|
||||||
*/
|
|
||||||
fun build(): SelectionLayout? {
|
|
||||||
val lastSlot = currentSlot + 1
|
|
||||||
return when (infoList.size) {
|
|
||||||
0 -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
SingleSelectionLayout(
|
|
||||||
info = infoList.single(),
|
|
||||||
startSlot = if (startSlot == UNASSIGNED_SLOT) lastSlot else startSlot,
|
|
||||||
endSlot = if (endSlot == UNASSIGNED_SLOT) lastSlot else endSlot,
|
|
||||||
previousSelection = previousSelection,
|
|
||||||
isStartHandle = isStartHandle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
MultiSelectionLayout(
|
|
||||||
selectableIdToInfoListIndex = selectableIdToInfoListIndex,
|
|
||||||
infoList = infoList,
|
|
||||||
startSlot = if (startSlot == UNASSIGNED_SLOT) lastSlot else startSlot,
|
|
||||||
endSlot = if (endSlot == UNASSIGNED_SLOT) lastSlot else endSlot,
|
|
||||||
isStartHandle = isStartHandle,
|
|
||||||
previousSelection = previousSelection,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Appends a selection info to this builder. */
|
|
||||||
fun appendInfo(
|
|
||||||
selectableId: Long,
|
|
||||||
rawStartHandleOffset: Int,
|
|
||||||
startXHandleDirection: Direction,
|
|
||||||
startYHandleDirection: Direction,
|
|
||||||
rawEndHandleOffset: Int,
|
|
||||||
endXHandleDirection: Direction,
|
|
||||||
endYHandleDirection: Direction,
|
|
||||||
rawPreviousHandleOffset: Int,
|
|
||||||
textLayoutResult: TextLayoutResult,
|
|
||||||
): SelectableInfo {
|
|
||||||
// We need currentSlot to equal the slot of the "last" info when getLayout is called,
|
|
||||||
// so increment this before adding the info and leave the correct slot in place at the end.
|
|
||||||
currentSlot += 2
|
|
||||||
|
|
||||||
val selectableInfo =
|
|
||||||
SelectableInfo(
|
|
||||||
selectableId = selectableId,
|
|
||||||
slot = currentSlot,
|
|
||||||
rawStartHandleOffset = rawStartHandleOffset,
|
|
||||||
rawEndHandleOffset = rawEndHandleOffset,
|
|
||||||
rawPreviousHandleOffset = rawPreviousHandleOffset,
|
|
||||||
textLayoutResult = textLayoutResult,
|
|
||||||
)
|
|
||||||
|
|
||||||
startSlot = updateSlot(startSlot, startXHandleDirection, startYHandleDirection)
|
|
||||||
endSlot = updateSlot(endSlot, endXHandleDirection, endYHandleDirection)
|
|
||||||
selectableIdToInfoListIndex[selectableId] = infoList.size
|
|
||||||
infoList += selectableInfo
|
|
||||||
return selectableInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the slot for a selectable given the current position's directions from the selectable.
|
|
||||||
*
|
|
||||||
* The selectables must be ordered in the order in which they would be selected, and then this
|
|
||||||
* function should be called for each of those selectables.
|
|
||||||
*
|
|
||||||
* It is expected that the input [slot] is also assigned the result of this function.
|
|
||||||
*
|
|
||||||
* This function is stateful.
|
|
||||||
*
|
|
||||||
* @param slot the current value of this slot.
|
|
||||||
* @param xPositionDirection Where the x-position is relative to the selectable
|
|
||||||
* @param yPositionDirection Where the y-position is relative to the selectable
|
|
||||||
*/
|
|
||||||
private fun updateSlot(
|
|
||||||
slot: Int,
|
|
||||||
xPositionDirection: Direction,
|
|
||||||
yPositionDirection: Direction,
|
|
||||||
): Int {
|
|
||||||
if (slot != UNASSIGNED_SLOT) {
|
|
||||||
// don't overwrite if the slot has already been determined
|
|
||||||
return slot
|
|
||||||
}
|
|
||||||
|
|
||||||
// slot has not been determined yet,
|
|
||||||
// see if we are on or past the selectable we are looking for
|
|
||||||
return when (resolve2dDirection(xPositionDirection, yPositionDirection)) {
|
|
||||||
// If we get here, that means we never found a selectable that contains our gesture
|
|
||||||
// position. This is the first selectable that is after the position,
|
|
||||||
// so our slot must be between the previous and current selectables.
|
|
||||||
BEFORE -> currentSlot - 1
|
|
||||||
|
|
||||||
// The gesture position is directly on this selectable, so use this one.
|
|
||||||
ON -> currentSlot
|
|
||||||
|
|
||||||
// keep looking
|
|
||||||
AFTER -> slot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Where the position of a cursor/press is compared to a selectable. */
|
|
||||||
internal enum class Direction {
|
|
||||||
/** The cursor/press is before the selectable */
|
|
||||||
BEFORE,
|
|
||||||
|
|
||||||
/** The cursor/press is on the selectable */
|
|
||||||
ON,
|
|
||||||
|
|
||||||
/** The cursor/press is after the selectable */
|
|
||||||
AFTER,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine direction based on an x/y direction.
|
|
||||||
*
|
|
||||||
* This will use the [y] direction unless it is [ON], in which case it will use the [x] direction.
|
|
||||||
*/
|
|
||||||
internal fun resolve2dDirection(x: Direction, y: Direction): Direction =
|
|
||||||
when (y) {
|
|
||||||
BEFORE -> BEFORE
|
|
||||||
ON ->
|
|
||||||
when (x) {
|
|
||||||
BEFORE -> BEFORE
|
|
||||||
ON -> ON
|
|
||||||
AFTER -> AFTER
|
|
||||||
}
|
|
||||||
AFTER -> AFTER
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Data about a specific selectable within a [SelectionLayout]. */
|
|
||||||
internal class SelectableInfo(
|
|
||||||
val selectableId: Long,
|
|
||||||
val slot: Int,
|
|
||||||
val rawStartHandleOffset: Int,
|
|
||||||
val rawEndHandleOffset: Int,
|
|
||||||
val rawPreviousHandleOffset: Int,
|
|
||||||
val textLayoutResult: TextLayoutResult,
|
|
||||||
) {
|
|
||||||
|
|
||||||
/** The [String] in the selectable. */
|
|
||||||
val inputText: String
|
|
||||||
get() = textLayoutResult.layoutInput.text.text
|
|
||||||
|
|
||||||
/** The length of the [String] in the selectable. */
|
|
||||||
val textLength: Int
|
|
||||||
get() = inputText.length
|
|
||||||
|
|
||||||
/** Whether the raw offsets of this info are crossed. */
|
|
||||||
val rawCrossStatus: CrossStatus
|
|
||||||
get() =
|
|
||||||
when {
|
|
||||||
rawStartHandleOffset < rawEndHandleOffset -> CrossStatus.NOT_CROSSED
|
|
||||||
rawStartHandleOffset > rawEndHandleOffset -> CrossStatus.CROSSED
|
|
||||||
else -> CrossStatus.COLLAPSED
|
|
||||||
}
|
|
||||||
|
|
||||||
private val startRunDirection
|
|
||||||
get() = textLayoutResult.getTextDirectionForOffset(rawStartHandleOffset)
|
|
||||||
|
|
||||||
private val endRunDirection
|
|
||||||
get() = textLayoutResult.getTextDirectionForOffset(rawEndHandleOffset)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this info, compared to another info, has any relevant changes that would require
|
|
||||||
* recomputing selection.
|
|
||||||
*
|
|
||||||
* @param other the selectable info to check for changes compared to this one
|
|
||||||
*/
|
|
||||||
fun shouldRecomputeSelection(other: SelectableInfo): Boolean =
|
|
||||||
selectableId != other.selectableId ||
|
|
||||||
rawStartHandleOffset != other.rawStartHandleOffset ||
|
|
||||||
rawEndHandleOffset != other.rawEndHandleOffset
|
|
||||||
|
|
||||||
/** Get a [Selection.AnchorInfo] for this [SelectableInfo] at the given [offset]. */
|
|
||||||
fun anchorForOffset(offset: Int): Selection.AnchorInfo =
|
|
||||||
Selection.AnchorInfo(
|
|
||||||
direction = textLayoutResult.getTextDirectionForOffset(offset),
|
|
||||||
offset = offset,
|
|
||||||
selectableId = selectableId,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a [Selection] within the selectable represented by this [SelectableInfo] for the given
|
|
||||||
* [start] and [end] offsets.
|
|
||||||
*/
|
|
||||||
fun makeSingleLayoutSelection(start: Int, end: Int): Selection =
|
|
||||||
Selection(
|
|
||||||
start = anchorForOffset(start),
|
|
||||||
end = anchorForOffset(end),
|
|
||||||
handlesCrossed = start > end,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun toString(): String =
|
|
||||||
"SelectionInfo(id=$selectableId, " +
|
|
||||||
"range=($rawStartHandleOffset-$startRunDirection,$rawEndHandleOffset-$endRunDirection), " +
|
|
||||||
"prevOffset=$rawPreviousHandleOffset)"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that the selection is truly collapsed.
|
|
||||||
*
|
|
||||||
* If the selection is contained within one selectable, this simply checks if the offsets are equal.
|
|
||||||
*
|
|
||||||
* If the Selection spans multiple selectables, then this will verify that every selected selectable
|
|
||||||
* contains a zero-width selection.
|
|
||||||
*/
|
|
||||||
internal fun Selection?.isCollapsed(layout: SelectionLayout?): Boolean {
|
|
||||||
this ?: return true
|
|
||||||
layout ?: return true
|
|
||||||
|
|
||||||
// Selection is within one selectable, simply check if the offsets are the same.
|
|
||||||
if (start.selectableId == end.selectableId) {
|
|
||||||
return start.offset == end.offset
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that maxAnchor offset is 0, else the selection cannot be collapsed.
|
|
||||||
val maxAnchor = if (handlesCrossed) start else end
|
|
||||||
if (maxAnchor.offset != 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that the minAnchor offset is equal to the length of the text,
|
|
||||||
// else the selection is not collapsed
|
|
||||||
val minAnchor = if (handlesCrossed) end else start
|
|
||||||
if (layout.firstInfo.textLength != minAnchor.offset) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every selectable between the min and max must have empty text,
|
|
||||||
// else there is some text selected.
|
|
||||||
var allTextsEmpty = true
|
|
||||||
layout.forEachMiddleInfo {
|
|
||||||
if (it.inputText.isNotEmpty()) {
|
|
||||||
allTextsEmpty = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allTextsEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun <T> createSubSelections(
|
|
||||||
selection: Selection,
|
|
||||||
sortedItems: List<T>,
|
|
||||||
getId: (T) -> Long,
|
|
||||||
getSelectAll: (T, handlesCrossed: Boolean) -> Selection?,
|
|
||||||
createBoundarySelection:
|
|
||||||
(item: T, isStart: Boolean, offset: Int, handlesCrossed: Boolean) -> Selection?,
|
|
||||||
): LongObjectMap<Selection> {
|
|
||||||
val startId = selection.start.selectableId
|
|
||||||
val endId = selection.end.selectableId
|
|
||||||
|
|
||||||
if (startId == endId) {
|
|
||||||
return longObjectMapOf(startId, selection)
|
|
||||||
}
|
|
||||||
|
|
||||||
val startIndex = sortedItems.indexOfFirst { getId(it) == startId }
|
|
||||||
val endIndex = sortedItems.indexOfFirst { getId(it) == endId }
|
|
||||||
|
|
||||||
if (startIndex == -1 || endIndex == -1) {
|
|
||||||
return emptyLongObjectMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
val firstIndex = minOf(startIndex, endIndex)
|
|
||||||
val lastIndex = maxOf(startIndex, endIndex)
|
|
||||||
val handlesCrossed = startIndex > endIndex
|
|
||||||
|
|
||||||
val result = mutableLongObjectMapOf<Selection>()
|
|
||||||
|
|
||||||
for (i in firstIndex..lastIndex) {
|
|
||||||
val item = sortedItems[i]
|
|
||||||
val id = getId(item)
|
|
||||||
|
|
||||||
val subSelection =
|
|
||||||
when (id) {
|
|
||||||
startId ->
|
|
||||||
createBoundarySelection(item, true, selection.start.offset, handlesCrossed)
|
|
||||||
endId -> createBoundarySelection(item, false, selection.end.offset, handlesCrossed)
|
|
||||||
else -> getSelectAll(item, handlesCrossed)
|
|
||||||
}
|
|
||||||
subSelection?.let { result[id] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
+2
-2
@@ -10,7 +10,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: IconText — icon-font text on the standard text path
|
// MARK: IconText - icon-font text on the standard text path
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.unit.TextUnit
|
|||||||
Now just a [BasicText]: the icon font family and its variable axes are threaded
|
Now just a [BasicText]: the icon font family and its variable axes are threaded
|
||||||
through [namedFontFamily] (`axes = …`), which the skiko text engine reads via
|
through [namedFontFamily] (`axes = …`), which the skiko text engine reads via
|
||||||
`FontFamily.projectFontVariations()` and applies to the typeface. So icons
|
`FontFamily.projectFontVariations()` and applies to the typeface. So icons
|
||||||
measure + draw through the same skiko `skparagraph` path as ordinary text — no
|
measure + draw through the same skiko `skparagraph` path as ordinary text - no
|
||||||
separate renderer/measurer seam.
|
separate renderer/measurer seam.
|
||||||
|
|
||||||
Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText /
|
Material `Icon(codepoint = …, fontFamily = …)` uses this; Text / BasicText /
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
|
|
||||||
/** One selection-highlight rectangle (in layout pixels), painted behind the glyphs.
|
/** One selection-highlight rectangle (in layout pixels), painted behind the glyphs.
|
||||||
Internal render glue shared by BasicText (selection-aware) and BasicTextField.
|
Internal render glue shared by BasicText (selection-aware) and BasicTextField.
|
||||||
Sizes come from Paragraph.getBoundingBox / getPathForRange — already pixels
|
Sizes come from Paragraph.getBoundingBox / getPathForRange - already pixels
|
||||||
(see § HiDPI in CLAUDE.md), so use the pixel-based offset + a raw Layout
|
(see § HiDPI in CLAUDE.md), so use the pixel-based offset + a raw Layout
|
||||||
sizing shim instead of `.dp` (which would double-scale on Retina). */
|
sizing shim instead of `.dp` (which would double-scale on Retina). */
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ fun HorizontalSplitPane(
|
|||||||
|
|
||||||
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
||||||
|
|
||||||
// Solid divider that fills its own slot — what you see is exactly what
|
// 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
|
// 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
|
// size change → no layout shift, and no flicker when the pointer briefly
|
||||||
// leaves the slot mid-drag).
|
// leaves the slot mid-drag).
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user