mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 10:47:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b420a7bad | ||
|
|
2dad6c9bcc | ||
|
|
723f80c8af | ||
|
|
450247175f | ||
|
|
5d6f772a1c | ||
|
|
575c19f921 | ||
|
|
328663b9e7 | ||
|
|
c9ddd3488d | ||
|
|
9958386792 | ||
|
|
f5fae6d32e | ||
|
|
b94f1f7bbb | ||
|
|
ffbb87eddd | ||
|
|
63fad4083f | ||
|
|
872d740ce7 | ||
|
|
4c2d46429d | ||
|
|
120abbad93 | ||
|
|
a39002f1b8 | ||
|
|
fb470ecf0c | ||
|
|
2b6ac65a1f | ||
|
|
1b2e83a155 | ||
|
|
8a6cb868d9 | ||
|
|
4a7080b313 | ||
|
|
1afb13aae0 | ||
|
|
3d519bcfcd | ||
|
|
72287fc995 | ||
|
|
0b4a170910 | ||
|
|
72e6f145b3 | ||
|
|
367ed94c01 | ||
|
|
fa9bd93ada | ||
|
|
d1551bc9c1 |
@@ -18,9 +18,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Each host runs only the publish tasks Gradle actually generated for it.
|
||||
# macOS also publishes the shared kotlinMultiplatform metadata module.
|
||||
# No library module declares jvm() today — apps do their own JVM parity
|
||||
# build against upstream Compose, so JVM isn't part of the publication.
|
||||
# WINDOWS also publishes the shared kotlinMultiplatform metadata modules
|
||||
# (see its matrix entry) + the jvm jar + the bridge plugin.
|
||||
# `target` is the Gradle K/N target token (Capitalised) — one per host,
|
||||
# used for :demo / :apidemo linkReleaseExecutable<Target>. linuxArm64 is
|
||||
# skipped for the app build because SDL3 static libs on the ubuntu runner
|
||||
@@ -31,13 +30,8 @@ jobs:
|
||||
host: macos
|
||||
target: MacosArm64
|
||||
exe_ext: kexe
|
||||
# jvm publication: only :material-symbols declares a jvm() target
|
||||
# (its common API renders via upstream Compose on JVM) — publish
|
||||
# its host-independent jar from the same host as the metadata.
|
||||
tasks: >-
|
||||
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
|
||||
publishMacosArm64PublicationToGitHubPackagesRepository
|
||||
publishJvmPublicationToGitHubPackagesRepository
|
||||
- runner: ubuntu-22.04
|
||||
host: linux
|
||||
target: LinuxX64
|
||||
@@ -49,8 +43,18 @@ jobs:
|
||||
host: windows
|
||||
target: MingwX64
|
||||
exe_ext: exe
|
||||
# WINDOWS owns the root KotlinMultiplatform metadata modules: it is
|
||||
# the ONLY host that declares every target (vHostSupportsMingw is
|
||||
# host-gated), so only its generated .module files carry the full
|
||||
# variant table — the macOS-published roots of v0.1.15 had NO
|
||||
# mingwX64 variants and Windows consumers could not resolve them.
|
||||
# The host-independent jvm jar (:material-symbols) and the bridge
|
||||
# plugin ride along on the same job.
|
||||
tasks: >-
|
||||
publishMingwX64PublicationToGitHubPackagesRepository
|
||||
publishKotlinMultiplatformPublicationToGitHubPackagesRepository
|
||||
publishJvmPublicationToGitHubPackagesRepository
|
||||
:compose-desktop-native-bridge:publishAllPublicationsToGitHubPackagesRepository
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
@@ -135,16 +139,65 @@ jobs:
|
||||
shell: bash
|
||||
run: bash scripts/compose-fork/sync.sh
|
||||
|
||||
# GitHub Packages sometimes returns 409 Conflict on a fresh upload when
|
||||
# the same coordinate was recently deleted (soft-delete propagation).
|
||||
# `--continue` lets Gradle finish the rest of the publish tasks past a
|
||||
# single 409, and the outer retry re-runs the whole thing (Gradle skips
|
||||
# already-uploaded artifacts). Two attempts is enough for the transient
|
||||
# cases we've hit; the third pass falls through as a hard failure.
|
||||
# GitHub Packages FORBIDS overwriting a file that already exists in a
|
||||
# version, and maven-publish uploads each publication's files
|
||||
# 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
|
||||
# Conflict on the first already-uploaded file and aborts BEFORE reaching
|
||||
# the missing tail files — the version is poisoned and can never
|
||||
# complete. A 409 is therefore only benign when the publication had
|
||||
# already completed; you can't tell from the 409 alone. The fix: between
|
||||
# attempts, DELETE this host's package versions for the current release
|
||||
# and re-upload from a clean slate. Each host owns DISJOINT package
|
||||
# names (roots / -mingwx64 / -jvm / bridge on Windows, -macosarm64 on
|
||||
# macOS, -linuxx64/-linuxarm64 on Linux), so parallel jobs never touch
|
||||
# each other's uploads. Uses the run's GITHUB_TOKEN (packages: write);
|
||||
# a 403 on delete just degrades to the old blind-retry behaviour.
|
||||
- name: Publish to GitHub Packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
VER="${PUBLISH_VERSION#v}"
|
||||
OWNER="${GITHUB_REPOSITORY%%/*}"
|
||||
# Keep in sync with the module list in settings.gradle.kts.
|
||||
MODULES="ui ui-util ui-geometry ui-unit ui-backhandler ui-tooling-preview animation animation-core animation-graphics foundation foundation-layout material3 material-ripple window material-symbols navigation3-ui components-resources"
|
||||
case "${{ matrix.host }}" in
|
||||
macos) SUFFIXES="-macosarm64" ;;
|
||||
linux) SUFFIXES="-linuxx64 -linuxarm64" ;;
|
||||
windows) SUFFIXES="_ROOT_ -mingwx64" ;;
|
||||
esac
|
||||
host_packages() {
|
||||
for m in $MODULES; do
|
||||
for s in $SUFFIXES; do
|
||||
if [ "$s" = "_ROOT_" ]; then echo "com.bitsycore.compose.sdl.$m"; else echo "com.bitsycore.compose.sdl.$m$s"; fi
|
||||
done
|
||||
done
|
||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||
echo "com.bitsycore.compose.sdl.material-symbols-jvm"
|
||||
echo "com.bitsycore.compose.sdl.compose-desktop-native-bridge"
|
||||
echo "com.bitsycore.compose-desktop-native.bridge.com.bitsycore.compose-desktop-native.bridge.gradle.plugin"
|
||||
fi
|
||||
}
|
||||
cleanup_host_versions() {
|
||||
echo "Deleting this host's partially-published $VER package versions"
|
||||
host_packages | while read -r pkg; do
|
||||
vid=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions?per_page=100" \
|
||||
| jq -r --arg v "$VER" '[.[]? | select(.name==$v) | .id] | first // empty')
|
||||
[ -z "$vid" ] && continue
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/users/$OWNER/packages/maven/$pkg/versions/$vid")
|
||||
if [ "$code" != "204" ]; then
|
||||
# Deleting the LAST version of a package is refused (400) —
|
||||
# a package new in this release: drop the whole package.
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/users/$OWNER/packages/maven/$pkg")
|
||||
fi
|
||||
echo " $pkg@$VER -> HTTP $code"
|
||||
done
|
||||
}
|
||||
for attempt in 1 2 3; do
|
||||
echo "::group::Publish attempt $attempt"
|
||||
if ./gradlew ${{ matrix.tasks }} --continue --no-daemon --stacktrace; then
|
||||
@@ -152,7 +205,11 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
[ $attempt -lt 3 ] && { echo "::warning::Publish attempt $attempt failed — sleeping 60s and retrying"; sleep 60; }
|
||||
if [ $attempt -lt 3 ]; then
|
||||
echo "::warning::Publish attempt $attempt failed — cleaning this host's $VER packages and retrying"
|
||||
cleanup_host_versions || true
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
echo "::error::Publish failed after 3 attempts"
|
||||
exit 1
|
||||
|
||||
@@ -42,7 +42,12 @@ compose/
|
||||
│ ├── ui-util/ → :ui-util — androidx.compose.ui.util.* (+ Experimental/InternalComposeUiApi)
|
||||
│ ├── ui-geometry/ → :ui-geometry — androidx.compose.ui.geometry.*
|
||||
│ ├── ui-unit/ → :ui-unit — androidx.compose.ui.unit.*
|
||||
│ └── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
|
||||
│ ├── ui-backhandler/ → :ui-backhandler — androidx.compose.ui.backhandler.*
|
||||
│ └── ui-tooling-preview/ → :ui-tooling-preview — androidx.compose.ui.tooling.preview.*
|
||||
│ (the common @Preview + PreviewParameterProvider,
|
||||
│ vendored verbatim; the Maven artifact ships no
|
||||
│ mingwX64/linux klibs). IDE-only metadata — previews
|
||||
│ render through the apps' jvm parity targets.
|
||||
├── animation/
|
||||
│ ├── animation-core/ → :animation-core — androidx.compose.animation.core.*
|
||||
│ ├── animation/ → :animation — androidx.compose.animation.* (non-core)
|
||||
@@ -67,9 +72,12 @@ utils/
|
||||
(Typeface.makeClone per axes — upstream's FontCache
|
||||
drops variationSettings from its key). Its commonMain
|
||||
declares official Maven compose coords — the root
|
||||
build's FULL-COMMONIZATION BRIDGE substitutes ui /
|
||||
foundation / material3 to project modules on native
|
||||
configs. Apps get one dep; the consumer Zip task
|
||||
build's FULL-COMMONIZATION BRIDGE substitutes the
|
||||
whole ui / foundation / animation / material3 /
|
||||
nav3-ui family to project modules on native configs
|
||||
(:demo and :apidemo commonMains rely on the same
|
||||
bridge; :compose-desktop-native-bridge ships it to
|
||||
consumers). Apps get one dep; the consumer Zip task
|
||||
bundles only the fonts used (native) and
|
||||
jvmProcessResources stages the same fonts (jvm).
|
||||
|
||||
@@ -103,7 +111,22 @@ demo/ → :demo — flagship showcase app (30+ screens) + th
|
||||
MULTIPLATFORM: also has a jvm() target running the SAME shared
|
||||
screens on stock JVM Compose Desktop (`./gradlew :demo:run`,
|
||||
MainJvmKt) — the parity reference; differences vs native = port bugs
|
||||
apidemo/ → :apidemo — Postman-style REST API manager
|
||||
apidemo/ → :apidemo — Postman-style REST API manager. MULTIPLATFORM like :demo
|
||||
(`./gradlew :apidemo:run`): the whole UI lives in commonMain
|
||||
against the official Maven coords; SDL-backed APIs go through
|
||||
expect/actual seams (compat/Compat.kt — native actuals delegate
|
||||
to com.compose.sdl, jvm actuals use AWT + upstream desktop).
|
||||
mTLS / TLS-chain inspection stays native-only (bundled libcurl).
|
||||
gradle-plugin/
|
||||
└── compose-desktop-native-bridge/ → :compose-desktop-native-bridge — the CONSUMER-side bridge as a
|
||||
published Gradle plugin (id com.bitsycore.compose-desktop-native
|
||||
.bridge, applies to Settings or Project): substitution rules
|
||||
can't ship inside Maven artifacts, so third-party apps apply the
|
||||
plugin, declare OFFICIAL CMP coords in commonMain, and native
|
||||
configurations swap in the published com.bitsycore.compose.sdl
|
||||
klibs. Substituted version defaults to the plugin's own
|
||||
(override: composeDesktopNative.version property). Published by
|
||||
the macOS publish job.
|
||||
scripts/ → vendor-sync + python helper scripts (compose-coverage = API
|
||||
coverage/fidelity vs upstream, material-symbols generate/subset)
|
||||
+ compose-fork/;
|
||||
@@ -252,11 +275,15 @@ commonMain
|
||||
attached to: mingwX64Main always; macOS/Linux when -Prenderer=sdl3.
|
||||
```
|
||||
|
||||
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are declared identically
|
||||
in both `skikoRendererMain` and `sdlRendererMain`. `:window` calls them straight
|
||||
from `:ui` — no `expect`/`actual`, no factory layer — and the right impl
|
||||
resolves because **only one of the two renderer source sets is attached to a
|
||||
given target**. Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
|
||||
`createRenderBackend(…)` + `rendererPreferredGpuMode()` are `expect`s in
|
||||
`:ui`'s nativeMain with `actual`s in BOTH `skikoRendererMain` and
|
||||
`sdlRendererMain` — unambiguous because **only one of the two renderer source
|
||||
sets is attached to a given target**. (They used to be plain duplicate
|
||||
declarations with no expect; that compiled per-target but shared nativeMain
|
||||
METADATA couldn't see them on a host whose targets span both renderers, which
|
||||
blocked the WINDOWS host from producing :window's KotlinMultiplatform
|
||||
publication — and Windows must publish the root modules, see the publish
|
||||
workflow.) Under `-Prenderer=sdl3`, the `skikoRenderer*` source sets are
|
||||
**not even created**, so Gradle has nothing to warn about and Skiko is never
|
||||
pulled in.
|
||||
|
||||
@@ -318,8 +345,9 @@ gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||
./gradlew :demo:runDebugExecutableMacosArm64 -Prenderer=sdl3
|
||||
|
||||
# Stock JVM Compose Desktop (any host) — the parity reference: the SAME shared
|
||||
# demo screens on upstream Compose; differences vs the native build = port bugs.
|
||||
# screens on upstream Compose; differences vs the native build = port bugs.
|
||||
./gradlew :demo:run
|
||||
./gradlew :apidemo:run
|
||||
```
|
||||
|
||||
### System dependencies
|
||||
@@ -430,6 +458,46 @@ build tell you what broke.
|
||||
`Dialog` / `DropdownMenu` / `DropdownMenuItem` / `TooltipBox` (m3 doesn't
|
||||
ship drop-in equivalents for our anchor / scrim patterns).
|
||||
|
||||
## Parity harness — native-vs-JVM screenshot diff
|
||||
|
||||
`:demo` renders the **same commonMain screens** on two stacks: native
|
||||
(SDL/Skia, Kotlin/Native) and a `jvm()` target on upstream Compose Desktop.
|
||||
`scripts/parity/parity.py` screenshots every screen on both and pixel-diffs
|
||||
them, so a screen that visually diverges is a **port regression** (missing
|
||||
content, wrong shape/colour, broken clip). Several past renderer regressions
|
||||
would have been caught here.
|
||||
|
||||
```bash
|
||||
python scripts/parity/parity.py # all screens (builds first)
|
||||
python scripts/parity/parity.py Buttons Shapes # a subset
|
||||
python scripts/parity/parity.py --no-build # reuse the last renders
|
||||
```
|
||||
|
||||
Mechanics: the JVM leg renders all screens headlessly via `ImageComposeScene`
|
||||
in ONE process (`:demo:run --args=--screenshot-all=<dir>`, wired in
|
||||
`MainJvm.kt`); the native leg launches the exe once per screen
|
||||
(`--screen=<Name> --screenshot=<x>.bmp`). Output lands in **`build/parity/`
|
||||
(gitignored)**: `<pct>_<Name>_diff.png` (amplified difference heatmap),
|
||||
`<pct>_<Name>_compare.png` (native ∣ jvm ∣ diff, side by side), and
|
||||
`report.txt` ranked worst-first. Windows-only for the native leg today; needs
|
||||
Pillow. See `scripts/parity/README.md`.
|
||||
|
||||
**What the number means — read this before trusting it.** The `%differ` is the
|
||||
fraction of pixels whose per-channel difference exceeds a tolerance. It is NOT
|
||||
a pass/fail score and pixel-perfection is not the goal: the two stacks use
|
||||
different default fonts, so **every screen carries a steady baseline
|
||||
difference** — in the heatmap, text shows as a faint *doubled ghost* from
|
||||
slightly different line metrics/baselines. A healthy full sweep is a smooth
|
||||
gradient (~2% for a sparse screen like Counter, up to ~32% for a text-dense
|
||||
one like Tabs). Known SDL parity gaps also inflate specific screens
|
||||
predictably (Brushes ~23% — gradients render solid on SDL; Shadows / Canvas /
|
||||
GraphicsLayer — effect differences). **The signal is the RANKING and the
|
||||
delta from a screen's own history**, not the absolute value: a text-light
|
||||
screen suddenly reading 60%, or a screen jumping far above its neighbours, is
|
||||
the bug. In a `_diff.png`: ghosted/doubled text + dark shapes = normal font
|
||||
drift; a **solid bright block, or a shape present on only one side** = a real
|
||||
regression — open the `_compare.png` to see which stack is wrong.
|
||||
|
||||
## Conventions
|
||||
|
||||
Kotlin standard style — plain `camelCase` for parameters, local variables,
|
||||
@@ -481,6 +549,18 @@ that should surface in tooling.
|
||||
toggling the renderer property may not invalidate it. Delete
|
||||
`.gradle/configuration-cache/` between switches if you see weird
|
||||
"couldn't find sdl3_ttf" errors.
|
||||
- **Substituted Maven modules hide their transitives from common metadata** —
|
||||
when the bridge swaps an official coord for a project module on native
|
||||
configs, KGP's granular-metadata visibility check drops that Maven module's
|
||||
TRANSITIVES from the commonMain classpath (symptom: `Unresolved reference
|
||||
'Color'` / `Cannot access class ...` in `compileCommonMainKotlinMetadata`,
|
||||
while per-target compilation is fine). Declare EVERY artifact the common
|
||||
code touches DIRECTLY (ui-graphics, ui-text, ui-unit, …) and give each its
|
||||
own bridge rule. Note only the WINDOWS publish job compiles common metadata
|
||||
(it owns the root KotlinMultiplatform publications — the only host that
|
||||
declares every target, so only its .module files carry the full variant
|
||||
table; macOS-published roots left v0.1.15 without mingwX64 variants) —
|
||||
test with `gradlew :<module>:compileCommonMainKotlinMetadata` before tagging.
|
||||
- **`Path()` in commonMain returns different actuals per renderer** —
|
||||
the Skia renderer produces a `SkiaBackedPath` (wraps
|
||||
`org.jetbrains.skia.Path`), the SDL renderer produces a project
|
||||
|
||||
@@ -32,7 +32,9 @@ compose/
|
||||
│ ├── ui-util/ → :ui-util
|
||||
│ ├── ui-geometry/ → :ui-geometry
|
||||
│ ├── ui-unit/ → :ui-unit
|
||||
│ └── ui-backhandler/ → :ui-backhandler
|
||||
│ ├── ui-backhandler/ → :ui-backhandler
|
||||
│ └── ui-tooling-preview/ → :ui-tooling-preview (common @Preview + PreviewParameterProvider —
|
||||
│ IDE-only metadata; previews render via the apps' jvm targets)
|
||||
├── animation/
|
||||
│ ├── animation-core/ → :animation-core
|
||||
│ ├── animation/ → :animation
|
||||
@@ -62,9 +64,13 @@ navigation3/
|
||||
└── navigation3-ui/ → :navigation3-ui (vendored NavDisplay + scene machinery — the one
|
||||
Navigation 3 layer without a K/N desktop artifact)
|
||||
|
||||
gradle-plugin/
|
||||
└── compose-desktop-native-bridge/ → :compose-desktop-native-bridge (the consumer-side bridge, published
|
||||
as a Gradle plugin — see "Using it from your own build")
|
||||
|
||||
demo/ → :demo (showcase + CLI probe suite; multiplatform — the same screens
|
||||
also build for stock JVM Compose Desktop as a parity reference)
|
||||
apidemo/ → :apidemo (Postman-style REST client)
|
||||
apidemo/ → :apidemo (Postman-style REST client; multiplatform — same UI on JVM too)
|
||||
```
|
||||
|
||||
## demo — widget & feature showcase
|
||||
@@ -106,8 +112,13 @@ file-based sessions.
|
||||
./gradlew :apidemo:runDebugExecutableMacosArm64
|
||||
./gradlew :apidemo:runDebugExecutableLinuxX64
|
||||
gradlew.bat :apidemo:runDebugExecutableMingwX64
|
||||
./gradlew :apidemo:run # stock JVM Compose Desktop (parity reference)
|
||||
```
|
||||
|
||||
Like the demo, the whole UI lives in `commonMain` and also runs on upstream
|
||||
Compose Desktop (JVM); only the mTLS / TLS-chain features are native-only —
|
||||
they drive the bundled libcurl directly.
|
||||
|
||||
Add `-Prenderer=sdl3` on macOS/Linux to drop Skiko and use the pure-SDL3
|
||||
renderer everywhere.
|
||||
|
||||
@@ -135,6 +146,33 @@ implementation(project(":material3")) // Material 3 widgets
|
||||
implementation(project(":material-symbols")) // icon-font composables (optional)
|
||||
```
|
||||
|
||||
## Using it from your own build — the bridge plugin
|
||||
|
||||
The klibs publish to GitHub Packages under `com.bitsycore.compose.sdl:*`, and
|
||||
a companion Gradle plugin ships the "redirect trick" this repo uses
|
||||
internally: apply **`com.bitsycore.compose-desktop-native.bridge`** once in
|
||||
`settings.gradle.kts` and declare the **official Compose Multiplatform
|
||||
coordinates** in `commonMain` — the plugin substitutes the port's klibs on
|
||||
every native desktop configuration (mingwX64 / linuxX64 / linuxArm64 /
|
||||
macosArm64), while android / jvm / iOS / wasm keep resolving the official
|
||||
artifacts. One dependency list, all CMP platforms + the port's new ones.
|
||||
|
||||
```kotlin
|
||||
// settings.gradle.kts
|
||||
plugins { id("com.bitsycore.compose-desktop-native.bridge") version "<release>" }
|
||||
|
||||
// build.gradle.kts — official coords, everywhere
|
||||
commonMain.dependencies {
|
||||
implementation("org.jetbrains.compose.runtime:runtime:<runtime-version>")
|
||||
implementation("org.jetbrains.compose.ui:ui:<cmp-version>")
|
||||
implementation("org.jetbrains.compose.foundation:foundation:<cmp-version>")
|
||||
implementation("org.jetbrains.compose.material3:material3:<cmp-m3-version>")
|
||||
}
|
||||
```
|
||||
|
||||
Setup details (repositories, credentials, version pinning):
|
||||
[gradle-plugin/compose-desktop-native-bridge/README.md](gradle-plugin/compose-desktop-native-bridge/README.md).
|
||||
|
||||
## Building
|
||||
|
||||
All four native dependencies — **SDL3**, **SDL3_ttf** (the in-house
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# Renderer roadmap — fixes, performance, memory
|
||||
|
||||
Grounded in the bubble-wrap findings (2026-07): lazy clips took the stress
|
||||
sheet from 23 → 39 FPS, but 75 Hz needs structural work; issue #2 showed that
|
||||
Cleaner-managed native memory starves when the Kotlin heap is quiet.
|
||||
|
||||
## 1. Deterministic native-resource lifecycle
|
||||
|
||||
Ownership closes resources the moment they're finished; GC stays as the
|
||||
safeguard, not the mechanism.
|
||||
|
||||
- [x] **`NativeReleaseQueue`** — SDL calls are main-thread-only, so nothing may
|
||||
destroy directly from a Cleaner or worker: release actions ENQUEUE, the main
|
||||
loop DRAINS between frames. Cleaners become safe from any thread because
|
||||
they only enqueue. Verified end-to-end (direct enqueue + a real
|
||||
Cleaner→enqueue→drain cycle).
|
||||
- [x] **`SdlImageBitmap` textures** — previously never destroyed (manual SDL
|
||||
memory, invisible to the GC nudge). A holder + Cleaner now enqueues
|
||||
`SDL_DestroyTexture` / `SDL_DestroySurface` for the texture, render target
|
||||
and never-realized decoded surface; explicit `close()` frees promptly.
|
||||
- [ ] **Wire `close()` into cache eviction** — the Cleaner is the current
|
||||
release path for decoded/vector bitmaps (correct but GC-timed). Call
|
||||
`SdlImageBitmap.close()` on image-cache eviction / `removeMemoryResource`
|
||||
for prompt release (needs a cross-module hook — the caches hold the
|
||||
`ImageBitmap` interface, not the concrete type).
|
||||
- [ ] **Cache-eviction closes (rest)** — every image/text cache closes what it
|
||||
evicts (Sdl3ImageCache already does; sweep the rest, both renderers).
|
||||
- [ ] **Renderer `destroy()` chain** — window close synchronously frees every
|
||||
pool (clip targets already do; text caches, image caches, shadow cache,
|
||||
typefaces to follow).
|
||||
- [ ] **`SkiaImageBitmap.close()`** (surface + snapshot) + close-on-eviction in
|
||||
the Skia caches. *Skia source sets build only on macOS/Linux — needs a mac
|
||||
or CI leg to verify.*
|
||||
- [ ] **Demote the GC nudge** — once ownership does the work, stretch the 10s
|
||||
interval / gate it on churn; it remains the backstop.
|
||||
|
||||
Verification: `demo --leaktest`-style probe (cycle screens N times, print
|
||||
RSS; expect a plateau without GC sawtooth).
|
||||
|
||||
## 2. SDL renderer performance (the 39 → 75 Hz path)
|
||||
|
||||
- [x] **Frame profiler** — `CDN_PROFILE=1` env flag prints per-phase timings
|
||||
(events / app pump / window pump / render / present) every ~2 s. Measure
|
||||
first, optimize second.
|
||||
- [ ] **Right-size clip scratch targets** — the pool allocates window-sized
|
||||
textures per mask depth; allocate at the clip bbox (size-bucketed pool) to
|
||||
cut fill-rate for the masks that remain after lazy clips.
|
||||
- [ ] **Dirty-region rendering** — the big structural item: accumulate damage
|
||||
from invalidated layers, scissor redraw to it. One bubble's spring should
|
||||
not re-tessellate 84. Most likely single item to reach 75 Hz.
|
||||
- [ ] **Glyph atlas** — text draws currently break geometry batches (z-order
|
||||
flush per run); an atlas texture lets glyphs ride the vertex batches.
|
||||
- [ ] **Retained layer textures** (later) — RenderNode-style caching of static
|
||||
subtrees keyed by draw content.
|
||||
|
||||
## 3. Skia renderer
|
||||
|
||||
- [ ] Bounded `SkiaImageCache` (LRU + eviction close) — unbounded HashMap today.
|
||||
- [ ] Eager-close audit of the text pipeline (mirror of the SDL sweep).
|
||||
- [ ] `saveLayer` huge-bounds clamp (GPU offscreen memory spikes — see
|
||||
CLAUDE.md pitfall).
|
||||
|
||||
## 4. Correctness / parity gaps (future bug reports waiting)
|
||||
|
||||
Ordered by likelihood of a user hitting them:
|
||||
|
||||
- [ ] **Gradient brushes on SDL** — `Brush.linearGradient/radialGradient`
|
||||
render solid; the per-vertex sampler infrastructure is half-present.
|
||||
- [ ] **Layer rotation** — rotationZ repositions but content doesn't rotate
|
||||
(hit-testing too).
|
||||
- [ ] **Real `saveLayer` alpha on SDL** — overlapping content composites at
|
||||
paint level; needs an offscreen (the clip-target pool can serve it).
|
||||
- [ ] **`clipPath` generic shapes** — bbox fallback clips square.
|
||||
|
||||
## 5. Permanent tooling
|
||||
|
||||
- [x] **Parity harness** — scripts/parity/: renders every :demo screen native +
|
||||
JVM from the same commonMain composables, pixel-diffs, ranks by %differ,
|
||||
emits per-screen diff heatmaps + side-by-side compares (pct in filename).
|
||||
build/parity/ (gitignored). See scripts/parity/README.md.
|
||||
- [ ] Promote the press/hover automation rig (window-handle-relative input +
|
||||
screenshot) into `scripts/`.
|
||||
|
||||
## 6. Long-term
|
||||
|
||||
- [ ] **SDL_GPU render backend** — real stencil clipping (no masks at all),
|
||||
pipelined batching, shader gradients. Weeks of work across three
|
||||
platforms; items 2–4 stay useful beneath it.
|
||||
|
||||
Suggested order: profiler → lifecycle queue + leak closes → clip-target
|
||||
right-sizing → gradients → dirty regions → the rest by demand.
|
||||
+101
-13
@@ -16,12 +16,38 @@ plugins {
|
||||
// Skip mingwX64 on non-Windows hosts; see root build.gradle.kts.
|
||||
val vHostSupportsMingw: Boolean by rootProject.extra
|
||||
|
||||
// JVM parity target's Compose version — the dev build matching the pinned
|
||||
// COMPOSE_CORE_REF (see scripts/compose-fork/compose.properties); mirrors
|
||||
// :demo's forcing (Gradle orders "+dev" BELOW the plain version, so the
|
||||
// plugin's beta01 would win conflict resolution without it).
|
||||
val vComposeJvmVersion = "1.12.0-beta01+dev4324"
|
||||
val vComposeM3JvmVersion = "1.12.0-alpha03+dev4324"
|
||||
val vComposeJvmForced = mapOf(
|
||||
"org.jetbrains.compose.runtime" to vComposeJvmVersion,
|
||||
"org.jetbrains.compose.ui" to vComposeJvmVersion,
|
||||
"org.jetbrains.compose.foundation" to vComposeJvmVersion,
|
||||
"org.jetbrains.compose.animation" to vComposeJvmVersion,
|
||||
"org.jetbrains.compose.material" to vComposeJvmVersion,
|
||||
"org.jetbrains.compose.material3" to vComposeM3JvmVersion,
|
||||
)
|
||||
configurations.configureEach {
|
||||
if (name.startsWith("jvm")) {
|
||||
resolutionStrategy.eachDependency {
|
||||
vComposeJvmForced[requested.group]?.let { useVersion(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
linuxArm64()
|
||||
linuxX64()
|
||||
macosArm64()
|
||||
if (vHostSupportsMingw) mingwX64()
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
targets.withType<KotlinNativeTarget>().all {
|
||||
val isMingw = name == "mingwX64"
|
||||
val isLinux = name.startsWith("linux")
|
||||
@@ -68,9 +94,31 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(project(":window"))
|
||||
implementation(project(":material3"))
|
||||
// Common API on both stacks — usable from shared code.
|
||||
implementation(project(":material-symbols"))
|
||||
|
||||
// Official Maven coords for everything the shared UI touches,
|
||||
// so commonMain metadata (and the IDE's common analysis)
|
||||
// resolve. Metadata + jvm resolve them from Maven; the NATIVE
|
||||
// configurations substitute each for its port module (root
|
||||
// build's FULL-COMMONIZATION BRIDGE). Every artifact must be
|
||||
// declared DIRECTLY: transitives of a substituted module are
|
||||
// invisible to the granular-metadata visibility check. The
|
||||
// runtime is the official klib everywhere, never substituted
|
||||
// (jvm configs force the +dev build above).
|
||||
implementation("org.jetbrains.compose.runtime:runtime:1.11.1")
|
||||
implementation("org.jetbrains.compose.ui:ui:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-graphics:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-text:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-unit:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-geometry:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-util:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.foundation:foundation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.foundation:foundation-layout:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.animation:animation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.animation:animation-core:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
@@ -80,17 +128,32 @@ kotlin {
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
// Single HTTP engine on every desktop target: Ktor's Curl engine. It
|
||||
// bundles a static libcurl per target (Schannel on Windows, OpenSSL on
|
||||
// macOS/Linux) embedded in its cinterop klib — no runtime DLL, one copy.
|
||||
// We use Curl everywhere (not WinHttp/Darwin) so that an upcoming
|
||||
// client-certificate (mTLS) path — which calls the same bundled libcurl
|
||||
// directly, since Ktor's engines expose no client-cert API — shares the
|
||||
// exact same TLS stack as ordinary requests.
|
||||
if (vHostSupportsMingw) getByName("mingwX64Main").dependencies { implementation(libs.ktor.client.curl) }
|
||||
getByName("macosArm64Main").dependencies { implementation(libs.ktor.client.curl) }
|
||||
getByName("linuxX64Main").dependencies { implementation(libs.ktor.client.curl) }
|
||||
getByName("linuxArm64Main").dependencies { implementation(libs.ktor.client.curl) }
|
||||
nativeMain {
|
||||
dependencies {
|
||||
// The SDL window shell + main loop (native-only).
|
||||
implementation(project(":window"))
|
||||
// Single HTTP engine on every native target: Ktor's Curl
|
||||
// engine. It bundles a static libcurl per target (Schannel on
|
||||
// Windows, OpenSSL on macOS/Linux) embedded in its cinterop
|
||||
// klib — no runtime DLL, one copy. Curl everywhere (not
|
||||
// WinHttp/Darwin) so the client-certificate (mTLS) path —
|
||||
// which calls the same bundled libcurl directly, since Ktor's
|
||||
// engines expose no client-cert API — shares the exact same
|
||||
// TLS stack as ordinary requests.
|
||||
implementation(libs.ktor.client.curl)
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
// Upstream Compose Desktop — the parity stack.
|
||||
implementation(compose.desktop.currentOs)
|
||||
// Coroutine-based engine on JVM; native targets keep Curl.
|
||||
implementation(libs.ktor.client.cio)
|
||||
// Dispatchers.Main provider on desktop JVM (must match
|
||||
// kotlinx-coroutines-core's version — the catalog pins both).
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
@@ -99,6 +162,12 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "apidemo.MainJvmKt"
|
||||
}
|
||||
}
|
||||
|
||||
// SDL3 / SDL3_ttf / SDL3_image / FreeType are linked statically into the
|
||||
// executable (see the mingw linkerOpts above), so there are no runtime DLLs to
|
||||
// bundle — the distributable is just <app>.exe + data.kres.
|
||||
@@ -277,3 +346,22 @@ for (variant in variants) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: Stage fonts onto the JVM classpath
|
||||
// ==================
|
||||
// The JVM analog of the data.kres font/ entries: :material-symbols' JVM actual
|
||||
// loads each style's font from the classpath at font/<Style>.ttf, and the
|
||||
// jvm Fonts actual loads the mono body font at font/NotoSansMono.ttf.
|
||||
tasks.named<ProcessResources>("jvmProcessResources") {
|
||||
from(notoMonoFile) { into("font") }
|
||||
dependsOn(":ui:downloadNotoFonts")
|
||||
val vSymbolsProject = rootProject.project(":material-symbols")
|
||||
for (vStyle in vUsedStyles) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val vFontFile = vSymbolsProject.extra["iconFontFile$vStyle"] as org.gradle.api.provider.Provider<org.gradle.api.file.RegularFile>
|
||||
val vDownloadTask = vSymbolsProject.extra["iconFontDownloadTask$vStyle"] as TaskProvider<*>
|
||||
from(vFontFile) { into("font") }
|
||||
dependsOn(vDownloadTask)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
|
||||
// ==================
|
||||
// MARK: Monospace body font (seam)
|
||||
// ==================
|
||||
// Family name the body editor passes for monospace text. Resolves to the
|
||||
// bundled Noto Sans Mono once registered; null means the font isn't shipped
|
||||
// (data.kres on native, classpath font/ on jvm), in which case the body falls
|
||||
// back to the default proportional font.
|
||||
const val kMonoFamily = "noto-mono"
|
||||
|
||||
/* The raw family name string — used by wrappedRowCount, which measures via the
|
||||
platform text pipeline (accepts a name string). Null when not bundled. */
|
||||
expect val monoFontFamilyName: String?
|
||||
|
||||
/* The material3 Text / BasicTextField-shaped FontFamily for the mono family.
|
||||
Null when not bundled. */
|
||||
expect val monoFontFamily: FontFamily?
|
||||
@@ -1,7 +1,6 @@
|
||||
package apidemo
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.curl.Curl
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
@@ -23,7 +22,7 @@ import kotlin.time.TimeSource
|
||||
run() is a suspend fun — call it off the UI dispatcher. */
|
||||
class HttpRunner {
|
||||
|
||||
private val fClient = HttpClient(Curl)
|
||||
private val fClient = createApiHttpClient()
|
||||
|
||||
init {
|
||||
// Clear any temporary client certs a prior crash left in the Windows
|
||||
@@ -140,7 +139,7 @@ private fun isGzip(inEncoding: String?, inBytes: ByteArray): Boolean =
|
||||
|
||||
/* Read a file's raw bytes (used for a FILE request body). */
|
||||
internal fun readFileBytes(inPath: String): ByteArray =
|
||||
FileSystem.SYSTEM.read(inPath.trim().toPath()) { readByteArray() }
|
||||
systemFileSystem.read(inPath.trim().toPath()) { readByteArray() }
|
||||
|
||||
/* Inflate a gzip stream to its original bytes via okio's GzipSource. */
|
||||
private fun gunzip(inBytes: ByteArray): ByteArray {
|
||||
@@ -19,7 +19,7 @@ private val fJson = Json {
|
||||
/* Write the pack as pretty JSON to inPath. Returns null on success, else the
|
||||
error message. */
|
||||
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) {
|
||||
systemFileSystem.write(inPath.trim().toPath()) {
|
||||
writeUtf8(fJson.encodeToString(inPack))
|
||||
}
|
||||
null
|
||||
@@ -29,7 +29,7 @@ fun exportPack(inPack: Pack, inPath: String): String? = try {
|
||||
|
||||
/* Read a pack back from inPath. */
|
||||
fun importPack(inPath: String): Result<Pack> = try {
|
||||
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() }
|
||||
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||
Result.success(fJson.decodeFromString<Pack>(vText))
|
||||
} catch (e: Throwable) {
|
||||
Result.failure(e)
|
||||
@@ -37,7 +37,7 @@ fun importPack(inPath: String): Result<Pack> = try {
|
||||
|
||||
/* Write a whole self-contained session to inPath. Returns null on success. */
|
||||
fun exportSession(inSession: Session, inPath: String): String? = try {
|
||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) {
|
||||
systemFileSystem.write(inPath.trim().toPath()) {
|
||||
writeUtf8(fJson.encodeToString(inSession))
|
||||
}
|
||||
null
|
||||
@@ -47,7 +47,7 @@ fun exportSession(inSession: Session, inPath: String): String? = try {
|
||||
|
||||
/* Read a session back from inPath. */
|
||||
fun importSession(inPath: String): Result<Session> = try {
|
||||
val vText = FileSystem.SYSTEM.read(inPath.trim().toPath()) { readUtf8() }
|
||||
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||
Result.success(fJson.decodeFromString<Session>(vText))
|
||||
} catch (e: Throwable) {
|
||||
Result.failure(e)
|
||||
@@ -59,7 +59,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
|
||||
val vSrc = inPath.trim().toPath()
|
||||
val vLeaf = inNewName.trim().let { if (it.contains('.')) it else "$it.json" }
|
||||
val vDst = vSrc.parent?.div(vLeaf) ?: vLeaf.toPath()
|
||||
FileSystem.SYSTEM.atomicMove(vSrc, vDst)
|
||||
systemFileSystem.atomicMove(vSrc, vDst)
|
||||
Result.success(vDst.toString())
|
||||
} catch (e: Throwable) {
|
||||
Result.failure(e)
|
||||
@@ -68,7 +68,7 @@ fun renameFile(inPath: String, inNewName: String): Result<String> = try {
|
||||
/* Write arbitrary text (a response body / headers dump) to inPath. Returns null
|
||||
on success, else the error message. */
|
||||
fun writeTextFile(inPath: String, inText: String): String? = try {
|
||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) { writeUtf8(inText) }
|
||||
systemFileSystem.write(inPath.trim().toPath()) { writeUtf8(inText) }
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
e.message ?: e.toString()
|
||||
@@ -76,7 +76,7 @@ fun writeTextFile(inPath: String, inText: String): String? = try {
|
||||
|
||||
/* Write raw bytes (a binary response body, e.g. an image) to inPath. */
|
||||
fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
|
||||
FileSystem.SYSTEM.write(inPath.trim().toPath()) { write(inBytes) }
|
||||
systemFileSystem.write(inPath.trim().toPath()) { write(inBytes) }
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
e.message ?: e.toString()
|
||||
@@ -1,7 +1,5 @@
|
||||
package apidemo
|
||||
|
||||
import com.compose.sdl.appDataDir
|
||||
import com.compose.sdl.revealInFileManager
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okio.FileSystem
|
||||
@@ -86,7 +84,7 @@ private fun stateFilePath(): String? {
|
||||
fun loadAppState(): AppState {
|
||||
val vPath = stateFilePath() ?: return AppState()
|
||||
return try {
|
||||
val vText = FileSystem.SYSTEM.read(vPath.toPath()) { readUtf8() }
|
||||
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
||||
fStateJson.decodeFromString<AppState>(vText)
|
||||
} catch (e: Throwable) {
|
||||
AppState()
|
||||
@@ -98,7 +96,7 @@ fun loadAppState(): AppState {
|
||||
fun saveAppState(inState: AppState) {
|
||||
val vPath = stateFilePath() ?: return
|
||||
try {
|
||||
FileSystem.SYSTEM.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
||||
systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
||||
} catch (e: Throwable) {
|
||||
// ignore — nothing the user can act on, and packs can still be exported
|
||||
}
|
||||
+2
-4
@@ -17,10 +17,8 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.compose.sdl.TextLayoutConfig
|
||||
import com.compose.sdl.icons.MaterialSymbols
|
||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||
import com.compose.sdl.showOpenFileDialog
|
||||
|
||||
// ==================
|
||||
// MARK: Panel 3 — request builder (Query / Headers / Body)
|
||||
@@ -219,7 +217,7 @@ internal fun TabSizeSelector() {
|
||||
var vOpen by remember { mutableStateOf(false) }
|
||||
val vHoverSrc = remember { MutableInteractionSource() }
|
||||
val vHover by vHoverSrc.collectIsHoveredAsState()
|
||||
val vSize = TextLayoutConfig.tabWidth
|
||||
val vSize = editorTabWidth
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = { PlainTooltip { Text("Editor tab size") } },
|
||||
@@ -243,7 +241,7 @@ internal fun TabSizeSelector() {
|
||||
for (vN in listOf(2, 4, 8)) {
|
||||
DropdownMenuItem(text = {
|
||||
Text("$vN spaces", color = if (vN == vSize) c.accent else c.text)
|
||||
}, onClick = { TextLayoutConfig.tabWidth = vN; vOpen = false })
|
||||
}, onClick = { editorTabWidth = vN; vOpen = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -25,7 +25,6 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.compose.sdl.icons.MaterialSymbols
|
||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||
import com.compose.sdl.layout.x
|
||||
|
||||
// ==================
|
||||
// MARK: Request tab strip (open requests; drag a tab to reorder)
|
||||
@@ -31,11 +31,8 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.compose.sdl.fileManagerName
|
||||
import com.compose.sdl.icons.MaterialSymbols
|
||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||
import com.compose.sdl.layout.y
|
||||
import com.compose.sdl.revealInFileManager
|
||||
|
||||
// ==================
|
||||
// MARK: Pack colour (palette dot + picker)
|
||||
+1
-3
@@ -23,8 +23,6 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.Popup
|
||||
import com.compose.sdl.icons.MaterialSymbols
|
||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||
import com.compose.sdl.layout.x
|
||||
import com.compose.sdl.layout.y
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -141,7 +139,7 @@ internal fun CopyButton(inText: String, inLabel: String? = null) {
|
||||
.background(if (vHover) c.accent.copy(alpha = 0.18f) else Color.Transparent, RoundedCornerShape(7.dp))
|
||||
.hoverable(vHoverSrc)
|
||||
.clickable {
|
||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(inText)) }
|
||||
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(inText)) }
|
||||
vCopied = true
|
||||
}
|
||||
.padding(horizontal = if (inLabel != null) 10.dp else 5.dp, vertical = 5.dp),
|
||||
@@ -0,0 +1,29 @@
|
||||
package apidemo
|
||||
|
||||
// ==================
|
||||
// MARK: TLS chain / client-certificate seam
|
||||
// ==================
|
||||
// The real implementations drive the bundled libcurl directly (CurlMtls.kt,
|
||||
// nativeMain) — Ktor's engines expose no client-cert / CERTINFO API. The jvm
|
||||
// parity app has no bundled libcurl, so its actuals report the feature as
|
||||
// native-only instead.
|
||||
|
||||
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
||||
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
||||
presented it. Derived certs — an issuer resolved from the OS store, or a
|
||||
name-only placeholder — have fromServer = false and are drawn dotted. */
|
||||
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
|
||||
|
||||
/* The server's TLS certificate chain. error is set instead when it couldn't be
|
||||
fetched; fields vary by TLS backend. */
|
||||
class TlsChain(val certs: List<ChainCert>, val error: String?)
|
||||
|
||||
/* Fetch the TLS certificate chain the request's server presents. */
|
||||
expect fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
||||
|
||||
/* Send the request with its client certificate attached (mTLS). */
|
||||
expect fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
||||
|
||||
/* Remove temp client certs a previous crashed run left behind. Call once at
|
||||
startup; no-op where the OS store isn't used. */
|
||||
expect fun sweepTempClientCerts()
|
||||
+10
-18
@@ -39,14 +39,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.compose.sdl.icons.MaterialSymbols
|
||||
import com.compose.sdl.icons.material.symbols.MaterialSymbolsOutlined
|
||||
import com.compose.sdl.removeMemoryResource
|
||||
import com.compose.sdl.res.ResourceKind
|
||||
import com.compose.sdl.res.painterResource
|
||||
import com.compose.sdl.scrollbar.ScrollbarStyle
|
||||
import com.compose.sdl.scrollbar.VerticalScrollbar
|
||||
import com.compose.sdl.scrollbar.rememberScrollbarAdapter
|
||||
import com.compose.sdl.showSaveFileDialog
|
||||
import com.compose.sdl.text.currentTextMeasurer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// ==================
|
||||
@@ -169,8 +161,8 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
||||
inBody = if (vRespImage) null else vBody,
|
||||
inIsImage = vRespImage && !vLoading,
|
||||
inImagePainter = if (vRespImage && !vLoading) {
|
||||
val vKind = if (vResp?.contentType?.contains("svg", ignoreCase = true) == true) ResourceKind.Svg else ResourceKind.Raster
|
||||
painterResource(inRs.imageKey!!, vKind)
|
||||
val vSvg = vResp?.contentType?.contains("svg", ignoreCase = true) == true
|
||||
memoryImagePainter(inRs.imageKey!!, vSvg)
|
||||
} else null,
|
||||
inHeadersCollapsed = vHeadersCollapsed,
|
||||
inOnToggleCollapse = { vHeadersCollapsed = !vHeadersCollapsed },
|
||||
@@ -247,7 +239,7 @@ internal fun ViewerPanel(inRs: ReqState, inResolved: ApiRequest) {
|
||||
inResponseBody = vRespBody,
|
||||
inOnMessage = { vMsg = it },
|
||||
inOnClear = {
|
||||
inRs.imageKey?.let { removeMemoryResource(it) }
|
||||
inRs.imageKey?.let { removeMemoryImage(it) }
|
||||
inRs.imageKey = null
|
||||
inRs.response = null
|
||||
inRs.sentReq = null
|
||||
@@ -508,7 +500,7 @@ internal fun HttpFlowView(
|
||||
val vS = vSel
|
||||
if (vS != null && !vS.isEmpty) {
|
||||
val vText = vBody.substring(vS.start, vS.end)
|
||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vText)) }
|
||||
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vText)) }
|
||||
true
|
||||
} else false
|
||||
}
|
||||
@@ -974,18 +966,18 @@ internal fun BodyView(
|
||||
// also use it for the first frame, before the body reports its width).
|
||||
val vNumColor = c.dim.copy(alpha = 0.45f)
|
||||
val vNumbers = remember(inText, vBodyWidthPx, inSoftWrap, vFontPx) {
|
||||
val vM = if (inSoftWrap && vBodyWidthPx > 0) currentTextMeasurer else null
|
||||
val vWrapRows = inSoftWrap && vBodyWidthPx > 0
|
||||
buildString {
|
||||
for ((vIdx, vSrc) in vLines.withIndex()) {
|
||||
if (vIdx > 0) append('\n')
|
||||
append(vIdx + 1)
|
||||
if (vM != null) {
|
||||
if (vWrapRows) {
|
||||
// Pass the density-scaled font size so this wrap matches the body's
|
||||
// actual glyph pixel widths — passing 12 (sp) here counted each
|
||||
// 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
|
||||
// here and the gutter numbers drifted upward.
|
||||
val vRows = vM.wrap(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).lines.size.coerceAtLeast(1)
|
||||
val vRows = wrappedRowCount(vSrc, vFontPx, vBodyWidthPx, monoFontFamilyName).coerceAtLeast(1)
|
||||
repeat(vRows - 1) { append('\n') }
|
||||
}
|
||||
}
|
||||
@@ -1309,18 +1301,18 @@ internal fun ViewerOverflowMenu(
|
||||
}
|
||||
DropdownMenu(expanded = vOpen, onDismissRequest = { vOpen = false }) {
|
||||
DropdownMenuItem(text = { Text("Copy all") }, onClick = {
|
||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(copyAll())) }
|
||||
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(copyAll())) }
|
||||
inOnMessage("Copied all.")
|
||||
vOpen = false
|
||||
})
|
||||
DropdownMenuItem(text = { Text("Copy headers") }, onClick = {
|
||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vHeadersText)) }
|
||||
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vHeadersText)) }
|
||||
inOnMessage("Copied headers.")
|
||||
vOpen = false
|
||||
})
|
||||
if (vCanCopyBody) {
|
||||
DropdownMenuItem(text = { Text("Copy body") }, onClick = {
|
||||
vScope.launch { vClipboard.setClipEntry(ClipEntry.withPlainText(vBodyText)) }
|
||||
vScope.launch { vClipboard.setClipEntry(clipEntryOfText(vBodyText)) }
|
||||
inOnMessage("Copied body.")
|
||||
vOpen = false
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import io.ktor.client.HttpClient
|
||||
import okio.FileSystem
|
||||
|
||||
// ==================
|
||||
// MARK: Platform seams (native = com.compose.sdl / SDL3, jvm = AWT + upstream desktop)
|
||||
// ==================
|
||||
// The port modules are native-only, so shared code can't call com.compose.sdl
|
||||
// directly — every SDL-backed API the app uses goes through these expects.
|
||||
// Signatures mirror the native originals so the native actuals are pure
|
||||
// delegation (same pattern as :demo's demo.shim package).
|
||||
|
||||
/* Per-user app-data dir (created on demand, ends with a separator) or null. */
|
||||
expect fun appDataDir(inOrg: String, inApp: String): String?
|
||||
|
||||
/* Open the OS file manager showing the given path. Fire-and-forget. */
|
||||
expect fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)? = null)
|
||||
|
||||
/* "Explorer" / "Finder" / "Files" — for menu labels. */
|
||||
expect fun fileManagerName(): String
|
||||
|
||||
/* Async save-file dialog; the callback gets the chosen absolute path or null. */
|
||||
expect fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> Unit)
|
||||
|
||||
/* Async open-file dialog; the callback gets the chosen absolute path or null. */
|
||||
expect fun showOpenFileDialog(inOnResult: (String?) -> Unit)
|
||||
|
||||
/* A plain-text clipboard entry (ClipEntry construction differs per stack). */
|
||||
expect fun clipEntryOfText(inText: String): ClipEntry
|
||||
|
||||
// ============
|
||||
// In-memory response images
|
||||
|
||||
/* Register/replace decoded-image source bytes under a key (resp-image://N). */
|
||||
expect fun registerMemoryImage(inKey: String, inBytes: ByteArray)
|
||||
|
||||
/* Drop a registered image's bytes. */
|
||||
expect fun removeMemoryImage(inKey: String)
|
||||
|
||||
/* Painter for a registered image key (raster or SVG). */
|
||||
@Composable
|
||||
expect fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter
|
||||
|
||||
// ============
|
||||
// Text layout
|
||||
|
||||
/* Rows the given line occupies when soft-wrapped to inMaxWidthPx at
|
||||
inFontPx (already density-scaled). Used by the body gutter so its line
|
||||
numbers stay aligned with the wrapped body text. */
|
||||
expect fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int
|
||||
|
||||
/* Editor tab width in spaces — how wide a typed '\t' renders. On the native
|
||||
stack this drives the project text pipeline (TextLayoutConfig); the jvm
|
||||
parity app only stores the preference. */
|
||||
expect var editorTabWidth: Int
|
||||
|
||||
// ============
|
||||
// Infrastructure
|
||||
|
||||
/* okio's FileSystem.SYSTEM is declared per-platform, not in its common
|
||||
metadata — surface it through a seam. */
|
||||
internal expect val systemFileSystem: FileSystem
|
||||
|
||||
/* The app's Ktor client: Curl on native (bundled libcurl — same TLS stack as
|
||||
the mTLS path), CIO on the jvm parity target. */
|
||||
expect fun createApiHttpClient(): HttpClient
|
||||
|
||||
/* Window-shell integration: an intercepting close handler (return true to
|
||||
allow the close) and an app-wide key-shortcut handler; both cleared when
|
||||
the composition leaves. */
|
||||
@Composable
|
||||
expect fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean)
|
||||
@@ -0,0 +1,16 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.positionInWindow
|
||||
|
||||
// COPY of the port's com.compose.sdl.layout LayoutCoordinates sugar so the
|
||||
// same helpers resolve on the jvm parity target (the port modules are
|
||||
// native-only). Guards `isAttached`: a menu/tooltip anchor can be read while
|
||||
// its node is mid-recycle, and positionInWindow() throws on a detached
|
||||
// coordinator.
|
||||
|
||||
/* Logical-point X of this layout in the window. */
|
||||
val LayoutCoordinates.x: Int get() = if (isAttached) positionInWindow().x.toInt() else 0
|
||||
|
||||
/* Logical-point Y of this layout in the window. */
|
||||
val LayoutCoordinates.y: Int get() = if (isAttached) positionInWindow().y.toInt() else 0
|
||||
@@ -0,0 +1,333 @@
|
||||
// COPY of the port's Scrollbar (package com.compose.sdl.scrollbar) so the SAME
|
||||
// implementation runs on the jvm parity target — the port modules are
|
||||
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||
// Keep in sync with the original when it changes.
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// ==================
|
||||
// MARK: Scrollbar (Compose Desktop-style)
|
||||
// ==================
|
||||
|
||||
/* A faithful subset of Compose Desktop's scrollbar (androidx.compose.foundation).
|
||||
You overlay a VerticalScrollbar / HorizontalScrollbar on a scrollable and feed
|
||||
it a ScrollbarAdapter built from the same ScrollState. This project's Box has
|
||||
no BoxScope (so no Modifier.align) — pin the bar with the Box's
|
||||
contentAlignment; the fillMaxSize content fills the rest:
|
||||
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) {
|
||||
val state = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(state)) { ... }
|
||||
VerticalScrollbar(
|
||||
adapter = rememberScrollbarAdapter(state),
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
|
||||
Always visible (no auto-hide); the thumb tweens unhoverColor -> hoverColor on
|
||||
hover over hoverDurationMillis, matching upstream. Drag the thumb to scroll;
|
||||
click the track to page. */
|
||||
|
||||
@Immutable
|
||||
class ScrollbarStyle(
|
||||
val minimalHeight: Dp,
|
||||
val thickness: Dp,
|
||||
val shape: Shape,
|
||||
val hoverDurationMillis: Int,
|
||||
val unhoverColor: Color,
|
||||
val hoverColor: Color,
|
||||
)
|
||||
|
||||
/* Upstream default: an 8dp black thumb that's faint until hovered. Dark UIs
|
||||
provide a white-tinted style via LocalScrollbarStyle or the `style` param. */
|
||||
fun defaultScrollbarStyle(): ScrollbarStyle = ScrollbarStyle(
|
||||
minimalHeight = 16.dp,
|
||||
thickness = 8.dp,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
hoverDurationMillis = 300,
|
||||
unhoverColor = Color.Black.copy(alpha = 0.12f),
|
||||
hoverColor = Color.Black.copy(alpha = 0.50f),
|
||||
)
|
||||
|
||||
val LocalScrollbarStyle = compositionLocalOf { defaultScrollbarStyle() }
|
||||
|
||||
// ==================
|
||||
// MARK: ScrollbarAdapter
|
||||
// ==================
|
||||
|
||||
/* Bridges a scrollable's state to the scrollbar in pixels. scrollOffset /
|
||||
contentSize / viewportSize are along the scroll axis; the thumb is sized by
|
||||
viewportSize / contentSize and positioned by scrollOffset / (content - viewport). */
|
||||
interface ScrollbarAdapter {
|
||||
val scrollOffset: Double
|
||||
val contentSize: Double
|
||||
val viewportSize: Double
|
||||
suspend fun scrollTo(scrollOffset: Double)
|
||||
}
|
||||
|
||||
// (The port also ships a ScrollState-based adapter; it reads foundation
|
||||
// internals (ScrollState.contentSize/viewportSize) that a copy outside
|
||||
// :foundation cannot access. apidemo only scrollbars a LazyColumn.)
|
||||
|
||||
/* Adapter for a virtualized LazyColumn / LazyRow. A lazy list only knows the
|
||||
size of its VISIBLE items, so exact pixel geometry of off-screen content is
|
||||
unknowable — we estimate it from the average visible item size × total item
|
||||
count (the same approach as Compose Desktop). Accurate enough for a thumb
|
||||
whose length/position track the scroll position; the estimate self-corrects
|
||||
as items of different sizes scroll through the viewport. */
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(scrollState: LazyListState): ScrollbarAdapter =
|
||||
remember(scrollState) { LazyListScrollbarAdapter(scrollState) }
|
||||
|
||||
private class LazyListScrollbarAdapter(private val state: LazyListState) : ScrollbarAdapter {
|
||||
// Mean main-axis size of the currently-visible items, in px (>= 1 to avoid /0).
|
||||
private fun averageItemSize(): Double {
|
||||
val vVisible = state.layoutInfo.visibleItemsInfo
|
||||
if (vVisible.isEmpty()) return 0.0
|
||||
val vSpan = (vVisible.last().offset + vVisible.last().size) - vVisible.first().offset
|
||||
return (vSpan.toDouble() / vVisible.size).coerceAtLeast(1.0)
|
||||
}
|
||||
|
||||
override val scrollOffset: Double
|
||||
get() = averageItemSize() * state.firstVisibleItemIndex + state.firstVisibleItemScrollOffset
|
||||
|
||||
override val contentSize: Double
|
||||
get() = averageItemSize() * state.layoutInfo.totalItemsCount
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = (state.layoutInfo.viewportEndOffset - state.layoutInfo.viewportStartOffset).toDouble()
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
val vDistance = scrollOffset - this.scrollOffset
|
||||
// Within a viewport, scroll by EXACT pixels so a drag stays smooth. A lazy
|
||||
// list's pixel geometry is only ESTIMATED (average visible item size), and
|
||||
// that estimate drifts as differently-sized items scroll through the
|
||||
// viewport — so index-snapping every frame makes the thumb jump. scrollBy
|
||||
// sidesteps the estimate entirely. This is what Compose Desktop's own
|
||||
// LazyListScrollbarAdapter does, and for the same reason.
|
||||
if (abs(vDistance) <= viewportSize) {
|
||||
state.scrollBy(vDistance.toFloat())
|
||||
return
|
||||
}
|
||||
// Far jump (track-click paging past a viewport): no exact geometry to lean
|
||||
// on, so estimate an index/offset from the average and snap to it.
|
||||
val vAvg = averageItemSize()
|
||||
if (vAvg <= 0.0) return
|
||||
val vTarget = scrollOffset.coerceAtLeast(0.0)
|
||||
val vTotal = state.layoutInfo.totalItemsCount
|
||||
val vIndex = (vTarget / vAvg).toInt().coerceIn(0, (vTotal - 1).coerceAtLeast(0))
|
||||
val vItemOffset = (vTarget - vIndex * vAvg).toInt().coerceAtLeast(0)
|
||||
state.scrollToItem(vIndex, vItemOffset)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: VerticalScrollbar / HorizontalScrollbar
|
||||
// ==================
|
||||
|
||||
@Composable
|
||||
fun VerticalScrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier = Modifier,
|
||||
reverseLayout: Boolean = false,
|
||||
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = true)
|
||||
|
||||
@Composable
|
||||
fun HorizontalScrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier = Modifier,
|
||||
reverseLayout: Boolean = false,
|
||||
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = Scrollbar(adapter, modifier, reverseLayout, style, interactionSource, isVertical = false)
|
||||
|
||||
/* Shared impl. The root Box is the track (fixed thickness on the cross axis,
|
||||
filling the main axis from the caller's modifier); a child Box is the thumb,
|
||||
positioned with an offset. Drag + track-click are handled on the track in
|
||||
track-relative coordinates (a stable origin), so the grab point stays under
|
||||
the pointer with no feedback from the thumb moving. interactionSource is
|
||||
accepted for API parity; hover here is driven by Modifier.hoverable. */
|
||||
@Composable
|
||||
private fun Scrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier,
|
||||
reverseLayout: Boolean,
|
||||
style: ScrollbarStyle,
|
||||
@Suppress("UNUSED_PARAMETER") interactionSource: MutableInteractionSource,
|
||||
isVertical: Boolean,
|
||||
) {
|
||||
val vScope = rememberCoroutineScope()
|
||||
var vTrackPx by remember { mutableStateOf(0) }
|
||||
val vHoverSource = remember { MutableInteractionSource() }
|
||||
val vHovered by vHoverSource.collectIsHoveredAsState()
|
||||
var vDraggingThumb by remember { mutableStateOf(false) }
|
||||
// onSizeChanged reports PHYSICAL pixels (Option-B density flow), so every
|
||||
// int/px carried through here — vTrackPx, vThumbLen, vThumbPos — is in
|
||||
// physical pixels. Passing them into `.dp` doubles again through
|
||||
// density.toPx() on Retina, which visibly bloated the thumb (filled the
|
||||
// whole track) and made drag/track-click positions land 2× too far down.
|
||||
// Route pixel-sized modifiers through the current density so `.dp` and the
|
||||
// pixel value stay in the same coord space.
|
||||
val vDensity = LocalDensity.current
|
||||
|
||||
val vContent = adapter.contentSize
|
||||
val vViewport = adapter.viewportSize
|
||||
val vOffset = adapter.scrollOffset
|
||||
val vMaxScroll = vContent - vViewport
|
||||
val vScrollable = vMaxScroll > 0.5 && vViewport > 0.0
|
||||
|
||||
val vColor by animateColorAsState(
|
||||
targetValue = if (vHovered || vDraggingThumb) style.hoverColor else style.unhoverColor,
|
||||
animationSpec = tween(style.hoverDurationMillis),
|
||||
)
|
||||
|
||||
val vRoot = (if (isVertical) modifier.width(style.thickness) else modifier.height(style.thickness))
|
||||
.onSizeChanged { vTrackPx = if (isVertical) it.height else it.width }
|
||||
.hoverable(vHoverSource)
|
||||
|
||||
Box(modifier = vRoot) {
|
||||
if (!vScrollable || vTrackPx <= 0) return@Box
|
||||
|
||||
// ============
|
||||
// Thumb metrics (px)
|
||||
val vMinLen = style.minimalHeight.value.toInt()
|
||||
val vThumbLen = (vTrackPx * (vViewport / vContent)).roundToInt()
|
||||
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
|
||||
val vTravel = vTrackPx - vThumbLen
|
||||
val vRawPos = if (vTravel <= 0) 0 else (vTravel * (vOffset / vMaxScroll)).roundToInt()
|
||||
val vThumbPos = (if (reverseLayout) vTravel - vRawPos else vRawPos).coerceIn(0, vTravel)
|
||||
|
||||
val vThumbLenDp = with(vDensity) { vThumbLen.toDp() }
|
||||
val vThumbMod = (
|
||||
if (isVertical) Modifier.offset { IntOffset(0, vThumbPos) }.width(style.thickness).height(vThumbLenDp)
|
||||
else Modifier.offset { IntOffset(vThumbPos, 0) }.height(style.thickness).width(vThumbLenDp)
|
||||
)
|
||||
.clip(style.shape)
|
||||
.background(vColor)
|
||||
|
||||
Box(modifier = vThumbMod)
|
||||
|
||||
// ============
|
||||
// Drag the thumb / page on track click — handled on the track so the
|
||||
// coordinates have a stable (non-moving) origin. The press is CONSUMED:
|
||||
// a scrollbar owns the pointer input it handles, so content sharing this
|
||||
// region (the scrollable beneath, or a parent that also reads pointer
|
||||
// input) never reacts to the same press. Geometry is read LIVE from the
|
||||
// adapter at press time — not from the captured render metrics above,
|
||||
// which go stale between pointerInput relaunches — so a grab after a
|
||||
// wheel-scroll still hit-tests against the thumb's current position.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchTrackSize(isVertical, with(vDensity) { vTrackPx.toDp() }, style.thickness)
|
||||
.pointerInput(isVertical, adapter, vTrackPx, reverseLayout) {
|
||||
awaitEachGesture {
|
||||
val vDown = awaitFirstDown(requireUnconsumed = false)
|
||||
vDown.consume()
|
||||
|
||||
// Live geometry — stable for the drag's duration (content /
|
||||
// viewport / track sizes don't change mid-drag).
|
||||
val vContentPx = adapter.contentSize
|
||||
val vViewportPx = adapter.viewportSize
|
||||
val vMaxScrollPx = vContentPx - vViewportPx
|
||||
if (vMaxScrollPx <= 0.0 || vTrackPx <= 0) return@awaitEachGesture
|
||||
val vMinLen = style.minimalHeight.value.toInt()
|
||||
val vLen = (vTrackPx * (vViewportPx / vContentPx)).roundToInt()
|
||||
.coerceIn(vMinLen.coerceAtMost(vTrackPx), vTrackPx)
|
||||
val vTravelPx = vTrackPx - vLen
|
||||
val vRaw = if (vTravelPx <= 0) 0
|
||||
else (vTravelPx * (adapter.scrollOffset / vMaxScrollPx)).roundToInt()
|
||||
val vPos = (if (reverseLayout) vTravelPx - vRaw else vRaw).coerceIn(0, vTravelPx)
|
||||
|
||||
// Map a track-relative thumb-top to a scroll offset. The
|
||||
// target uses the LIVE max scroll (read fresh each move),
|
||||
// not the press-time value — the adapter compares it against
|
||||
// its live scrollOffset, and mixing a stale scale with a live
|
||||
// one makes the delta run away (grab at the bottom, drag up,
|
||||
// and the list overshoots to the middle).
|
||||
fun scrollToTrackPos(inThumbTop: Int) {
|
||||
if (vTravelPx <= 0) return
|
||||
val vLiveMax = adapter.contentSize - adapter.viewportSize
|
||||
if (vLiveMax <= 0.0) return
|
||||
val vClamped = inThumbTop.coerceIn(0, vTravelPx)
|
||||
val vFrac = (if (reverseLayout) vTravelPx - vClamped else vClamped).toDouble() / vTravelPx
|
||||
vScope.launch { adapter.scrollTo(vFrac * vLiveMax) }
|
||||
}
|
||||
|
||||
val vPress = (if (isVertical) vDown.position.y else vDown.position.x).toInt()
|
||||
val vGrab: Int
|
||||
if (vPress in vPos..(vPos + vLen)) {
|
||||
vDraggingThumb = true
|
||||
vGrab = vPress - vPos
|
||||
} else {
|
||||
// Page toward the click by one viewport.
|
||||
vDraggingThumb = false
|
||||
vGrab = 0
|
||||
val vDir = if (vPress < vPos) -1.0 else 1.0
|
||||
vScope.launch { adapter.scrollTo(adapter.scrollOffset + vDir * vViewportPx) }
|
||||
}
|
||||
|
||||
// Follow the pressed pointer to release, consuming every
|
||||
// change so the interaction can't leak. Only a thumb grab
|
||||
// drags-to-scroll; a track press just consumes.
|
||||
do {
|
||||
val vEvent = awaitPointerEvent()
|
||||
val vChange = vEvent.changes.firstOrNull { it.id == vDown.id } ?: break
|
||||
if (vDraggingThumb) {
|
||||
val vCur = (if (isVertical) vChange.position.y else vChange.position.x).toInt()
|
||||
scrollToTrackPos(vCur - vGrab)
|
||||
}
|
||||
vChange.consume()
|
||||
} while (vEvent.changes.any { it.id == vDown.id && it.pressed })
|
||||
|
||||
vDraggingThumb = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Sizes the invisible drag-capture overlay to the full track. `inTrackDp`
|
||||
comes from the caller pre-converted via LocalDensity (Option-B: layout
|
||||
coords are physical px, so px-to-dp goes through the current density). */
|
||||
private fun Modifier.matchTrackSize(inVertical: Boolean, inTrackDp: Dp, inThickness: Dp): Modifier =
|
||||
if (inVertical) this.width(inThickness).height(inTrackDp)
|
||||
else this.height(inThickness).width(inTrackDp)
|
||||
@@ -0,0 +1,172 @@
|
||||
// COPY of the port's SplitPane (package com.compose.sdl.widgets) so the SAME
|
||||
// implementation runs on the jvm parity target — the port modules are
|
||||
// native-only, so apidemo's commonMain cannot reference them directly.
|
||||
// Keep in sync with the original when it changes.
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// ==================
|
||||
// MARK: SplitPane
|
||||
// ==================
|
||||
|
||||
/** Two-pane resizable split. `initialFirstSize` seeds the first pane's width
|
||||
* (horizontal split) or height (vertical split); after first layout it tracks
|
||||
* the user's drag. `minFirstSize` / `minSecondSize` clamp so neither pane can
|
||||
* shrink past its minimum.
|
||||
*
|
||||
* Internal sizing is in **pixels** to match this port's pixel-space layout
|
||||
* tree (Option-B density flow). `Modifier.width((px / density).dp)` converts
|
||||
* back at the modifier boundary. */
|
||||
@Composable
|
||||
fun HorizontalSplitPane(
|
||||
modifier: Modifier = Modifier,
|
||||
initialFirstSize: Dp = 200.dp,
|
||||
minFirstSize: Dp = 50.dp,
|
||||
minSecondSize: Dp = 50.dp,
|
||||
dividerColor: Color = Color(0x33FFFFFFL),
|
||||
dividerHoverColor: Color = Color(0x66FFFFFFL),
|
||||
first: @Composable () -> Unit,
|
||||
second: @Composable () -> Unit,
|
||||
) {
|
||||
val vDensity = LocalDensity.current
|
||||
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
|
||||
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
|
||||
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
|
||||
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
|
||||
|
||||
var vTotalWidthPx by remember { mutableStateOf(0) }
|
||||
var vFirstWidthPx by remember { mutableStateOf(vInitialFirstPx) }
|
||||
val vDividerSource = remember { MutableInteractionSource() }
|
||||
val vDividerHover by vDividerSource.collectIsHoveredAsState()
|
||||
var vDragging by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { vTotalWidthPx = it.width },
|
||||
) {
|
||||
val vClampedFirstPx = if (vTotalWidthPx > 0)
|
||||
vFirstWidthPx.coerceIn(vMinFirstPx, (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
|
||||
else vFirstWidthPx
|
||||
val vSecondWidthPx = (vTotalWidthPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
|
||||
|
||||
Box(modifier = Modifier.width(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxHeight()) { first() }
|
||||
|
||||
// Solid divider that fills its own slot — what you see is exactly what
|
||||
// you can grab. Hover or an in-progress drag just changes the colour (no
|
||||
// size change → no layout shift, and no flicker when the pointer briefly
|
||||
// leaves the slot mid-drag).
|
||||
val vActive = vDividerHover || vDragging
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(SplitPaneDefaults.DividerThickness)
|
||||
.fillMaxHeight()
|
||||
.background(if (vActive) dividerHoverColor else dividerColor)
|
||||
.hoverable(vDividerSource)
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { vDragging = true },
|
||||
onDragEnd = { vDragging = false },
|
||||
onDragCancel = { vDragging = false },
|
||||
onDrag = { _, delta ->
|
||||
val vMax = (vTotalWidthPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
|
||||
vFirstWidthPx = (vFirstWidthPx + delta.x.toInt()).coerceIn(vMinFirstPx, vMax)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.width(with(vDensity) { vSecondWidthPx.toDp() }).fillMaxHeight()) { second() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalSplitPane(
|
||||
modifier: Modifier = Modifier,
|
||||
initialFirstSize: Dp = 150.dp,
|
||||
minFirstSize: Dp = 50.dp,
|
||||
minSecondSize: Dp = 50.dp,
|
||||
dividerColor: Color = Color(0x33FFFFFFL),
|
||||
dividerHoverColor: Color = Color(0x66FFFFFFL),
|
||||
first: @Composable () -> Unit,
|
||||
second: @Composable () -> Unit,
|
||||
) {
|
||||
val vDensity = LocalDensity.current
|
||||
val vDividerPx = with(vDensity) { SplitPaneDefaults.DividerThickness.toPx().toInt() }
|
||||
val vMinFirstPx = with(vDensity) { minFirstSize.toPx().toInt() }
|
||||
val vMinSecondPx = with(vDensity) { minSecondSize.toPx().toInt() }
|
||||
val vInitialFirstPx = with(vDensity) { initialFirstSize.toPx().toInt() }
|
||||
|
||||
var vTotalHeightPx by remember { mutableStateOf(0) }
|
||||
var vFirstHeightPx by remember { mutableStateOf(vInitialFirstPx) }
|
||||
val vDividerSource = remember { MutableInteractionSource() }
|
||||
val vDividerHover by vDividerSource.collectIsHoveredAsState()
|
||||
var vDragging by remember { mutableStateOf(false) }
|
||||
var vPressStartFirstPx by remember { mutableStateOf(0) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { vTotalHeightPx = it.height },
|
||||
) {
|
||||
val vClampedFirstPx = if (vTotalHeightPx > 0)
|
||||
vFirstHeightPx.coerceIn(vMinFirstPx, (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx))
|
||||
else vFirstHeightPx
|
||||
val vSecondHeightPx = (vTotalHeightPx - vClampedFirstPx - vDividerPx).coerceAtLeast(0)
|
||||
|
||||
Box(modifier = Modifier.height(with(vDensity) { vClampedFirstPx.toDp() }).fillMaxWidth()) { first() }
|
||||
|
||||
// Solid divider that fills its slot; hover/drag only changes the colour
|
||||
// (no size change → no layout shift, no flicker mid-drag).
|
||||
val vActive = vDividerHover || vDragging
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(SplitPaneDefaults.DividerThickness)
|
||||
.fillMaxWidth()
|
||||
.background(if (vActive) dividerHoverColor else dividerColor)
|
||||
.hoverable(vDividerSource)
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
onDragStart = { vDragging = true },
|
||||
onDragEnd = { vDragging = false },
|
||||
onDragCancel = { vDragging = false },
|
||||
onDrag = { _, delta ->
|
||||
val vMax = (vTotalHeightPx - vDividerPx - vMinSecondPx).coerceAtLeast(vMinFirstPx)
|
||||
vFirstHeightPx = (vFirstHeightPx + delta.y.toInt()).coerceIn(vMinFirstPx, vMax)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.height(with(vDensity) { vSecondHeightPx.toDp() }).fillMaxWidth()) { second() }
|
||||
}
|
||||
}
|
||||
|
||||
object SplitPaneDefaults {
|
||||
val DividerThickness: Dp = 4.dp // divider width = hit/drag area (solid; what you see is what you grab)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
@file:OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.loadSvgPainter
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.sp
|
||||
import java.awt.Desktop
|
||||
import java.awt.EventQueue
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.awt.datatransfer.StringSelection
|
||||
import java.io.File
|
||||
import org.jetbrains.skia.Image
|
||||
|
||||
// ==================
|
||||
// MARK: JVM actuals — AWT + upstream Compose Desktop
|
||||
// ==================
|
||||
|
||||
private val kIsWindows = System.getProperty("os.name").startsWith("Windows")
|
||||
private val kIsMac = System.getProperty("os.name").startsWith("Mac")
|
||||
|
||||
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
|
||||
|
||||
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
|
||||
io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
|
||||
|
||||
// ============
|
||||
// Window hooks — the shared App installs them; MainJvm wires them into the
|
||||
// upstream Window() (onCloseRequest / onPreviewKeyEvent).
|
||||
|
||||
internal var jvmOnCloseRequest: (() -> Boolean)? = null
|
||||
internal var jvmOnKeyShortcut: ((KeyEvent) -> Boolean)? = null
|
||||
|
||||
@Composable
|
||||
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
|
||||
androidx.compose.runtime.DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
|
||||
jvmOnCloseRequest = inOnCloseRequest
|
||||
jvmOnKeyShortcut = inOnKeyShortcut
|
||||
onDispose { jvmOnCloseRequest = null; jvmOnKeyShortcut = null }
|
||||
}
|
||||
}
|
||||
|
||||
/* Mirrors SDL_GetPrefPath's layout: <os user-data dir>/<org>/<app>/ with a
|
||||
trailing separator, created on demand. */
|
||||
actual fun appDataDir(inOrg: String, inApp: String): String? {
|
||||
val vHome = System.getProperty("user.home") ?: return null
|
||||
val vBase = when {
|
||||
kIsWindows -> System.getenv("APPDATA") ?: "$vHome/AppData/Roaming"
|
||||
kIsMac -> "$vHome/Library/Application Support"
|
||||
else -> System.getenv("XDG_DATA_HOME") ?: "$vHome/.local/share"
|
||||
}
|
||||
val vDir = File(File(vBase, inOrg), inApp)
|
||||
if (!vDir.exists() && !vDir.mkdirs()) return null
|
||||
return vDir.absolutePath + File.separator
|
||||
}
|
||||
|
||||
/* Opens the path's containing folder (AWT has no cross-platform "select in
|
||||
file manager"); fire-and-forget off the EDT. */
|
||||
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) {
|
||||
Thread {
|
||||
val vOk = runCatching {
|
||||
val vFile = File(inPath)
|
||||
Desktop.getDesktop().open(if (vFile.isDirectory) vFile else vFile.parentFile ?: vFile)
|
||||
}.isSuccess
|
||||
inOnResult?.invoke(vOk)
|
||||
}.apply { isDaemon = true }.start()
|
||||
}
|
||||
|
||||
actual fun fileManagerName(): String = when {
|
||||
kIsWindows -> "Explorer"
|
||||
kIsMac -> "Finder"
|
||||
else -> "Files"
|
||||
}
|
||||
|
||||
/* AWT FileDialog runs its own nested event loop, so invoking from the EDT
|
||||
(where Compose Desktop also runs) keeps the UI serviced while modal. */
|
||||
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) {
|
||||
EventQueue.invokeLater {
|
||||
val vDlg = FileDialog(null as Frame?, "Save", FileDialog.SAVE)
|
||||
inDefaultName?.let { vDlg.file = it }
|
||||
vDlg.isVisible = true
|
||||
inOnResult(vDlg.file?.let { vDlg.directory + it })
|
||||
}
|
||||
}
|
||||
|
||||
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) {
|
||||
EventQueue.invokeLater {
|
||||
val vDlg = FileDialog(null as Frame?, "Open", FileDialog.LOAD)
|
||||
vDlg.isVisible = true
|
||||
inOnResult(vDlg.file?.let { vDlg.directory + it })
|
||||
}
|
||||
}
|
||||
|
||||
/* Upstream desktop's ClipEntry wraps an AWT Transferable. */
|
||||
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry(StringSelection(inText))
|
||||
|
||||
// ============
|
||||
// In-memory response images
|
||||
|
||||
private val kMemoryImages = HashMap<String, ByteArray>()
|
||||
|
||||
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) { kMemoryImages[inKey] = inBytes }
|
||||
|
||||
actual fun removeMemoryImage(inKey: String) { kMemoryImages.remove(inKey) }
|
||||
|
||||
@Composable
|
||||
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter {
|
||||
val vDensity = LocalDensity.current
|
||||
return remember(inKey, inSvg) {
|
||||
val vBytes = kMemoryImages[inKey]
|
||||
runCatching {
|
||||
if (vBytes == null) error("no bytes for $inKey")
|
||||
if (inSvg) loadSvgPainter(vBytes.inputStream(), vDensity)
|
||||
else BitmapPainter(Image.makeFromEncoded(vBytes).toComposeImageBitmap())
|
||||
}.getOrElse { BitmapPainter(ImageBitmap(1, 1)) }
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Text layout
|
||||
|
||||
/* Upstream TextMeasurer at density 1 so the px-valued font size maps 1:1
|
||||
(the caller passes an already density-scaled size). */
|
||||
private val kRowMeasurer: TextMeasurer by lazy {
|
||||
TextMeasurer(createFontFamilyResolver(), Density(1f), LayoutDirection.Ltr)
|
||||
}
|
||||
|
||||
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
|
||||
runCatching {
|
||||
kRowMeasurer.measure(
|
||||
AnnotatedString(inText),
|
||||
TextStyle(fontSize = inFontPx.sp, fontFamily = monoFontFamily ?: FontFamily.Monospace),
|
||||
constraints = Constraints(maxWidth = inMaxWidthPx),
|
||||
).lineCount
|
||||
}.getOrDefault(1)
|
||||
|
||||
/* The jvm parity app only stores the preference — upstream's text pipeline has
|
||||
no global tab-width knob. Backed by state so menu checkmarks recompose. */
|
||||
private var fTabWidth by mutableStateOf(4)
|
||||
actual var editorTabWidth: Int
|
||||
get() = fTabWidth
|
||||
set(value) { fTabWidth = value }
|
||||
@@ -0,0 +1,23 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.platform.Font
|
||||
|
||||
// ==================
|
||||
// MARK: Monospace body font — JVM actuals (classpath font/ staged by
|
||||
// jvmProcessResources)
|
||||
// ==================
|
||||
|
||||
actual val monoFontFamily: FontFamily? by lazy {
|
||||
val vBytes = object {}.javaClass.getResourceAsStream("/font/NotoSansMono.ttf")?.use { it.readBytes() }
|
||||
if (vBytes == null) {
|
||||
println("apidemo: NotoSansMono.ttf not on the classpath — body uses the default font")
|
||||
null
|
||||
} else {
|
||||
FontFamily(Font(identity = kMonoFamily, data = vBytes))
|
||||
}
|
||||
}
|
||||
|
||||
actual val monoFontFamilyName: String? by lazy {
|
||||
if (monoFontFamily != null) kMonoFamily else null
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
|
||||
// The JVM comparison app's entry point. Runs the SAME shared App() as the
|
||||
// native :apidemo, but on Compose Desktop (JVM) against upstream
|
||||
// org.jetbrains.compose. Client-certificate (mTLS) features are native-only
|
||||
// (they drive the bundled libcurl); the jvm actuals report that instead.
|
||||
fun main() = application {
|
||||
Window(
|
||||
// The shared App installs a persist-then-close hook (InstallWindowHooks).
|
||||
onCloseRequest = { if (jvmOnCloseRequest?.invoke() != false) exitApplication() },
|
||||
onPreviewKeyEvent = { jvmOnKeyShortcut?.invoke(it) ?: false },
|
||||
title = "API Manager — JVM (upstream Compose)",
|
||||
state = rememberWindowState(width = 1240.dp, height = 820.dp),
|
||||
) {
|
||||
App()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package apidemo
|
||||
|
||||
// ==================
|
||||
// MARK: TLS chain / client-certificate — JVM actuals (native-only feature)
|
||||
// ==================
|
||||
// The native builds drive the bundled libcurl (CURLOPT_SSLCERT / CERTINFO);
|
||||
// there is no libcurl on the jvm parity stack, so both entry points report
|
||||
// the limitation instead of half-implementing it over JSSE.
|
||||
|
||||
private const val kNativeOnly =
|
||||
"Client-certificate (mTLS) features need the native build — they drive the bundled libcurl directly."
|
||||
|
||||
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain = TlsChain(emptyList(), kNativeOnly)
|
||||
|
||||
actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse = ApiResponse(
|
||||
ok = false,
|
||||
status = 0,
|
||||
statusText = "",
|
||||
timeMs = 0,
|
||||
sizeBytes = 0,
|
||||
headers = emptyList(),
|
||||
body = "",
|
||||
error = kNativeOnly,
|
||||
)
|
||||
|
||||
actual fun sweepTempClientCerts() {}
|
||||
@@ -0,0 +1,63 @@
|
||||
package apidemo
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import com.compose.sdl.LocalComposeNativeWindow
|
||||
import com.compose.sdl.TextLayoutConfig
|
||||
import com.compose.sdl.registerMemoryResource
|
||||
import com.compose.sdl.removeMemoryResource
|
||||
import com.compose.sdl.res.ResourceKind
|
||||
import com.compose.sdl.text.currentTextMeasurer
|
||||
|
||||
// ==================
|
||||
// MARK: Native actuals — pure delegation to the port's SDL-backed APIs
|
||||
// ==================
|
||||
|
||||
actual fun appDataDir(inOrg: String, inApp: String): String? =
|
||||
com.compose.sdl.appDataDir(inOrg, inApp)
|
||||
|
||||
actual fun revealInFileManager(inPath: String, inOnResult: ((Boolean) -> Unit)?) =
|
||||
com.compose.sdl.revealInFileManager(inPath, inOnResult)
|
||||
|
||||
actual fun fileManagerName(): String = com.compose.sdl.fileManagerName()
|
||||
|
||||
actual fun showSaveFileDialog(inDefaultName: String?, inOnResult: (String?) -> Unit) =
|
||||
com.compose.sdl.showSaveFileDialog(inDefaultName, inOnResult)
|
||||
|
||||
actual fun showOpenFileDialog(inOnResult: (String?) -> Unit) =
|
||||
com.compose.sdl.showOpenFileDialog(inOnResult)
|
||||
|
||||
actual fun clipEntryOfText(inText: String): ClipEntry = ClipEntry.withPlainText(inText)
|
||||
|
||||
actual fun registerMemoryImage(inKey: String, inBytes: ByteArray) = registerMemoryResource(inKey, inBytes)
|
||||
|
||||
actual fun removeMemoryImage(inKey: String) = removeMemoryResource(inKey)
|
||||
|
||||
@Composable
|
||||
actual fun memoryImagePainter(inKey: String, inSvg: Boolean): Painter =
|
||||
com.compose.sdl.res.painterResource(inKey, if (inSvg) ResourceKind.Svg else ResourceKind.Raster)
|
||||
|
||||
actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inFamilyName: String?): Int =
|
||||
currentTextMeasurer.wrap(inText, inFontPx, inMaxWidthPx, inFamilyName).lines.size
|
||||
|
||||
actual var editorTabWidth: Int
|
||||
get() = TextLayoutConfig.tabWidth
|
||||
set(value) { TextLayoutConfig.tabWidth = value }
|
||||
|
||||
internal actual val systemFileSystem: okio.FileSystem = okio.FileSystem.SYSTEM
|
||||
|
||||
actual fun createApiHttpClient(): io.ktor.client.HttpClient =
|
||||
io.ktor.client.HttpClient(io.ktor.client.engine.curl.Curl)
|
||||
|
||||
@Composable
|
||||
actual fun InstallWindowHooks(inOnCloseRequest: () -> Boolean, inOnKeyShortcut: (KeyEvent) -> Boolean) {
|
||||
val vWindow = LocalComposeNativeWindow.current
|
||||
DisposableEffect(inOnCloseRequest, inOnKeyShortcut) {
|
||||
vWindow.setOnCloseRequest(inOnCloseRequest)
|
||||
vWindow.setOnKeyShortcut(inOnKeyShortcut)
|
||||
onDispose { vWindow.setOnCloseRequest(null); vWindow.setOnKeyShortcut(null) }
|
||||
}
|
||||
}
|
||||
@@ -39,21 +39,10 @@ class PreparedCert(
|
||||
Throws with a user-facing message if the cert/key can't be loaded. */
|
||||
expect fun prepareClientCert(inReq: ApiRequest): PreparedCert
|
||||
|
||||
/* Remove any temporary client certs (and their key containers) a previous run
|
||||
left behind in the Windows store after a crash — identified by our prefix, so
|
||||
the user's own certificates are never touched. No-op off Windows. Call once
|
||||
at startup. */
|
||||
expect fun sweepTempClientCerts()
|
||||
|
||||
/* One certificate in the chain: its CURLINFO_CERTINFO-style fields (Subject /
|
||||
Issuer / the PEM under "Cert" / dates …) and whether the server actually
|
||||
presented it. Derived certs — an issuer resolved from the OS store, or a
|
||||
name-only placeholder — have fromServer = false and are drawn dotted. */
|
||||
class ChainCert(val fields: List<Pair<String, String>>, val fromServer: Boolean)
|
||||
|
||||
/* The server's TLS certificate chain. error is set instead when it couldn't be
|
||||
fetched; fields vary by TLS backend. */
|
||||
class TlsChain(val certs: List<ChainCert>, val error: String?)
|
||||
// ChainCert / TlsChain and the sweepTempClientCerts / inspectTlsChain /
|
||||
// curlSendWithClientCert expects live in commonMain (TlsCommon.kt) — the UI
|
||||
// consumes them from shared code; the per-OS ClientCert*.kt actuals below the
|
||||
// native tree actualize sweepTempClientCerts directly.
|
||||
|
||||
/* Continue a server-presented chain with its issuer(s). Where possible the
|
||||
issuer is resolved from the OS certificate store with full info; otherwise the
|
||||
@@ -120,7 +109,7 @@ private fun onCurlDiscard(inBuffer: CPointer<ByteVar>, inSize: size_t, inCount:
|
||||
request's client certificate if it has one (so mTLS endpoints work too).
|
||||
inReq is already var-resolved. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
||||
actual fun inspectTlsChain(inReq: ApiRequest): TlsChain
|
||||
{
|
||||
val vCert = try { if (inReq.hasClientCert) prepareClientCert(inReq) else null }
|
||||
catch (e: Throwable) { return TlsChain(emptyList(), e.message ?: "Client certificate error") }
|
||||
@@ -185,7 +174,7 @@ private fun readCertInfo(inCurl: COpaquePointer): List<List<Pair<String, String>
|
||||
/* Send a client-certificate request through libcurl and adapt the result into
|
||||
the same ApiResponse the Ktor path produces. inReq is already var-resolved. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
||||
actual fun curlSendWithClientCert(inReq: ApiRequest): ApiResponse
|
||||
{
|
||||
val vMark = TimeSource.Monotonic.markNow()
|
||||
val vCert = try { prepareClientCert(inReq) }
|
||||
|
||||
@@ -6,17 +6,10 @@ import com.compose.sdl.loadComposeResourceBytes
|
||||
import com.compose.sdl.text.namedFontFamily
|
||||
|
||||
// ==================
|
||||
// MARK: Monospace body font
|
||||
// MARK: Monospace body font — native actuals (data.kres + IconFont)
|
||||
// ==================
|
||||
|
||||
// Family name the body editor passes for monospace text. Resolves to the bundled
|
||||
// Noto Sans Mono once registered; null means the font isn't in data.kres, in
|
||||
// which case the body falls back to the default (proportional Noto Sans).
|
||||
const val kMonoFamily = "noto-mono"
|
||||
|
||||
// The raw project family name string — used by the SyntaxHighlight tokeniser's
|
||||
// wrap() call which measures via NativeTextMeasurer (accepts a name string).
|
||||
val monoFontFamilyName: String? by lazy {
|
||||
actual val monoFontFamilyName: String? by lazy {
|
||||
val vBytes = loadComposeResourceBytes("font/NotoSansMono.ttf")
|
||||
if (vBytes == null) {
|
||||
println("apidemo: NotoSansMono.ttf not bundled — body uses the default font")
|
||||
@@ -27,9 +20,6 @@ val monoFontFamilyName: String? by lazy {
|
||||
}
|
||||
}
|
||||
|
||||
// The material3 Text / BasicTextField-shaped FontFamily wrapper. Wraps the
|
||||
// registered project family in a `NamedFont`-backed FontFamily so the m3 Text
|
||||
// signature (which takes `FontFamily?` proper) can be given the mono family.
|
||||
val monoFontFamily: FontFamily? by lazy {
|
||||
actual val monoFontFamily: FontFamily? by lazy {
|
||||
monoFontFamilyName?.let { namedFontFamily(it) }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+18
-1
@@ -5,6 +5,7 @@
|
||||
// apply the multiplatform plugin. Each subproject still applies the ones it needs.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.kotlin.plugin.compose) apply false
|
||||
alias(libs.plugins.kotlin.plugin.serialization) apply false
|
||||
alias(libs.plugins.compose.multiplatform) apply false
|
||||
@@ -17,7 +18,8 @@ plugins {
|
||||
// surface worth tracking, so skip them.
|
||||
apiValidation {
|
||||
klib { enabled = true }
|
||||
ignoredProjects.addAll(listOf("demo", "apidemo"))
|
||||
// compose-desktop-native-bridge is a plain JVM Gradle plugin — no klib ABI to track.
|
||||
ignoredProjects.addAll(listOf("demo", "apidemo", "compose-desktop-native-bridge"))
|
||||
}
|
||||
|
||||
// Project coordinates. `group` is baked into every module's Kotlin/Native klib
|
||||
@@ -47,6 +49,7 @@ allprojects {
|
||||
val kAppModules = setOf(":demo", ":apidemo")
|
||||
val kPublishedLibs = setOf(
|
||||
":ui", ":ui-util", ":ui-geometry", ":ui-unit", ":ui-backhandler",
|
||||
":ui-tooling-preview",
|
||||
":animation-core", ":animation", ":animation-graphics",
|
||||
":foundation", ":foundation-layout",
|
||||
":material3", ":material-ripple",
|
||||
@@ -121,8 +124,22 @@ allprojects {
|
||||
if (vNativeTargetTokens.any { name.contains(it, ignoreCase = true) }) {
|
||||
resolutionStrategy.dependencySubstitution {
|
||||
substitute(module("org.jetbrains.compose.ui:ui")).using(project(":ui"))
|
||||
// ui's split artifacts: ui-graphics + ui-text are merged INTO
|
||||
// :ui in the port; ui-unit / ui-geometry have their own modules.
|
||||
substitute(module("org.jetbrains.compose.ui:ui-graphics")).using(project(":ui"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-text")).using(project(":ui"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-unit")).using(project(":ui-unit"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-geometry")).using(project(":ui-geometry"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-util")).using(project(":ui-util"))
|
||||
substitute(module("org.jetbrains.compose.ui:ui-tooling-preview")).using(project(":ui-tooling-preview"))
|
||||
substitute(module("org.jetbrains.compose.foundation:foundation")).using(project(":foundation"))
|
||||
substitute(module("org.jetbrains.compose.foundation:foundation-layout")).using(project(":foundation-layout"))
|
||||
substitute(module("org.jetbrains.compose.animation:animation")).using(project(":animation"))
|
||||
substitute(module("org.jetbrains.compose.animation:animation-core")).using(project(":animation-core"))
|
||||
substitute(module("org.jetbrains.compose.material3:material3")).using(project(":material3"))
|
||||
// navigation3-ui: the JB Maven artifact has no K/N desktop
|
||||
// klibs — the port vendors it as :navigation3-ui.
|
||||
substitute(module("org.jetbrains.androidx.navigation3:navigation3-ui")).using(project(":navigation3-ui"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import com.compose.sdl.graphics.decodeEncodedImageBitmap
|
||||
drawables ship at a single density, so no decode-time rescale applies. */
|
||||
internal actual fun ByteArray.toImageBitmap(resourceDensity: Int, targetDensity: Int): ImageBitmap =
|
||||
decodeEncodedImageBitmap(this)
|
||||
?: error("Image decode failed — is the SDL renderer initialised before painterResource ran?")
|
||||
?: error("Image decode failed — is the render backend initialised before painterResource ran?")
|
||||
|
||||
/* SVG on SDL: SDL3_image rasterises SVG at its intrinsic size (IMG_LoadSVG),
|
||||
so the "element" is just the raw bytes and the painter is the rasterised
|
||||
@@ -34,5 +34,5 @@ internal actual fun ByteArray.toSvgElement(): SvgElement = SvgElement(this)
|
||||
internal actual fun SvgElement.toSvgPainter(density: Density): Painter =
|
||||
BitmapPainter(
|
||||
decodeEncodedImageBitmap(bytes)
|
||||
?: error("SVG rasterisation failed — is the SDL renderer initialised before painterResource ran?")
|
||||
?: error("SVG rasterisation failed — is the render backend initialised before painterResource ran?")
|
||||
)
|
||||
|
||||
@@ -34,6 +34,8 @@ import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import sdl3.SDL_Delay
|
||||
import sdl3.SDL_GetPerformanceCounter
|
||||
import sdl3.SDL_GetPerformanceFrequency
|
||||
import sdl3.SDL_GetTicks
|
||||
import sdl3.SDL_Quit
|
||||
import sdl3.SDL_SetWindowTitle
|
||||
@@ -146,7 +148,11 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
|
||||
// ============
|
||||
// Main loop
|
||||
var vGcLastTicks = SDL_GetTicks()
|
||||
var vRenderedSinceGc = false
|
||||
val vProfiler = if (platform.posix.getenv("CDN_PROFILE") != null) FrameProfiler() else null
|
||||
while (!runtime.exitRequested) {
|
||||
vProfiler?.mark()
|
||||
Snapshot.sendApplyNotifications()
|
||||
|
||||
// ============
|
||||
@@ -174,6 +180,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
}
|
||||
|
||||
mainDispatcher.drainPending()
|
||||
vProfiler?.phase(0)
|
||||
|
||||
// ============
|
||||
// App composition pump — Window()s may appear / disappear here.
|
||||
@@ -193,6 +200,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
// 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
|
||||
vProfiler?.phase(1)
|
||||
|
||||
// ============
|
||||
// Per-window pump + render.
|
||||
@@ -206,13 +214,24 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
Snapshot.sendApplyNotifications()
|
||||
vW.frameClock.sendFrame()
|
||||
yield()
|
||||
vProfiler?.phase(2)
|
||||
if (vW.shouldRender()) {
|
||||
vW.renderFrame()
|
||||
vAnyRendered = true
|
||||
if (!vW.backend.vsyncEnabled) vAllVsync = false
|
||||
}
|
||||
vProfiler?.phase(3)
|
||||
}
|
||||
runtime.reapDestroyed()
|
||||
vProfiler?.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.
|
||||
@@ -224,6 +243,23 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
SDL_WaitEventTimeout(null, 10)
|
||||
for (vW in runtime.windows) vW.resetFpsWindow()
|
||||
}
|
||||
|
||||
// ============
|
||||
// Native-memory nudge. Renderer resources (Skia surfaces / images /
|
||||
// fonts, SDL textures) are freed by Cleaners that only run when the
|
||||
// Kotlin/Native GC collects — and a Compose app's Kotlin heap is
|
||||
// small enough that the allocation-driven scheduler can starve them
|
||||
// for minutes while the NATIVE heap balloons (issue #2: memory
|
||||
// "never released" while navigating). Collect periodically, only
|
||||
// after rendering activity, between frames; costs ~ms at these
|
||||
// heap sizes and keeps RSS tracking real usage.
|
||||
if (vAnyRendered) vRenderedSinceGc = true
|
||||
val vNowTicks = SDL_GetTicks()
|
||||
if (vRenderedSinceGc && vNowTicks - vGcLastTicks >= 10_000uL) {
|
||||
vGcLastTicks = vNowTicks
|
||||
vRenderedSinceGc = false
|
||||
collectNativeGarbage()
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
@@ -231,6 +267,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
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()
|
||||
@@ -243,6 +280,52 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
||||
SDL_Quit()
|
||||
}
|
||||
|
||||
/* CDN_PROFILE=1 — per-phase main-loop timings, printed every ~2s of rendered
|
||||
frames. Phases: events (poll+dispatch), app (app-composition pump), pump
|
||||
(per-window clocks/dispatch), render (layout+draw+present). Measure first,
|
||||
optimize second — see ROADMAP.md. */
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
internal class FrameProfiler {
|
||||
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
|
||||
private val fSum = DoubleArray(4)
|
||||
private val fMax = DoubleArray(4)
|
||||
private var fFrames = 0
|
||||
private var fLastPrintMs = SDL_GetTicks()
|
||||
private var fMark = 0uL
|
||||
|
||||
fun mark() { fMark = SDL_GetPerformanceCounter() }
|
||||
|
||||
fun phase(inIndex: Int) {
|
||||
val vNow = SDL_GetPerformanceCounter()
|
||||
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
|
||||
fSum[inIndex] += vMs
|
||||
if (vMs > fMax[inIndex]) fMax[inIndex] = vMs
|
||||
fMark = vNow
|
||||
}
|
||||
|
||||
fun frameDone(inRendered: Boolean) {
|
||||
if (inRendered) fFrames++
|
||||
val vNowMs = SDL_GetTicks()
|
||||
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
|
||||
val vNames = listOf("events", "app", "pump", "render")
|
||||
val vParts = vNames.mapIndexed { vI, vName ->
|
||||
val vAvg = fSum[vI] / fFrames
|
||||
"$vName=${(vAvg * 100).toInt() / 100.0}/${(fMax[vI] * 100).toInt() / 100.0}ms"
|
||||
}
|
||||
val vTotal = fSum.sum() / fFrames
|
||||
println("[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + " total=${(vTotal * 100).toInt() / 100.0}ms")
|
||||
for (vI in fSum.indices) { fSum[vI] = 0.0; fMax[vI] = 0.0 }
|
||||
fFrames = 0
|
||||
fLastPrintMs = vNowMs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
||||
their native memory (see the main loop's native-memory nudge). */
|
||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
||||
|
||||
/* Single-window compatibility wrapper — the pre-multi-window entry point.
|
||||
Closing the window exits the app, exactly as before. */
|
||||
fun nativeComposeWindow(
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Klib ABI Dump
|
||||
// Targets: [linuxArm64, linuxX64, macosArm64, mingwX64]
|
||||
// Rendering settings:
|
||||
// - Signature version: 2
|
||||
// - Show manifest properties: true
|
||||
// - Show declarations: true
|
||||
|
||||
// Library unique name: <com.bitsycore.compose.sdl:ui-tooling-preview>
|
||||
open annotation class androidx.compose.ui.tooling.preview/AndroidUiMode : kotlin/Annotation { // androidx.compose.ui.tooling.preview/AndroidUiMode|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview/AndroidUiMode.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/Preview : kotlin/Annotation { // androidx.compose.ui.tooling.preview/Preview|null[0]
|
||||
constructor <init>(kotlin/String = ..., kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Float = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Long = ..., kotlin/Int = ..., kotlin/String = ..., kotlin/Int = ...) // androidx.compose.ui.tooling.preview/Preview.<init>|<init>(kotlin.String;kotlin.String;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.String;kotlin.Float;kotlin.Boolean;kotlin.Boolean;kotlin.Long;kotlin.Int;kotlin.String;kotlin.Int){}[0]
|
||||
|
||||
final val apiLevel // androidx.compose.ui.tooling.preview/Preview.apiLevel|{}apiLevel[0]
|
||||
final fun <get-apiLevel>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.apiLevel.<get-apiLevel>|<get-apiLevel>(){}[0]
|
||||
final val backgroundColor // androidx.compose.ui.tooling.preview/Preview.backgroundColor|{}backgroundColor[0]
|
||||
final fun <get-backgroundColor>(): kotlin/Long // androidx.compose.ui.tooling.preview/Preview.backgroundColor.<get-backgroundColor>|<get-backgroundColor>(){}[0]
|
||||
final val device // androidx.compose.ui.tooling.preview/Preview.device|{}device[0]
|
||||
final fun <get-device>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.device.<get-device>|<get-device>(){}[0]
|
||||
final val fontScale // androidx.compose.ui.tooling.preview/Preview.fontScale|{}fontScale[0]
|
||||
final fun <get-fontScale>(): kotlin/Float // androidx.compose.ui.tooling.preview/Preview.fontScale.<get-fontScale>|<get-fontScale>(){}[0]
|
||||
final val group // androidx.compose.ui.tooling.preview/Preview.group|{}group[0]
|
||||
final fun <get-group>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.group.<get-group>|<get-group>(){}[0]
|
||||
final val heightDp // androidx.compose.ui.tooling.preview/Preview.heightDp|{}heightDp[0]
|
||||
final fun <get-heightDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.heightDp.<get-heightDp>|<get-heightDp>(){}[0]
|
||||
final val locale // androidx.compose.ui.tooling.preview/Preview.locale|{}locale[0]
|
||||
final fun <get-locale>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.locale.<get-locale>|<get-locale>(){}[0]
|
||||
final val name // androidx.compose.ui.tooling.preview/Preview.name|{}name[0]
|
||||
final fun <get-name>(): kotlin/String // androidx.compose.ui.tooling.preview/Preview.name.<get-name>|<get-name>(){}[0]
|
||||
final val showBackground // androidx.compose.ui.tooling.preview/Preview.showBackground|{}showBackground[0]
|
||||
final fun <get-showBackground>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showBackground.<get-showBackground>|<get-showBackground>(){}[0]
|
||||
final val showSystemUi // androidx.compose.ui.tooling.preview/Preview.showSystemUi|{}showSystemUi[0]
|
||||
final fun <get-showSystemUi>(): kotlin/Boolean // androidx.compose.ui.tooling.preview/Preview.showSystemUi.<get-showSystemUi>|<get-showSystemUi>(){}[0]
|
||||
final val uiMode // androidx.compose.ui.tooling.preview/Preview.uiMode|{}uiMode[0]
|
||||
final fun <get-uiMode>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.uiMode.<get-uiMode>|<get-uiMode>(){}[0]
|
||||
final val wallpaper // androidx.compose.ui.tooling.preview/Preview.wallpaper|{}wallpaper[0]
|
||||
final fun <get-wallpaper>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.wallpaper.<get-wallpaper>|<get-wallpaper>(){}[0]
|
||||
final val widthDp // androidx.compose.ui.tooling.preview/Preview.widthDp|{}widthDp[0]
|
||||
final fun <get-widthDp>(): kotlin/Int // androidx.compose.ui.tooling.preview/Preview.widthDp.<get-widthDp>|<get-widthDp>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewDynamicColors : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewDynamicColors|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewDynamicColors.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewFontScale : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewFontScale|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewFontScale.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewLightDark : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewLightDark|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewLightDark.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewParameter : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewParameter|null[0]
|
||||
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>>, kotlin/Int = ...) // androidx.compose.ui.tooling.preview/PreviewParameter.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewParameterProvider<*>>;kotlin.Int){}[0]
|
||||
|
||||
final val limit // androidx.compose.ui.tooling.preview/PreviewParameter.limit|{}limit[0]
|
||||
final fun <get-limit>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameter.limit.<get-limit>|<get-limit>(){}[0]
|
||||
final val provider // androidx.compose.ui.tooling.preview/PreviewParameter.provider|{}provider[0]
|
||||
final fun <get-provider>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewParameterProvider<*>> // androidx.compose.ui.tooling.preview/PreviewParameter.provider.<get-provider>|<get-provider>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewScreenSizes : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewScreenSizes|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview/PreviewScreenSizes.<init>|<init>(){}[0]
|
||||
}
|
||||
|
||||
open annotation class androidx.compose.ui.tooling.preview/PreviewWrapper : kotlin/Annotation { // androidx.compose.ui.tooling.preview/PreviewWrapper|null[0]
|
||||
constructor <init>(kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider>) // androidx.compose.ui.tooling.preview/PreviewWrapper.<init>|<init>(kotlin.reflect.KClass<out|androidx.compose.ui.tooling.preview.PreviewWrapperProvider>){}[0]
|
||||
|
||||
final val wrapper // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper|{}wrapper[0]
|
||||
final fun <get-wrapper>(): kotlin.reflect/KClass<out androidx.compose.ui.tooling.preview/PreviewWrapperProvider> // androidx.compose.ui.tooling.preview/PreviewWrapper.wrapper.<get-wrapper>|<get-wrapper>(){}[0]
|
||||
}
|
||||
|
||||
abstract interface <#A: kotlin/Any?> androidx.compose.ui.tooling.preview/PreviewParameterProvider { // androidx.compose.ui.tooling.preview/PreviewParameterProvider|null[0]
|
||||
abstract val values // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values|{}values[0]
|
||||
abstract fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview/PreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
|
||||
open val count // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count|{}count[0]
|
||||
open fun <get-count>(): kotlin/Int // androidx.compose.ui.tooling.preview/PreviewParameterProvider.count.<get-count>|<get-count>(){}[0]
|
||||
|
||||
open fun getDisplayName(kotlin/Int): kotlin/String? // androidx.compose.ui.tooling.preview/PreviewParameterProvider.getDisplayName|getDisplayName(kotlin.Int){}[0]
|
||||
}
|
||||
|
||||
abstract interface androidx.compose.ui.tooling.preview/PreviewWrapperProvider { // androidx.compose.ui.tooling.preview/PreviewWrapperProvider|null[0]
|
||||
abstract fun Wrap(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // androidx.compose.ui.tooling.preview/PreviewWrapperProvider.Wrap|Wrap(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||
}
|
||||
|
||||
open class <#A: kotlin/Any?> androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider : androidx.compose.ui.tooling.preview/PreviewParameterProvider<#A> { // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider|null[0]
|
||||
constructor <init>(kotlin.collections/Collection<#A>) // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.<init>|<init>(kotlin.collections.Collection<1:0>){}[0]
|
||||
|
||||
open val values // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values|{}values[0]
|
||||
open fun <get-values>(): kotlin.sequences/Sequence<#A> // androidx.compose.ui.tooling.preview.datasource/CollectionPreviewParameterProvider.values.<get-values>|<get-values>(){}[0]
|
||||
}
|
||||
|
||||
open class androidx.compose.ui.tooling.preview.datasource/LoremIpsum : androidx.compose.ui.tooling.preview/PreviewParameterProvider<kotlin/String> { // androidx.compose.ui.tooling.preview.datasource/LoremIpsum|null[0]
|
||||
constructor <init>() // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(){}[0]
|
||||
constructor <init>(kotlin/Int) // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.<init>|<init>(kotlin.Int){}[0]
|
||||
|
||||
open val values // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values|{}values[0]
|
||||
open fun <get-values>(): kotlin.sequences/Sequence<kotlin/String> // androidx.compose.ui.tooling.preview.datasource/LoremIpsum.values.<get-values>|<get-values>(){}[0]
|
||||
}
|
||||
|
||||
final object androidx.compose.ui.tooling.preview/AndroidUiModes { // androidx.compose.ui.tooling.preview/AndroidUiModes|null[0]
|
||||
final const val UI_MODE_NIGHT_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK|{}UI_MODE_NIGHT_MASK[0]
|
||||
final fun <get-UI_MODE_NIGHT_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_MASK.<get-UI_MODE_NIGHT_MASK>|<get-UI_MODE_NIGHT_MASK>(){}[0]
|
||||
final const val UI_MODE_NIGHT_NO // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO|{}UI_MODE_NIGHT_NO[0]
|
||||
final fun <get-UI_MODE_NIGHT_NO>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_NO.<get-UI_MODE_NIGHT_NO>|<get-UI_MODE_NIGHT_NO>(){}[0]
|
||||
final const val UI_MODE_NIGHT_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED|{}UI_MODE_NIGHT_UNDEFINED[0]
|
||||
final fun <get-UI_MODE_NIGHT_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_UNDEFINED.<get-UI_MODE_NIGHT_UNDEFINED>|<get-UI_MODE_NIGHT_UNDEFINED>(){}[0]
|
||||
final const val UI_MODE_NIGHT_YES // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES|{}UI_MODE_NIGHT_YES[0]
|
||||
final fun <get-UI_MODE_NIGHT_YES>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_NIGHT_YES.<get-UI_MODE_NIGHT_YES>|<get-UI_MODE_NIGHT_YES>(){}[0]
|
||||
final const val UI_MODE_TYPE_APPLIANCE // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE|{}UI_MODE_TYPE_APPLIANCE[0]
|
||||
final fun <get-UI_MODE_TYPE_APPLIANCE>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_APPLIANCE.<get-UI_MODE_TYPE_APPLIANCE>|<get-UI_MODE_TYPE_APPLIANCE>(){}[0]
|
||||
final const val UI_MODE_TYPE_CAR // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR|{}UI_MODE_TYPE_CAR[0]
|
||||
final fun <get-UI_MODE_TYPE_CAR>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_CAR.<get-UI_MODE_TYPE_CAR>|<get-UI_MODE_TYPE_CAR>(){}[0]
|
||||
final const val UI_MODE_TYPE_DESK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK|{}UI_MODE_TYPE_DESK[0]
|
||||
final fun <get-UI_MODE_TYPE_DESK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_DESK.<get-UI_MODE_TYPE_DESK>|<get-UI_MODE_TYPE_DESK>(){}[0]
|
||||
final const val UI_MODE_TYPE_MASK // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK|{}UI_MODE_TYPE_MASK[0]
|
||||
final fun <get-UI_MODE_TYPE_MASK>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_MASK.<get-UI_MODE_TYPE_MASK>|<get-UI_MODE_TYPE_MASK>(){}[0]
|
||||
final const val UI_MODE_TYPE_NORMAL // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL|{}UI_MODE_TYPE_NORMAL[0]
|
||||
final fun <get-UI_MODE_TYPE_NORMAL>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_NORMAL.<get-UI_MODE_TYPE_NORMAL>|<get-UI_MODE_TYPE_NORMAL>(){}[0]
|
||||
final const val UI_MODE_TYPE_TELEVISION // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION|{}UI_MODE_TYPE_TELEVISION[0]
|
||||
final fun <get-UI_MODE_TYPE_TELEVISION>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_TELEVISION.<get-UI_MODE_TYPE_TELEVISION>|<get-UI_MODE_TYPE_TELEVISION>(){}[0]
|
||||
final const val UI_MODE_TYPE_UNDEFINED // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED|{}UI_MODE_TYPE_UNDEFINED[0]
|
||||
final fun <get-UI_MODE_TYPE_UNDEFINED>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_UNDEFINED.<get-UI_MODE_TYPE_UNDEFINED>|<get-UI_MODE_TYPE_UNDEFINED>(){}[0]
|
||||
final const val UI_MODE_TYPE_VR_HEADSET // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET|{}UI_MODE_TYPE_VR_HEADSET[0]
|
||||
final fun <get-UI_MODE_TYPE_VR_HEADSET>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_VR_HEADSET.<get-UI_MODE_TYPE_VR_HEADSET>|<get-UI_MODE_TYPE_VR_HEADSET>(){}[0]
|
||||
final const val UI_MODE_TYPE_WATCH // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH|{}UI_MODE_TYPE_WATCH[0]
|
||||
final fun <get-UI_MODE_TYPE_WATCH>(): kotlin/Int // androidx.compose.ui.tooling.preview/AndroidUiModes.UI_MODE_TYPE_WATCH.<get-UI_MODE_TYPE_WATCH>|<get-UI_MODE_TYPE_WATCH>(){}[0]
|
||||
}
|
||||
|
||||
final object androidx.compose.ui.tooling.preview/Devices { // androidx.compose.ui.tooling.preview/Devices|null[0]
|
||||
final const val AUTOMOTIVE_1024p // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p|{}AUTOMOTIVE_1024p[0]
|
||||
final fun <get-AUTOMOTIVE_1024p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.AUTOMOTIVE_1024p.<get-AUTOMOTIVE_1024p>|<get-AUTOMOTIVE_1024p>(){}[0]
|
||||
final const val DEFAULT // androidx.compose.ui.tooling.preview/Devices.DEFAULT|{}DEFAULT[0]
|
||||
final fun <get-DEFAULT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DEFAULT.<get-DEFAULT>|<get-DEFAULT>(){}[0]
|
||||
final const val DESKTOP // androidx.compose.ui.tooling.preview/Devices.DESKTOP|{}DESKTOP[0]
|
||||
final fun <get-DESKTOP>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.DESKTOP.<get-DESKTOP>|<get-DESKTOP>(){}[0]
|
||||
final const val FOLDABLE // androidx.compose.ui.tooling.preview/Devices.FOLDABLE|{}FOLDABLE[0]
|
||||
final fun <get-FOLDABLE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.FOLDABLE.<get-FOLDABLE>|<get-FOLDABLE>(){}[0]
|
||||
final const val NEXUS_10 // androidx.compose.ui.tooling.preview/Devices.NEXUS_10|{}NEXUS_10[0]
|
||||
final fun <get-NEXUS_10>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_10.<get-NEXUS_10>|<get-NEXUS_10>(){}[0]
|
||||
final const val NEXUS_5 // androidx.compose.ui.tooling.preview/Devices.NEXUS_5|{}NEXUS_5[0]
|
||||
final fun <get-NEXUS_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5.<get-NEXUS_5>|<get-NEXUS_5>(){}[0]
|
||||
final const val NEXUS_5X // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X|{}NEXUS_5X[0]
|
||||
final fun <get-NEXUS_5X>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_5X.<get-NEXUS_5X>|<get-NEXUS_5X>(){}[0]
|
||||
final const val NEXUS_6 // androidx.compose.ui.tooling.preview/Devices.NEXUS_6|{}NEXUS_6[0]
|
||||
final fun <get-NEXUS_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6.<get-NEXUS_6>|<get-NEXUS_6>(){}[0]
|
||||
final const val NEXUS_6P // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P|{}NEXUS_6P[0]
|
||||
final fun <get-NEXUS_6P>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_6P.<get-NEXUS_6P>|<get-NEXUS_6P>(){}[0]
|
||||
final const val NEXUS_7 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7|{}NEXUS_7[0]
|
||||
final fun <get-NEXUS_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7.<get-NEXUS_7>|<get-NEXUS_7>(){}[0]
|
||||
final const val NEXUS_7_2013 // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013|{}NEXUS_7_2013[0]
|
||||
final fun <get-NEXUS_7_2013>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_7_2013.<get-NEXUS_7_2013>|<get-NEXUS_7_2013>(){}[0]
|
||||
final const val NEXUS_9 // androidx.compose.ui.tooling.preview/Devices.NEXUS_9|{}NEXUS_9[0]
|
||||
final fun <get-NEXUS_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.NEXUS_9.<get-NEXUS_9>|<get-NEXUS_9>(){}[0]
|
||||
final const val PHONE // androidx.compose.ui.tooling.preview/Devices.PHONE|{}PHONE[0]
|
||||
final fun <get-PHONE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PHONE.<get-PHONE>|<get-PHONE>(){}[0]
|
||||
final const val PIXEL // androidx.compose.ui.tooling.preview/Devices.PIXEL|{}PIXEL[0]
|
||||
final fun <get-PIXEL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL.<get-PIXEL>|<get-PIXEL>(){}[0]
|
||||
final const val PIXEL_2 // androidx.compose.ui.tooling.preview/Devices.PIXEL_2|{}PIXEL_2[0]
|
||||
final fun <get-PIXEL_2>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2.<get-PIXEL_2>|<get-PIXEL_2>(){}[0]
|
||||
final const val PIXEL_2_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL|{}PIXEL_2_XL[0]
|
||||
final fun <get-PIXEL_2_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_2_XL.<get-PIXEL_2_XL>|<get-PIXEL_2_XL>(){}[0]
|
||||
final const val PIXEL_3 // androidx.compose.ui.tooling.preview/Devices.PIXEL_3|{}PIXEL_3[0]
|
||||
final fun <get-PIXEL_3>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3.<get-PIXEL_3>|<get-PIXEL_3>(){}[0]
|
||||
final const val PIXEL_3A // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A|{}PIXEL_3A[0]
|
||||
final fun <get-PIXEL_3A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A.<get-PIXEL_3A>|<get-PIXEL_3A>(){}[0]
|
||||
final const val PIXEL_3A_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL|{}PIXEL_3A_XL[0]
|
||||
final fun <get-PIXEL_3A_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3A_XL.<get-PIXEL_3A_XL>|<get-PIXEL_3A_XL>(){}[0]
|
||||
final const val PIXEL_3_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL|{}PIXEL_3_XL[0]
|
||||
final fun <get-PIXEL_3_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_3_XL.<get-PIXEL_3_XL>|<get-PIXEL_3_XL>(){}[0]
|
||||
final const val PIXEL_4 // androidx.compose.ui.tooling.preview/Devices.PIXEL_4|{}PIXEL_4[0]
|
||||
final fun <get-PIXEL_4>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4.<get-PIXEL_4>|<get-PIXEL_4>(){}[0]
|
||||
final const val PIXEL_4A // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A|{}PIXEL_4A[0]
|
||||
final fun <get-PIXEL_4A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4A.<get-PIXEL_4A>|<get-PIXEL_4A>(){}[0]
|
||||
final const val PIXEL_4_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL|{}PIXEL_4_XL[0]
|
||||
final fun <get-PIXEL_4_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_4_XL.<get-PIXEL_4_XL>|<get-PIXEL_4_XL>(){}[0]
|
||||
final const val PIXEL_5 // androidx.compose.ui.tooling.preview/Devices.PIXEL_5|{}PIXEL_5[0]
|
||||
final fun <get-PIXEL_5>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_5.<get-PIXEL_5>|<get-PIXEL_5>(){}[0]
|
||||
final const val PIXEL_6 // androidx.compose.ui.tooling.preview/Devices.PIXEL_6|{}PIXEL_6[0]
|
||||
final fun <get-PIXEL_6>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6.<get-PIXEL_6>|<get-PIXEL_6>(){}[0]
|
||||
final const val PIXEL_6A // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A|{}PIXEL_6A[0]
|
||||
final fun <get-PIXEL_6A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6A.<get-PIXEL_6A>|<get-PIXEL_6A>(){}[0]
|
||||
final const val PIXEL_6_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO|{}PIXEL_6_PRO[0]
|
||||
final fun <get-PIXEL_6_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_6_PRO.<get-PIXEL_6_PRO>|<get-PIXEL_6_PRO>(){}[0]
|
||||
final const val PIXEL_7 // androidx.compose.ui.tooling.preview/Devices.PIXEL_7|{}PIXEL_7[0]
|
||||
final fun <get-PIXEL_7>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7.<get-PIXEL_7>|<get-PIXEL_7>(){}[0]
|
||||
final const val PIXEL_7A // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A|{}PIXEL_7A[0]
|
||||
final fun <get-PIXEL_7A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7A.<get-PIXEL_7A>|<get-PIXEL_7A>(){}[0]
|
||||
final const val PIXEL_7_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO|{}PIXEL_7_PRO[0]
|
||||
final fun <get-PIXEL_7_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_7_PRO.<get-PIXEL_7_PRO>|<get-PIXEL_7_PRO>(){}[0]
|
||||
final const val PIXEL_8 // androidx.compose.ui.tooling.preview/Devices.PIXEL_8|{}PIXEL_8[0]
|
||||
final fun <get-PIXEL_8>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8.<get-PIXEL_8>|<get-PIXEL_8>(){}[0]
|
||||
final const val PIXEL_8A // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A|{}PIXEL_8A[0]
|
||||
final fun <get-PIXEL_8A>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8A.<get-PIXEL_8A>|<get-PIXEL_8A>(){}[0]
|
||||
final const val PIXEL_8_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO|{}PIXEL_8_PRO[0]
|
||||
final fun <get-PIXEL_8_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_8_PRO.<get-PIXEL_8_PRO>|<get-PIXEL_8_PRO>(){}[0]
|
||||
final const val PIXEL_9 // androidx.compose.ui.tooling.preview/Devices.PIXEL_9|{}PIXEL_9[0]
|
||||
final fun <get-PIXEL_9>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9.<get-PIXEL_9>|<get-PIXEL_9>(){}[0]
|
||||
final const val PIXEL_9_PRO // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO|{}PIXEL_9_PRO[0]
|
||||
final fun <get-PIXEL_9_PRO>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO.<get-PIXEL_9_PRO>|<get-PIXEL_9_PRO>(){}[0]
|
||||
final const val PIXEL_9_PRO_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD|{}PIXEL_9_PRO_FOLD[0]
|
||||
final fun <get-PIXEL_9_PRO_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_FOLD.<get-PIXEL_9_PRO_FOLD>|<get-PIXEL_9_PRO_FOLD>(){}[0]
|
||||
final const val PIXEL_9_PRO_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL|{}PIXEL_9_PRO_XL[0]
|
||||
final fun <get-PIXEL_9_PRO_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_9_PRO_XL.<get-PIXEL_9_PRO_XL>|<get-PIXEL_9_PRO_XL>(){}[0]
|
||||
final const val PIXEL_C // androidx.compose.ui.tooling.preview/Devices.PIXEL_C|{}PIXEL_C[0]
|
||||
final fun <get-PIXEL_C>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_C.<get-PIXEL_C>|<get-PIXEL_C>(){}[0]
|
||||
final const val PIXEL_FOLD // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD|{}PIXEL_FOLD[0]
|
||||
final fun <get-PIXEL_FOLD>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_FOLD.<get-PIXEL_FOLD>|<get-PIXEL_FOLD>(){}[0]
|
||||
final const val PIXEL_TABLET // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET|{}PIXEL_TABLET[0]
|
||||
final fun <get-PIXEL_TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_TABLET.<get-PIXEL_TABLET>|<get-PIXEL_TABLET>(){}[0]
|
||||
final const val PIXEL_XL // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL|{}PIXEL_XL[0]
|
||||
final fun <get-PIXEL_XL>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.PIXEL_XL.<get-PIXEL_XL>|<get-PIXEL_XL>(){}[0]
|
||||
final const val TABLET // androidx.compose.ui.tooling.preview/Devices.TABLET|{}TABLET[0]
|
||||
final fun <get-TABLET>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TABLET.<get-TABLET>|<get-TABLET>(){}[0]
|
||||
final const val TV_1080p // androidx.compose.ui.tooling.preview/Devices.TV_1080p|{}TV_1080p[0]
|
||||
final fun <get-TV_1080p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_1080p.<get-TV_1080p>|<get-TV_1080p>(){}[0]
|
||||
final const val TV_720p // androidx.compose.ui.tooling.preview/Devices.TV_720p|{}TV_720p[0]
|
||||
final fun <get-TV_720p>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.TV_720p.<get-TV_720p>|<get-TV_720p>(){}[0]
|
||||
final const val WEAR_OS_LARGE_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND|{}WEAR_OS_LARGE_ROUND[0]
|
||||
final fun <get-WEAR_OS_LARGE_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_LARGE_ROUND.<get-WEAR_OS_LARGE_ROUND>|<get-WEAR_OS_LARGE_ROUND>(){}[0]
|
||||
final const val WEAR_OS_RECT // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT|{}WEAR_OS_RECT[0]
|
||||
final fun <get-WEAR_OS_RECT>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_RECT.<get-WEAR_OS_RECT>|<get-WEAR_OS_RECT>(){}[0]
|
||||
final const val WEAR_OS_SMALL_ROUND // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND|{}WEAR_OS_SMALL_ROUND[0]
|
||||
final fun <get-WEAR_OS_SMALL_ROUND>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SMALL_ROUND.<get-WEAR_OS_SMALL_ROUND>|<get-WEAR_OS_SMALL_ROUND>(){}[0]
|
||||
final const val WEAR_OS_SQUARE // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE|{}WEAR_OS_SQUARE[0]
|
||||
final fun <get-WEAR_OS_SQUARE>(): kotlin/String // androidx.compose.ui.tooling.preview/Devices.WEAR_OS_SQUARE.<get-WEAR_OS_SQUARE>|<get-WEAR_OS_SQUARE>(){}[0]
|
||||
}
|
||||
|
||||
final object androidx.compose.ui.tooling.preview/Wallpapers { // androidx.compose.ui.tooling.preview/Wallpapers|null[0]
|
||||
final const val BLUE_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE|{}BLUE_DOMINATED_EXAMPLE[0]
|
||||
final fun <get-BLUE_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.BLUE_DOMINATED_EXAMPLE.<get-BLUE_DOMINATED_EXAMPLE>|<get-BLUE_DOMINATED_EXAMPLE>(){}[0]
|
||||
final const val GREEN_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE|{}GREEN_DOMINATED_EXAMPLE[0]
|
||||
final fun <get-GREEN_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.GREEN_DOMINATED_EXAMPLE.<get-GREEN_DOMINATED_EXAMPLE>|<get-GREEN_DOMINATED_EXAMPLE>(){}[0]
|
||||
final const val NONE // androidx.compose.ui.tooling.preview/Wallpapers.NONE|{}NONE[0]
|
||||
final fun <get-NONE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.NONE.<get-NONE>|<get-NONE>(){}[0]
|
||||
final const val RED_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE|{}RED_DOMINATED_EXAMPLE[0]
|
||||
final fun <get-RED_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.RED_DOMINATED_EXAMPLE.<get-RED_DOMINATED_EXAMPLE>|<get-RED_DOMINATED_EXAMPLE>(){}[0]
|
||||
final const val YELLOW_DOMINATED_EXAMPLE // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE|{}YELLOW_DOMINATED_EXAMPLE[0]
|
||||
final fun <get-YELLOW_DOMINATED_EXAMPLE>(): kotlin/Int // androidx.compose.ui.tooling.preview/Wallpapers.YELLOW_DOMINATED_EXAMPLE.<get-YELLOW_DOMINATED_EXAMPLE>|<get-YELLOW_DOMINATED_EXAMPLE>(){}[0]
|
||||
}
|
||||
|
||||
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop[0]
|
||||
final val androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop|#static{}androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop[0]
|
||||
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop|#static{}androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop[0]
|
||||
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop|#static{}androidx_compose_ui_tooling_preview_Devices$stableprop[0]
|
||||
final val androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop|#static{}androidx_compose_ui_tooling_preview_Wallpapers$stableprop[0]
|
||||
|
||||
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_CollectionPreviewParameterProvider$stableprop_getter(){}[0]
|
||||
final fun androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview.datasource/androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter|androidx_compose_ui_tooling_preview_datasource_LoremIpsum$stableprop_getter(){}[0]
|
||||
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter|androidx_compose_ui_tooling_preview_AndroidUiModes$stableprop_getter(){}[0]
|
||||
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Devices$stableprop_getter|androidx_compose_ui_tooling_preview_Devices$stableprop_getter(){}[0]
|
||||
final fun androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(): kotlin/Int // androidx.compose.ui.tooling.preview/androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter|androidx_compose_ui_tooling_preview_Wallpapers$stableprop_getter(){}[0]
|
||||
@@ -0,0 +1,34 @@
|
||||
// :ui-tooling-preview — androidx.compose.ui.tooling.preview.* (the common
|
||||
// @Preview annotation + PreviewParameterProvider), vendored VERBATIM from
|
||||
// upstream (compose/ui/ui-tooling-preview). The Maven artifact ships no
|
||||
// mingwX64/linux klibs — apps declare the OFFICIAL coords in commonMain and
|
||||
// the root bridge substitutes this module on native configurations. The
|
||||
// annotations are IDE-only metadata: previews render through the app's JVM
|
||||
// target against upstream Compose Desktop.
|
||||
// Never hand-edit src/vendor/ — change the manifest + `python scripts/compose-fork/sync.py compose/ui/ui-tooling-preview`.
|
||||
// Publication artifactId: ui-tooling-preview.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.plugin.compose)
|
||||
}
|
||||
|
||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||
|
||||
kotlin {
|
||||
linuxArm64(); linuxX64(); macosArm64(); if (vHostSupportsMingw) mingwX64()
|
||||
applyDefaultHierarchyTemplate()
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api("org.jetbrains.compose.runtime:runtime:1.11.1")
|
||||
}
|
||||
kotlin.srcDir("src/vendor/common/kotlin")
|
||||
}
|
||||
nativeMain { kotlin.srcDir("src/vendor/native/kotlin") }
|
||||
}
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexpect-actual-classes",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
SET_REPO=https://github.com/JetBrains/compose-multiplatform-core@<COMPOSE_CORE_REF>
|
||||
SET_FOLDER=compose/ui/ui-tooling-preview/src
|
||||
|
||||
commonMain/kotlin/ -> src/vendor/common/kotlin/
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/AndroidUiMode.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Device.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/MultiPreviews.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/PreviewWrapper.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
|
||||
# | commonMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt -> src/vendor/common/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.kt
|
||||
|
||||
nonJvmMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.nonJvm.kt -> src/vendor/native/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.native.kt
|
||||
|
||||
# >>> ---- DIAGNOSTIC GAPS ---- <<<<
|
||||
# desktopMain (1)
|
||||
# | desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt -> src/vendor/desktop/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt
|
||||
# jvmAndAndroidMain (1)
|
||||
# | jvmAndAndroidMain/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt -> src/vendor/jvmAndAndroid/kotlin/androidx/compose/ui/tooling/preview/internal/JvmDefaultWithCompatibility.jvmAndAndroid.kt
|
||||
# <<< ---- DIAGNOSTIC GAPS ---- >>>>
|
||||
@@ -20,8 +20,13 @@ linkerOpts.osx = \
|
||||
-framework QuartzCore -weak_framework CoreHaptics \
|
||||
-lpthread -lm
|
||||
|
||||
linkerOpts.linux_x64 = -lpthread -ldl -lm -lrt
|
||||
linkerOpts.linux_arm64 = -lpthread -ldl -lm -lrt
|
||||
# The -L multi-arch dirs + fontconfig/GL/X11 serve the SKIA renderer (font
|
||||
# matching, GL backend, windowing) — K/N's LLD sysroot doesn't include the
|
||||
# host's multi-arch dir, and carrying them here means apps (including bridge
|
||||
# consumers) need NO linker flags of their own. Unused under -Prenderer=sdl3;
|
||||
# the extra -l is harmless, and LLD skips missing -L dirs.
|
||||
linkerOpts.linux_x64 = -L/usr/lib/x86_64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
|
||||
linkerOpts.linux_arm64 = -L/usr/lib/aarch64-linux-gnu -lpthread -ldl -lm -lrt -lfontconfig -lGL -lX11
|
||||
|
||||
linkerOpts.mingw_x64 = \
|
||||
-lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 \
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.compose.sdl
|
||||
|
||||
// ==================
|
||||
// MARK: Renderer entry points (expect)
|
||||
// ==================
|
||||
|
||||
/* The renderer seam :window builds against. Each renderer source set
|
||||
(skikoRendererMain / sdlRendererMain) supplies the actuals; exactly one of
|
||||
the two is attached to any given target, so resolution stays unambiguous.
|
||||
|
||||
These were originally declared identically in both renderer source sets
|
||||
with NO expect — fine per-target, but shared nativeMain METADATA (=: what
|
||||
:window's KotlinMultiplatform publication compiles against) could not see
|
||||
them on a host where the attached targets span BOTH renderers (Windows:
|
||||
skikoRenderer for macos/linux + sdlRenderer for mingw). The expect makes
|
||||
nativeMain metadata self-contained, which lets the WINDOWS publish job —
|
||||
the only host that declares every target — produce the root modules. */
|
||||
|
||||
/* Create the render backend for the selected [GpuMode]; null when the
|
||||
backend can't initialise (caller falls back / reports). */
|
||||
expect fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend?
|
||||
|
||||
/* The renderer module's per-OS default GpuMode (used for GpuMode.Auto). */
|
||||
expect fun rendererPreferredGpuMode(): GpuMode
|
||||
@@ -7,18 +7,22 @@ import androidx.compose.ui.graphics.ImageBitmap
|
||||
// ==================
|
||||
|
||||
/* Decodes an ENCODED image (png / jpg / webp / bmp / gif / svg bytes) into a
|
||||
drawable [ImageBitmap]. The active renderer registers its implementation —
|
||||
the SDL backend goes through SDL3_image (see Sdl3EncodedImageDecoder); the
|
||||
Skia renderer never registers because its resources pipeline decodes through
|
||||
Skia directly (vendored skikoRenderer actuals in :components-resources).
|
||||
drawable [ImageBitmap]. The active renderer registers its implementation at
|
||||
backend construction — the SDL backend goes through SDL3_image
|
||||
(Sdl3EncodedImageDecoder), the Skia backend through Image.makeFromEncoded /
|
||||
SVGDOM (SkiaEncodedImageDecoder). :components-resources' actuals share this
|
||||
hook under BOTH renderers (its skikoRendererMain reuses the SDL actuals).
|
||||
|
||||
Consumed by :components-resources' SDL actuals (`ByteArray.toImageBitmap`,
|
||||
`SvgElement.toSvgPainter`) so painterResource(Res.drawable.x) works without
|
||||
that module reaching into renderer internals. */
|
||||
Consumed by `ByteArray.toImageBitmap` / `SvgElement.toSvgPainter` so
|
||||
painterResource(Res.drawable.x) works without that module reaching into
|
||||
renderer internals. */
|
||||
interface EncodedImageDecoder {
|
||||
fun decode(inBytes: ByteArray): ImageBitmap?
|
||||
}
|
||||
|
||||
/* Volatile: written by the render backend on the main thread, read by the
|
||||
resources pipeline on Dispatchers.Default workers. */
|
||||
@kotlin.concurrent.Volatile
|
||||
var encodedImageDecoder: EncodedImageDecoder? = null
|
||||
internal set
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.compose.sdl.graphics
|
||||
|
||||
import kotlinx.atomicfu.locks.SynchronizedObject
|
||||
import kotlinx.atomicfu.locks.synchronized
|
||||
|
||||
// ==================
|
||||
// MARK: NativeReleaseQueue — main-thread-deferred native resource disposal
|
||||
// ==================
|
||||
|
||||
/**
|
||||
* A queue of native-resource release actions drained on the MAIN THREAD once
|
||||
* per frame by the window loop (see ComposeWindow). It exists because the two
|
||||
* ways a renderer resource dies are both off the main thread, while the calls
|
||||
* that free them (SDL_DestroyTexture, and any renderer API) are main-thread-only:
|
||||
*
|
||||
* - a Kotlin/Native `Cleaner` runs on a GC worker thread, and
|
||||
* - explicit `close()` can be reached from composition/effect code on other
|
||||
* dispatchers.
|
||||
*
|
||||
* Both simply ENQUEUE here; the main loop runs the actions between frames. This
|
||||
* turns the GC (issue #2: Cleaner-managed native memory starving because the
|
||||
* quiet Kotlin heap rarely triggers a collection) from the disposal MECHANISM
|
||||
* into a mere safeguard — ownership closes resources promptly, the Cleaner is
|
||||
* the backstop for whatever leaks a `close()`.
|
||||
*
|
||||
* Actions must be idempotent-safe at the source (guard against a resource being
|
||||
* both explicitly closed and later GC-cleaned — see SdlImageBitmap's holder).
|
||||
*/
|
||||
object NativeReleaseQueue {
|
||||
|
||||
private val fLock = SynchronizedObject()
|
||||
private var fPending = ArrayList<() -> Unit>()
|
||||
|
||||
/** Enqueue a release action. Safe to call from any thread. */
|
||||
fun enqueue(action: () -> Unit) {
|
||||
synchronized(fLock) { fPending.add(action) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Run and clear every queued action. MAIN THREAD ONLY — the actions call
|
||||
* renderer APIs that aren't thread-safe. Returns the number drained.
|
||||
*/
|
||||
fun drain(): Int {
|
||||
val vBatch = synchronized(fLock) {
|
||||
if (fPending.isEmpty()) return 0
|
||||
val vTaken = fPending
|
||||
fPending = ArrayList()
|
||||
vTaken
|
||||
}
|
||||
for (vAction in vBatch) {
|
||||
runCatching { vAction() }
|
||||
}
|
||||
return vBatch.size
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import kotlin.experimental.ExperimentalNativeApi
|
||||
// MARK: SDL3 renderer per-OS default
|
||||
// ==================
|
||||
|
||||
fun rendererPreferredGpuMode(): GpuMode {
|
||||
actual fun rendererPreferredGpuMode(): GpuMode {
|
||||
@OptIn(ExperimentalNativeApi::class)
|
||||
return when (Platform.osFamily) {
|
||||
OsFamily.MACOSX -> GpuMode.Sdl3.Metal
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import com.compose.sdl.renderer.sdl.Sdl3RenderBackend
|
||||
renderer modules expose createRenderBackend / rendererPreferredGpuMode
|
||||
with identical signatures in this package, and the build includes exactly
|
||||
one of them per target. Rejects Skia.* since this module has no Skiko. */
|
||||
fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
||||
actual fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
||||
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||
if (vResolved is GpuMode.Skia) {
|
||||
error("$vResolved isn't available in this build — Skiko isn't linked. " +
|
||||
|
||||
+316
-36
@@ -1,5 +1,6 @@
|
||||
package com.compose.sdl.renderer.sdl
|
||||
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
@@ -26,6 +27,7 @@ import com.compose.sdl.icons.IconFont
|
||||
import kotlinx.cinterop.*
|
||||
import sdl3.*
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -126,7 +128,7 @@ internal class Sdl3Canvas(
|
||||
// offscreen clip opened inside this save frame (see fClipLayers below).
|
||||
private data class State(
|
||||
val a: Float, val b: Float, val c: Float, val d: Float, val e: Float, val f: Float,
|
||||
val clip: IntArray?, val alpha: Float, val clipLayers: Int,
|
||||
val clip: IntArray?, val alpha: Float, val clipLayers: Int, val pendingClips: Int,
|
||||
)
|
||||
private val fStack = ArrayDeque<State>()
|
||||
|
||||
@@ -146,9 +148,138 @@ internal class Sdl3Canvas(
|
||||
)
|
||||
private val fClipLayers = ArrayDeque<OffscreenClip>()
|
||||
|
||||
// ============
|
||||
// Lazy rounded clips. Opening an offscreen mask per clip is the SINGLE
|
||||
// most expensive thing this canvas does (two render-target switches +
|
||||
// clear + corner cut + feather + composite) - and the dominant Compose
|
||||
// idiom `Modifier.clip(CircleShape).background(...)` never needs one:
|
||||
// the background is a full-cover rect drawable AS the rounded shape, and
|
||||
// content wholly inside the rounded region needs no mask at all. So
|
||||
// clipRoundRect only RECORDS the clip; each draw either proves itself
|
||||
// safe (containment / cover) or realizes the offscreen on demand.
|
||||
|
||||
private class PendingRoundClip(
|
||||
val deviceRound: RoundRect, // device-space outline (clamped radii)
|
||||
val localRound: RoundRect, // as passed in (for cover -> shape-fill emission)
|
||||
val region: IntArray, // fClip intersect bbox at push time
|
||||
val bbox: IntArray, // device bbox of the outline
|
||||
val prevClip: IntArray?, // fClip before this clip was pushed
|
||||
// Affine snapshot at push - cover -> fill re-emits in LOCAL space and
|
||||
// must bail if the transform moved since.
|
||||
val ma: Float, val mb: Float, val mc: Float, val md: Float, val me: Float, val mf: Float,
|
||||
// fStack.size when the clip was pushed: a lazily-realized mask must be
|
||||
// composited by the save-frame that OWNS the clip, not by whatever
|
||||
// inner frame happened to trigger realization (a press draws the state
|
||||
// layer and the ripple in separate inner clipRect{} frames - the first
|
||||
// realize must not let the inner restore close the mask, or the second
|
||||
// draw runs unmasked: the "square on click" artifact).
|
||||
val saveDepth: Int,
|
||||
)
|
||||
private val fPendingClips = ArrayDeque<PendingRoundClip>()
|
||||
|
||||
/* Point-in-round-rect (device space) - inside the rect and, within a
|
||||
corner cut square, inside its ellipse. */
|
||||
private fun roundRectContains(inRR: RoundRect, inX: Float, inY: Float): Boolean {
|
||||
if (inX < inRR.left || inX > inRR.right || inY < inRR.top || inY > inRR.bottom) return false
|
||||
fun corner(inRx: Float, inRy: Float, inDx: Float, inDy: Float): Boolean {
|
||||
if (inRx <= 0f || inRy <= 0f) return true
|
||||
val vNx = inDx / inRx; val vNy = inDy / inRy
|
||||
return vNx * vNx + vNy * vNy <= 1f
|
||||
}
|
||||
val vTl = inRR.topLeftCornerRadius; val vTr = inRR.topRightCornerRadius
|
||||
val vBr = inRR.bottomRightCornerRadius; val vBl = inRR.bottomLeftCornerRadius
|
||||
if (inX < inRR.left + vTl.x && inY < inRR.top + vTl.y)
|
||||
return corner(vTl.x, vTl.y, inX - (inRR.left + vTl.x), inY - (inRR.top + vTl.y))
|
||||
if (inX > inRR.right - vTr.x && inY < inRR.top + vTr.y)
|
||||
return corner(vTr.x, vTr.y, inX - (inRR.right - vTr.x), inY - (inRR.top + vTr.y))
|
||||
if (inX > inRR.right - vBr.x && inY > inRR.bottom - vBr.y)
|
||||
return corner(vBr.x, vBr.y, inX - (inRR.right - vBr.x), inY - (inRR.bottom - vBr.y))
|
||||
if (inX < inRR.left + vBl.x && inY > inRR.bottom - vBl.y)
|
||||
return corner(vBl.x, vBl.y, inX - (inRR.left + vBl.x), inY - (inRR.bottom - vBl.y))
|
||||
return true
|
||||
}
|
||||
|
||||
/* A device-space aabb is inside the round rect iff its four corners are. */
|
||||
private fun roundRectContainsAabb(inRR: RoundRect, inL: Float, inT: Float, inR: Float, inB: Float): Boolean =
|
||||
roundRectContains(inRR, inL, inT) && roundRectContains(inRR, inR, inT) &&
|
||||
roundRectContains(inRR, inR, inB) && roundRectContains(inRR, inL, inB)
|
||||
|
||||
/* Device aabb of a LOCAL rect under the current affine. */
|
||||
private fun deviceAabb(inL: Float, inT: Float, inR: Float, inB: Float): FloatArray {
|
||||
val vX0 = mapX(inL, inT); val vX1 = mapX(inR, inT); val vX2 = mapX(inR, inB); val vX3 = mapX(inL, inB)
|
||||
val vY0 = mapY(inL, inT); val vY1 = mapY(inR, inT); val vY2 = mapY(inR, inB); val vY3 = mapY(inL, inB)
|
||||
return floatArrayOf(
|
||||
minOf(vX0, vX1, vX2, vX3), minOf(vY0, vY1, vY2, vY3),
|
||||
maxOf(vX0, vX1, vX2, vX3), maxOf(vY0, vY1, vY2, vY3),
|
||||
)
|
||||
}
|
||||
|
||||
/* If the LOCAL-space aabb stays inside every pending rounded clip, drawing
|
||||
it under the already-active rect clip is correct - no mask needed.
|
||||
Otherwise realize the pending masks before the draw proceeds. */
|
||||
private fun admitDraw(inL: Float, inT: Float, inR: Float, inB: Float) {
|
||||
if (fPendingClips.isEmpty()) return
|
||||
val vBox = deviceAabb(inL, inT, inR, inB)
|
||||
for (vPending in fPendingClips) {
|
||||
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
|
||||
realizePendingClips()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Convert pending clips into real offscreen mask layers (outermost first).
|
||||
|
||||
Each clip realizes with its OWN push-time region (pending.region =
|
||||
enclosing-clip-at-push ∩ bbox), NOT the current fClip: a lazily-realized
|
||||
clip may fire arbitrarily deep inside descendants whose own clips have
|
||||
since narrowed fClip. Re-intersecting with that narrow clip shrank an
|
||||
outer clip's offscreen to the innermost descendant's rect, so its
|
||||
composite blitted back only that sliver and every sibling drawn into the
|
||||
same offscreen was discarded (the TLS-chain regression: only the first
|
||||
card + nothing below it, until a scroll shifted which card triggered
|
||||
realization). The real (already-narrowed) drawing clip is preserved
|
||||
across the loop and restored at the end, so subsequent draws into the
|
||||
innermost target stay correctly clipped. */
|
||||
private fun realizePendingClips() {
|
||||
if (fPendingClips.isEmpty()) return
|
||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||
val vDrawClip = fClip
|
||||
while (fPendingClips.isNotEmpty()) {
|
||||
val vPending = fPendingClips.removeFirst()
|
||||
fScope.flush()
|
||||
val vRegion = vPending.region
|
||||
val vTarget = fClipTargets?.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt())
|
||||
if (vTarget == null || vRegion[2] <= vRegion[0] || vRegion[3] <= vRegion[1]) {
|
||||
// Degrade to the rect clip that is already active.
|
||||
continue
|
||||
}
|
||||
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
|
||||
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
|
||||
fClip = vRegion
|
||||
applyClip()
|
||||
clearRegion(vRegion)
|
||||
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPending.prevClip, vRegion, vPending.bbox, vPending.deviceRound))
|
||||
// Attribute the realized mask to the save-frame that pushed the
|
||||
// clip: inner frames entered since must NOT composite it on their
|
||||
// restore - bump their thresholds past the new layer.
|
||||
for (vI in vPending.saveDepth until fStack.size) {
|
||||
val vState = fStack[vI]
|
||||
if (vState.clipLayers < fClipLayers.size) {
|
||||
fStack[vI] = vState.copy(clipLayers = fClipLayers.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Restore the real drawing clip (already narrowed by inner clipRects /
|
||||
// scroll bounds) for draws into the now-active innermost target.
|
||||
fClip = vDrawClip
|
||||
applyClip()
|
||||
}
|
||||
|
||||
// Flushes any pending batched geometry to SDL, then frees the scope's
|
||||
// native buffer + clears the SDL clip. Call once per frame after the draw.
|
||||
fun finish() {
|
||||
fPendingClips.clear()
|
||||
fScope.release()
|
||||
// Safety: an unbalanced save/restore must never leave the renderer pointed
|
||||
// at a scratch target when the frame is presented.
|
||||
@@ -166,11 +297,14 @@ internal class Sdl3Canvas(
|
||||
// Offscreen canvas: the outermost save (CanvasDrawScope brackets its draw with
|
||||
// save/restore) switches the SDL render target to the ImageBitmap's texture.
|
||||
if (fOffscreenTexture != null && fStack.isEmpty()) beginOffscreen()
|
||||
fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size))
|
||||
fStack.addLast(State(fMa, fMb, fMc, fMd, fMe, fMf, fClip, fAlpha, fClipLayers.size, fPendingClips.size))
|
||||
}
|
||||
|
||||
override fun restore() {
|
||||
val vPrev = fStack.removeLastOrNull() ?: return
|
||||
// Pending (never-realized) rounded clips opened inside this save frame:
|
||||
// nothing was drawn through an offscreen for them - just drop them.
|
||||
while (fPendingClips.size > vPrev.pendingClips) fPendingClips.removeLast()
|
||||
// Composite + pop any offscreen rounded clips opened inside this save frame
|
||||
// before restoring the plain state (each composite restores render target
|
||||
// and SDL clip to what it was when the clip was opened).
|
||||
@@ -267,6 +401,7 @@ internal class Sdl3Canvas(
|
||||
// treating Difference as intersect clipped the border TO the notch instead
|
||||
// (a floating line under the label, no outline anywhere else).
|
||||
private fun clipRectDifference(inL: Float, inT: Float, inR: Float, inB: Float) {
|
||||
realizePendingClips()
|
||||
val vDiff = mapRectAABB(inL, inT, inR, inB)
|
||||
val vRegion = fClip ?: intArrayOf(0, 0, fSize.width.toInt(), fSize.height.toInt())
|
||||
if (fClipTargets == null || fSize.width < 1f || fSize.height < 1f ||
|
||||
@@ -330,11 +465,19 @@ internal class Sdl3Canvas(
|
||||
// arbitrary paths keep the clipPath bbox fallback.
|
||||
|
||||
override fun clipRoundRect(inRoundRect: RoundRect) {
|
||||
// The mask is cut in DEVICE space (the bbox below goes through the
|
||||
// affine), but the corner radii arrive in LOCAL user space — scale
|
||||
// them by the affine too, or a graphicsLayer-scaled circle clip cuts
|
||||
// with oversized corners and degenerates toward a lozenge (seen as
|
||||
// diamond-shaped "popped" bubbles under scale 0.55). Column norms
|
||||
// approximate per-axis scale; rotation already degrades to AABBs here.
|
||||
val vScaleX = sqrt(fMa * fMa + fMb * fMb)
|
||||
val vScaleY = sqrt(fMc * fMc + fMd * fMd)
|
||||
val vMaxRadius = maxOf(
|
||||
inRoundRect.topLeftCornerRadius.x, inRoundRect.topLeftCornerRadius.y,
|
||||
inRoundRect.topRightCornerRadius.x, inRoundRect.topRightCornerRadius.y,
|
||||
inRoundRect.bottomRightCornerRadius.x, inRoundRect.bottomRightCornerRadius.y,
|
||||
inRoundRect.bottomLeftCornerRadius.x, inRoundRect.bottomLeftCornerRadius.y,
|
||||
inRoundRect.topLeftCornerRadius.x * vScaleX, inRoundRect.topLeftCornerRadius.y * vScaleY,
|
||||
inRoundRect.topRightCornerRadius.x * vScaleX, inRoundRect.topRightCornerRadius.y * vScaleY,
|
||||
inRoundRect.bottomRightCornerRadius.x * vScaleX, inRoundRect.bottomRightCornerRadius.y * vScaleY,
|
||||
inRoundRect.bottomLeftCornerRadius.x * vScaleX, inRoundRect.bottomLeftCornerRadius.y * vScaleY,
|
||||
)
|
||||
// Effectively-square corners or no offscreen pool → plain rectangular clip.
|
||||
if (vMaxRadius < 0.5f || fClipTargets == null || fSize.width < 1f || fSize.height < 1f) {
|
||||
@@ -343,6 +486,20 @@ internal class Sdl3Canvas(
|
||||
}
|
||||
fScope.flush()
|
||||
val vBbox = mapRectAABB(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
|
||||
// Clamp to the DEVICE half-box: the bbox is int-truncated, so a scaled
|
||||
// radius can exceed it by a fraction — opposite corner cuts then overlap
|
||||
// and leave flat spots at N/E/S/W (a circle read as an octagon).
|
||||
val vHalfW = (vBbox[2] - vBbox[0]) / 2f
|
||||
val vHalfH = (vBbox[3] - vBbox[1]) / 2f
|
||||
fun deviceRadius(inR: CornerRadius) =
|
||||
CornerRadius(minOf(inR.x * vScaleX, vHalfW), minOf(inR.y * vScaleY, vHalfH))
|
||||
val vDeviceRound = RoundRect(
|
||||
vBbox[0].toFloat(), vBbox[1].toFloat(), vBbox[2].toFloat(), vBbox[3].toFloat(),
|
||||
topLeftCornerRadius = deviceRadius(inRoundRect.topLeftCornerRadius),
|
||||
topRightCornerRadius = deviceRadius(inRoundRect.topRightCornerRadius),
|
||||
bottomRightCornerRadius = deviceRadius(inRoundRect.bottomRightCornerRadius),
|
||||
bottomLeftCornerRadius = deviceRadius(inRoundRect.bottomLeftCornerRadius),
|
||||
)
|
||||
val vRegion = intersect(fClip, vBbox)
|
||||
// Nothing visible: keep behaviour of a normal clip (cull) without an
|
||||
// offscreen pass. The enclosing save/restore restores the clip afterwards.
|
||||
@@ -351,20 +508,18 @@ internal class Sdl3Canvas(
|
||||
applyClip()
|
||||
return
|
||||
}
|
||||
val vTarget = fClipTargets.target(fClipLayers.size, fSize.width.toInt(), fSize.height.toInt())
|
||||
if (vTarget == null) {
|
||||
// Couldn't allocate a scratch target — degrade to a rectangular clip.
|
||||
clipRect(inRoundRect.left, inRoundRect.top, inRoundRect.right, inRoundRect.bottom)
|
||||
return
|
||||
}
|
||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||
val vPrevTarget = SDL_GetRenderTarget(vRenderer)
|
||||
// LAZY: record only — the rect part of the clip applies immediately,
|
||||
// the offscreen mask is opened by the first draw that actually needs
|
||||
// it (admitDraw / realizePendingClips). The common clip+background
|
||||
// idiom never does.
|
||||
val vPrevClip = fClip
|
||||
SDL_SetRenderTarget(vRenderer, vTarget.reinterpret())
|
||||
fClip = vRegion
|
||||
applyClip()
|
||||
clearRegion(vRegion)
|
||||
fClipLayers.addLast(OffscreenClip(vTarget, vPrevTarget, vPrevClip, vRegion, vBbox, inRoundRect))
|
||||
fPendingClips.addLast(PendingRoundClip(
|
||||
vDeviceRound, inRoundRect, vRegion, vBbox, vPrevClip,
|
||||
fMa, fMb, fMc, fMd, fMe, fMf,
|
||||
fStack.size,
|
||||
))
|
||||
}
|
||||
|
||||
// Pops the top offscreen clip: flush its subtree, cut the rounded corners out
|
||||
@@ -427,22 +582,24 @@ internal class Sdl3Canvas(
|
||||
private fun zeroRoundRectCorners(inBbox: IntArray, inRoundRect: RoundRect) {
|
||||
val vLeft = inBbox[0].toFloat(); val vTop = inBbox[1].toFloat()
|
||||
val vRight = inBbox[2].toFloat(); val vBottom = inBbox[3].toFloat()
|
||||
val vSeg = 12
|
||||
val vTl = inRoundRect.topLeftCornerRadius
|
||||
val vTr = inRoundRect.topRightCornerRadius
|
||||
val vBr = inRoundRect.bottomRightCornerRadius
|
||||
val vBl = inRoundRect.bottomLeftCornerRadius
|
||||
// Upper bound: 4 corners × vSeg triangles × 3 vertices. Written straight
|
||||
// into the native SDL_Vertex buffer — the old ArrayList<Float> staging
|
||||
// boxed ~300 floats per clip layer per frame.
|
||||
fun cornerCount(inR: androidx.compose.ui.geometry.CornerRadius): Int =
|
||||
if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3
|
||||
val vMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl)
|
||||
if (vMax == 0) return
|
||||
// Segments per quarter arc ~ every 4 device px along the arc, matching
|
||||
// Sdl3DrawScope's density (~64/full circle at typical radii).
|
||||
val vMaxR = maxOf(vTl.x, vTl.y, vTr.x, vTr.y, vBr.x, vBr.y, vBl.x, vBl.y)
|
||||
val vSeg = ((vMaxR * 1.5708f) / 4f).toInt().coerceIn(8, 32)
|
||||
fun cornerCount(inR: CornerRadius): Int = if (inR.x <= 0f || inR.y <= 0f) 0 else vSeg * 3
|
||||
val vFanMax = cornerCount(vTl) + cornerCount(vTr) + cornerCount(vBr) + cornerCount(vBl)
|
||||
if (vFanMax == 0) return
|
||||
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
|
||||
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
|
||||
memScoped {
|
||||
val vBuf = allocArray<SDL_Vertex>(vMax)
|
||||
// ============
|
||||
// Pass 1 — hard cut: zero everything outside each corner arc
|
||||
// (triangle fan from the corner apex, blend NONE writes RGBA 0).
|
||||
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_NONE)
|
||||
val vBuf = allocArray<SDL_Vertex>(vFanMax)
|
||||
var vIdx = 0
|
||||
fun put(inX: Float, inY: Float) {
|
||||
vBuf[vIdx].position.x = inX; vBuf[vIdx].position.y = inY
|
||||
@@ -470,6 +627,51 @@ internal class Sdl3Canvas(
|
||||
cutout(vRight, vBottom, vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
|
||||
cutout(vLeft, vBottom, vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
|
||||
if (vIdx > 0) SDL_RenderGeometry(vRenderer, null, vBuf, vIdx, null, 0)
|
||||
|
||||
// ============
|
||||
// Pass 2 — feathered edge: a ~1.2px ring band straddling each arc
|
||||
// whose vertex alpha ramps 1 → 0 outward, drawn with a custom
|
||||
// "dst *= srcAlpha" blend. SDL_RenderGeometry has no antialiasing;
|
||||
// without this the mask boundary is a hard polygon edge while the
|
||||
// drawn shapes get Sdl3DrawScope's fringe AA (visibly rougher —
|
||||
// scaled-down circle clips read as low-poly).
|
||||
val vMul = SDL_ComposeCustomBlendMode(
|
||||
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
|
||||
SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_SRC_ALPHA, SDL_BLENDOPERATION_ADD,
|
||||
)
|
||||
SDL_SetRenderDrawBlendMode(vRenderer, vMul)
|
||||
val vFeather = 1.2f
|
||||
val vBandMax = 4 * vSeg * 6
|
||||
val vBand = allocArray<SDL_Vertex>(vBandMax)
|
||||
var vBIdx = 0
|
||||
fun putBand(inX: Float, inY: Float, inKeep: Float) {
|
||||
vBand[vBIdx].position.x = inX; vBand[vBIdx].position.y = inY
|
||||
vBand[vBIdx].color.r = 1f; vBand[vBIdx].color.g = 1f; vBand[vBIdx].color.b = 1f; vBand[vBIdx].color.a = inKeep
|
||||
vBand[vBIdx].tex_coord.x = 0f; vBand[vBIdx].tex_coord.y = 0f
|
||||
vBIdx++
|
||||
}
|
||||
fun feather(inCx: Float, inCy: Float, inRx: Float, inRy: Float, inStartDeg: Float) {
|
||||
if (inRx <= 1f || inRy <= 1f) return
|
||||
var vPrevInX = 0f; var vPrevInY = 0f; var vPrevOutX = 0f; var vPrevOutY = 0f
|
||||
for (vI in 0..vSeg) {
|
||||
val vAngle = ((inStartDeg + 90f * vI / vSeg) * (PI / 180.0)).toFloat()
|
||||
val vCos = cos(vAngle); val vSin = sin(vAngle)
|
||||
val vInX = inCx + (inRx - vFeather) * vCos
|
||||
val vInY = inCy + (inRy - vFeather) * vSin
|
||||
val vOutX = inCx + inRx * vCos
|
||||
val vOutY = inCy + inRy * vSin
|
||||
if (vI > 0) {
|
||||
putBand(vPrevInX, vPrevInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vInX, vInY, 1f)
|
||||
putBand(vInX, vInY, 1f); putBand(vPrevOutX, vPrevOutY, 0f); putBand(vOutX, vOutY, 0f)
|
||||
}
|
||||
vPrevInX = vInX; vPrevInY = vInY; vPrevOutX = vOutX; vPrevOutY = vOutY
|
||||
}
|
||||
}
|
||||
feather(vLeft + vTl.x, vTop + vTl.y, vTl.x, vTl.y, 180f)
|
||||
feather(vRight - vTr.x, vTop + vTr.y, vTr.x, vTr.y, 270f)
|
||||
feather(vRight - vBr.x, vBottom - vBr.y, vBr.x, vBr.y, 0f)
|
||||
feather(vLeft + vBl.x, vBottom - vBl.y, vBl.x, vBl.y, 90f)
|
||||
if (vBIdx > 0) SDL_RenderGeometry(vRenderer, null, vBand, vBIdx, null, 0)
|
||||
}
|
||||
SDL_SetRenderDrawBlendMode(vRenderer, SDL_BLENDMODE_BLEND)
|
||||
}
|
||||
@@ -502,6 +704,7 @@ internal class Sdl3Canvas(
|
||||
inAmbientColor: androidx.compose.ui.graphics.Color,
|
||||
inSpotColor: androidx.compose.ui.graphics.Color,
|
||||
) {
|
||||
realizePendingClips()
|
||||
if (inElevationPx <= 0f) return
|
||||
|
||||
// Generic paths (CutCornerShape, GenericShape) don't 9-slice — blur the
|
||||
@@ -636,6 +839,7 @@ internal class Sdl3Canvas(
|
||||
// before re-rasterising a vector). Write transparent directly instead of the
|
||||
// paint colour.
|
||||
if (paint.blendMode == BlendMode.Clear) {
|
||||
realizePendingClips()
|
||||
fScope.flush()
|
||||
clearRegion(intArrayOf(
|
||||
mapX(left, top).toInt(), mapY(left, top).toInt(),
|
||||
@@ -643,21 +847,75 @@ internal class Sdl3Canvas(
|
||||
))
|
||||
return
|
||||
}
|
||||
if (fPendingClips.isNotEmpty() && tryDrawRectUnderPendingClips(left, top, right, bottom, paint)) return
|
||||
prep().rectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
/* The `clip(shape).background(color)` idiom: a FILL rect that fully covers
|
||||
the innermost pending clip becomes a direct rounded-shape fill (with the
|
||||
scope's fringe AA — no offscreen mask at all). A rect fully INSIDE every
|
||||
pending clip draws as-is. Returns true when the draw has been emitted. */
|
||||
private fun tryDrawRectUnderPendingClips(inL: Float, inT: Float, inR: Float, inB: Float, inPaint: Paint): Boolean {
|
||||
val vBox = deviceAabb(inL, inT, inR, inB)
|
||||
val vInner = fPendingClips.last()
|
||||
// Cover case: fill paint, no stroke, rect covers the whole clip outline,
|
||||
// affine unchanged since the clip was pushed (local-space re-emission),
|
||||
// uniform corner radii (the scope's roundRectCore takes one radius), and
|
||||
// the clip outline itself contained in every OUTER pending clip.
|
||||
val vLr = vInner.localRound
|
||||
val vUniform = vLr.topLeftCornerRadius.x == vLr.topLeftCornerRadius.y &&
|
||||
vLr.topLeftCornerRadius == vLr.topRightCornerRadius &&
|
||||
vLr.topLeftCornerRadius == vLr.bottomRightCornerRadius &&
|
||||
vLr.topLeftCornerRadius == vLr.bottomLeftCornerRadius
|
||||
val vAffineUnchanged = vInner.ma == fMa && vInner.mb == fMb && vInner.mc == fMc &&
|
||||
vInner.md == fMd && vInner.me == fMe && vInner.mf == fMf
|
||||
if (inPaint.style == PaintingStyle.Fill && inPaint.blendMode == BlendMode.SrcOver &&
|
||||
vUniform && vAffineUnchanged &&
|
||||
vBox[0] <= vInner.bbox[0] && vBox[1] <= vInner.bbox[1] &&
|
||||
vBox[2] >= vInner.bbox[2] && vBox[3] >= vInner.bbox[3]
|
||||
) {
|
||||
var vOutersContain = true
|
||||
for (vPending in fPendingClips) {
|
||||
if (vPending === vInner) continue
|
||||
if (!roundRectContainsAabb(vPending.deviceRound, vInner.deviceRound.left, vInner.deviceRound.top, vInner.deviceRound.right, vInner.deviceRound.bottom)) {
|
||||
vOutersContain = false; break
|
||||
}
|
||||
}
|
||||
if (vOutersContain) {
|
||||
prep().roundRectCore(
|
||||
brushFor(inPaint),
|
||||
Offset(vLr.left, vLr.top), Size(vLr.width, vLr.height),
|
||||
vLr.topLeftCornerRadius.x,
|
||||
(inPaint.alpha * fAlpha), Fill,
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Containment: inside every pending outline → the active rect clip is enough.
|
||||
for (vPending in fPendingClips) {
|
||||
if (!roundRectContainsAabb(vPending.deviceRound, vBox[0], vBox[1], vBox[2], vBox[3])) {
|
||||
realizePendingClips()
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun drawRoundRect(
|
||||
left: Float, top: Float, right: Float, bottom: Float,
|
||||
radiusX: Float, radiusY: Float, paint: Paint,
|
||||
) {
|
||||
admitDraw(left, top, right, bottom)
|
||||
prep().roundRectCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), radiusX, (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
override fun drawOval(left: Float, top: Float, right: Float, bottom: Float, paint: Paint) {
|
||||
admitDraw(left, top, right, bottom)
|
||||
prep().ovalCore(brushFor(paint), Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
override fun drawCircle(center: Offset, radius: Float, paint: Paint) {
|
||||
admitDraw(center.x - radius, center.y - radius, center.x + radius, center.y + radius)
|
||||
prep().circleCore(brushFor(paint), radius, center, (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
@@ -665,14 +923,17 @@ internal class Sdl3Canvas(
|
||||
left: Float, top: Float, right: Float, bottom: Float,
|
||||
startAngle: Float, sweepAngle: Float, useCenter: Boolean, paint: Paint,
|
||||
) {
|
||||
realizePendingClips()
|
||||
prep().arcCore(brushFor(paint), startAngle, sweepAngle, useCenter, Offset(left, top), Size(right - left, bottom - top), (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
override fun drawLine(p1: Offset, p2: Offset, paint: Paint) {
|
||||
realizePendingClips()
|
||||
prep().lineCore(brushFor(paint), p1, p2, paint.strokeWidth, paint.strokeCap, (paint.alpha * fAlpha))
|
||||
}
|
||||
|
||||
override fun drawPath(path: ComposePath, paint: Paint) {
|
||||
realizePendingClips()
|
||||
prep().pathCore(path, brushFor(paint), (paint.alpha * fAlpha), styleFor(paint))
|
||||
}
|
||||
|
||||
@@ -697,6 +958,11 @@ internal class Sdl3Canvas(
|
||||
inBaseItalic: Boolean,
|
||||
inTextDecoration: androidx.compose.ui.text.style.TextDecoration?,
|
||||
) {
|
||||
// Small centred labels (a popped bubble's "pop") usually sit fully
|
||||
// inside a rounded clip — gate on containment like other draws, with
|
||||
// a 2px margin for glyph overhang / AA bleed, instead of paying a
|
||||
// mask pass per text run.
|
||||
admitDraw(inX - 2f, inY - 2f, inX + inBoxWidth + 2f, inY + inBoxHeight + 2f)
|
||||
fScope.flush()
|
||||
// Paragraph-level decoration bits forwarded to every wrapped line.
|
||||
val vUnderline = inTextDecoration?.contains(androidx.compose.ui.text.style.TextDecoration.Underline) == true
|
||||
@@ -712,15 +978,26 @@ internal class Sdl3Canvas(
|
||||
// node's real box. The per-line path below centres within a lineHeight band
|
||||
// (1.2 em for Material Symbols), taller than the size-clamped icon node,
|
||||
// which pushed every icon ~0.1 em below centre.
|
||||
// Device-space scale for GLYPHS: the pen position already maps through
|
||||
// the affine, but font size and centering boxes must scale with it too
|
||||
// or text inside a graphicsLayer(scale) renders at full size, off
|
||||
// centre (JVM/Skia scales glyphs with the canvas). Wrap stays in LOCAL
|
||||
// units below so line breaks match what layout measured.
|
||||
val vTextScaleX = sqrt(fMa * fMa + fMb * fMb)
|
||||
val vTextScaleY = sqrt(fMc * fMc + fMd * fMd)
|
||||
val vDeviceFontPx =
|
||||
if (vTextScaleY == 1f) inFontSizePx
|
||||
else (inFontSizePx * vTextScaleY).toInt().coerceAtLeast(1)
|
||||
|
||||
if (inFontFamily != null && IconFont.isIconFamily(inFontFamily)) {
|
||||
vTr.drawText(
|
||||
inText = inText,
|
||||
inX = mapX(inX, inY).toInt(),
|
||||
inY = mapY(inX, inY).toInt(),
|
||||
inBoxWidth = inBoxWidth.toInt(),
|
||||
inBoxHeight = inBoxHeight.toInt(),
|
||||
inBoxWidth = (inBoxWidth * vTextScaleX).toInt(),
|
||||
inBoxHeight = (inBoxHeight * vTextScaleY).toInt(),
|
||||
inColor = vColor,
|
||||
inFontSize = inFontSizePx,
|
||||
inFontSize = vDeviceFontPx,
|
||||
inAlign = inTextAlign,
|
||||
inFontFamily = inFontFamily,
|
||||
inFontVariations = inFontVariations,
|
||||
@@ -758,15 +1035,15 @@ internal class Sdl3Canvas(
|
||||
if (vLineY >= inY + inBoxHeight) break
|
||||
vTr.drawText(
|
||||
inText = vLine,
|
||||
// Position maps through the affine (translate/scale reach the origin);
|
||||
// glyph scaling/rotation isn't wired, so text in a rotated/scaled layer
|
||||
// repositions but doesn't itself scale or rotate.
|
||||
// Position maps through the affine; size/boxes switch to DEVICE
|
||||
// units here (scaled by the affine's column norms) so glyphs
|
||||
// scale with the layer. Rotation still only repositions.
|
||||
inX = mapX(inX, vLineY).toInt(),
|
||||
inY = mapY(inX, vLineY).toInt(),
|
||||
inBoxWidth = inBoxWidth.toInt(),
|
||||
inBoxHeight = vLineH.toInt(),
|
||||
inBoxWidth = (inBoxWidth * vTextScaleX).toInt(),
|
||||
inBoxHeight = (vLineH * vTextScaleY).toInt(),
|
||||
inColor = vColor,
|
||||
inFontSize = inFontSizePx,
|
||||
inFontSize = vDeviceFontPx,
|
||||
inAlign = inTextAlign,
|
||||
inFontFamily = inFontFamily,
|
||||
inFontVariations = inFontVariations,
|
||||
@@ -794,6 +1071,7 @@ internal class Sdl3Canvas(
|
||||
inContentScale: androidx.compose.ui.layout.ContentScale,
|
||||
inAlpha: Float,
|
||||
) {
|
||||
admitDraw(inX, inY, inX + inWidth, inY + inHeight)
|
||||
fScope.flush()
|
||||
// Map the origin through the affine and scale the size by the matrix's axis
|
||||
// magnitudes (rotation contributes 1, so it only repositions the blit — the
|
||||
@@ -812,6 +1090,7 @@ internal class Sdl3Canvas(
|
||||
|
||||
override fun drawImage(image: ImageBitmap, topLeftOffset: Offset, paint: Paint) {
|
||||
val vBmp = image as? SdlImageBitmap ?: return
|
||||
admitDraw(topLeftOffset.x, topLeftOffset.y, topLeftOffset.x + vBmp.width, topLeftOffset.y + vBmp.height)
|
||||
drawImageRect(
|
||||
image,
|
||||
androidx.compose.ui.unit.IntOffset.Zero,
|
||||
@@ -834,6 +1113,7 @@ internal class Sdl3Canvas(
|
||||
dstSize: androidx.compose.ui.unit.IntSize,
|
||||
paint: Paint,
|
||||
) {
|
||||
realizePendingClips()
|
||||
val vTex = (image as? SdlImageBitmap)?.texture ?: return
|
||||
// Commit pending frame geometry and re-assert this canvas's target + clip
|
||||
// (an offscreen render just borrowed the render target).
|
||||
|
||||
+14
-6
@@ -893,7 +893,13 @@ internal class Sdl3DrawScope(
|
||||
// through the current affine so scale/rotate reach the GPU; the colour is
|
||||
// sampled at the pre-transform point so a gradient rides its shape.
|
||||
private fun writeVertex(inX: Float, inY: Float, inColor: ComposeColor, inAlphaScale: Float = 1f) {
|
||||
if (fBatchCount >= kBatchCapacity) flush()
|
||||
// Auto-flush ONLY at a triangle boundary: SDL_RenderGeometry renders
|
||||
// floor(count/3) triangles and silently drops the tail, so flushing
|
||||
// mid-triangle tears it AND misaligns every following triangle in the
|
||||
// new batch (seen as ~350-degree pac-man circles and glitch shapes
|
||||
// once a frame's geometry outgrew one batch - e.g. 84 unclipped
|
||||
// drawBehind bubbles). kBatchCapacity is a multiple of 3.
|
||||
if (fBatchCount >= kBatchCapacity && fBatchCount % 3 == 0) flush()
|
||||
val vBase = fBatchCount * kFloatsPerVertex
|
||||
fVertexData[vBase + 0] = fMa * inX + fMc * inY + fMe
|
||||
fVertexData[vBase + 1] = fMb * inX + fMd * inY + fMf
|
||||
@@ -914,11 +920,13 @@ private const val kAaFeather: Float = 1.0f
|
||||
private const val kAaHalf: Float = kAaFeather * 0.5f
|
||||
|
||||
// ============
|
||||
// Batch capacity — 8192 vertices = ~2730 triangles per submission. At
|
||||
// 64 segments per full circle that's room for ~21 full-circle filled
|
||||
// shapes per Canvas{} before any flush. Bigger gives fewer GPU
|
||||
// submissions; smaller saves RAM. ~128 KB at 16 bytes per SDL_Vertex.
|
||||
private const val kBatchCapacity: Int = 8192
|
||||
// Batch capacity — 8190 vertices = 2730 triangles per submission (kept a
|
||||
// MULTIPLE OF 3: the auto-flush in writeVertex only fires on triangle
|
||||
// boundaries, so the cap must land on one). At 64 segments per full circle
|
||||
// that's room for ~21 full-circle filled shapes per Canvas{} before any
|
||||
// flush. Bigger gives fewer GPU submissions; smaller saves RAM. ~128 KB at
|
||||
// 16 bytes per SDL_Vertex.
|
||||
private const val kBatchCapacity: Int = 8190
|
||||
|
||||
// Floats per SDL_Vertex: position(x,y) + color(r,g,b,a) + tex_coord(x,y), tightly
|
||||
// packed. Used to stage vertices in a Kotlin FloatArray and memcpy them across.
|
||||
|
||||
+78
-20
@@ -29,30 +29,90 @@ internal var currentMainCanvas: Sdl3Canvas? = null
|
||||
// An ImageBitmap backed by an SDL render-target texture. The vector rasterises
|
||||
// into the texture (via a Canvas returned by the factory) and Sdl3Canvas.drawImageRect
|
||||
// blits it back — with the Icon tint applied through SDL_SetTextureColorMod.
|
||||
// Mutable native handles held BY REFERENCE so a Cleaner can free them without
|
||||
// capturing the (collectable) SdlImageBitmap. Both the explicit close() and the
|
||||
// GC Cleaner enqueue release of whatever is still here — the first to run nulls
|
||||
// the fields, so the other is a no-op (they can't overlap: close() only runs
|
||||
// while the bitmap is reachable, the Cleaner only after it isn't).
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private class SdlBitmapHandles(
|
||||
var texture: COpaquePointer?,
|
||||
var surface: CPointer<SDL_Surface>?,
|
||||
)
|
||||
|
||||
/* Enqueue destruction of the handles' current contents on the main thread
|
||||
(SDL calls aren't thread-safe; the Cleaner runs on a GC worker). Idempotent:
|
||||
clears the fields so a second call frees nothing. Top-level so the Cleaner
|
||||
block never captures the bitmap. */
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun enqueueRelease(inHandles: SdlBitmapHandles) {
|
||||
val vTex = inHandles.texture
|
||||
val vSurf = inHandles.surface
|
||||
if (vTex == null && vSurf == null) return
|
||||
inHandles.texture = null
|
||||
inHandles.surface = null
|
||||
com.compose.sdl.graphics.NativeReleaseQueue.enqueue {
|
||||
if (vSurf != null) SDL_DestroySurface(vSurf)
|
||||
if (vTex != null) SDL_DestroyTexture(vTex.reinterpret())
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
internal class SdlImageBitmap(
|
||||
inRenderer: COpaquePointer,
|
||||
private val fRenderer: COpaquePointer,
|
||||
override val width: Int,
|
||||
override val height: Int,
|
||||
override val config: ImageBitmapConfig,
|
||||
override val hasAlpha: Boolean,
|
||||
override val colorSpace: ColorSpace,
|
||||
// A pre-made texture (the encoded-image decode path — see
|
||||
// Sdl3EncodedImageDecoder); null → create a render TARGET for the
|
||||
// vector-rasterisation path below.
|
||||
inDecodedTexture: COpaquePointer? = null,
|
||||
// A surface decoded OFF the main thread (the encoded-image path — see
|
||||
// Sdl3EncodedImageDecoder); converted to a texture on the first draw.
|
||||
// null → create a render TARGET for the vector-rasterisation path below
|
||||
// (that path always constructs on the main thread).
|
||||
fDecodedSurface: CPointer<SDL_Surface>? = null,
|
||||
) : ImageBitmap {
|
||||
|
||||
// RGBA render-target texture, premultiplied blend for compositing back (content
|
||||
// is drawn over a transparent clear with ordinary BLEND, leaving premultiplied
|
||||
// colours — see Sdl3ClipTargets for the same reasoning).
|
||||
val texture: COpaquePointer? = inDecodedTexture ?: SDL_CreateTexture(
|
||||
inRenderer.reinterpret(),
|
||||
SDL_PIXELFORMAT_RGBA32,
|
||||
SDL_TextureAccess.SDL_TEXTUREACCESS_TARGET,
|
||||
maxOf(1, width),
|
||||
maxOf(1, height),
|
||||
)?.also { SDL_SetTextureBlendMode(it.reinterpret(), SDL_BLENDMODE_BLEND_PREMULTIPLIED) }
|
||||
private val fHandles = SdlBitmapHandles(
|
||||
texture = if (fDecodedSurface != null) null else SDL_CreateTexture(
|
||||
fRenderer.reinterpret(),
|
||||
SDL_PIXELFORMAT_RGBA32,
|
||||
SDL_TextureAccess.SDL_TEXTUREACCESS_TARGET,
|
||||
maxOf(1, width),
|
||||
maxOf(1, height),
|
||||
)?.also { SDL_SetTextureBlendMode(it.reinterpret(), SDL_BLENDMODE_BLEND_PREMULTIPLIED) },
|
||||
surface = fDecodedSurface,
|
||||
)
|
||||
|
||||
// GC backstop: if nothing calls close(), free the native handles when the
|
||||
// bitmap is collected (deferred to the main thread via the queue). Ownership
|
||||
// via close() is the primary path; this catches leaks.
|
||||
@OptIn(kotlin.experimental.ExperimentalNativeApi::class)
|
||||
private val fCleaner = kotlin.native.ref.createCleaner(fHandles) { enqueueRelease(it) }
|
||||
|
||||
// SDL renderer calls are NOT thread-safe, and the resources pipeline
|
||||
// decodes on Dispatchers.Default workers — so the decode path hands over a
|
||||
// plain SDL_Surface and the texture is created here, lazily, on the first
|
||||
// access (createCanvas / the draw paths — main thread only). Decoded
|
||||
// surfaces carry STRAIGHT alpha → ordinary BLEND, unlike the premultiplied
|
||||
// render-target path above.
|
||||
val texture: COpaquePointer?
|
||||
get() {
|
||||
fHandles.surface?.let { vSurface ->
|
||||
fHandles.texture = SDL_CreateTextureFromSurface(fRenderer.reinterpret(), vSurface)
|
||||
?.also { SDL_SetTextureBlendMode(it.reinterpret(), SDL_BLENDMODE_BLEND) }
|
||||
SDL_DestroySurface(vSurface)
|
||||
fHandles.surface = null
|
||||
}
|
||||
return fHandles.texture
|
||||
}
|
||||
|
||||
/* Free the texture/surface now (deferred to the main thread). Call on
|
||||
cache eviction / removeMemoryResource so native memory releases promptly
|
||||
instead of waiting for a GC. The Cleaner remains as a backstop. */
|
||||
fun close() = enqueueRelease(fHandles)
|
||||
|
||||
override fun readPixels(
|
||||
buffer: IntArray,
|
||||
@@ -116,24 +176,22 @@ internal class Sdl3EncodedImageDecoder(
|
||||
|
||||
override fun decode(inBytes: ByteArray): ImageBitmap? {
|
||||
if (inBytes.isEmpty()) return null
|
||||
// Pure decode only — IMG_Load_IO touches no renderer state, so it is
|
||||
// safe on the resources pipeline's Dispatchers.Default workers. The
|
||||
// texture is created from this surface on the first MAIN-THREAD draw
|
||||
// (SdlImageBitmap.texture) — SDL renderer calls are not thread-safe.
|
||||
val vSurface = inBytes.usePinned { vPinned ->
|
||||
val vIo = SDL_IOFromConstMem(vPinned.addressOf(0), inBytes.size.convert())
|
||||
?: return@usePinned null
|
||||
sdl3_image.IMG_Load_IO(vIo.reinterpret(), true)
|
||||
} ?: return null
|
||||
val vSdlSurface = vSurface.reinterpret<SDL_Surface>()
|
||||
val vW = vSdlSurface.pointed.w
|
||||
val vH = vSdlSurface.pointed.h
|
||||
val vTex = SDL_CreateTextureFromSurface(fRenderer.reinterpret(), vSdlSurface)
|
||||
SDL_DestroySurface(vSdlSurface)
|
||||
if (vTex == null) return null
|
||||
SDL_SetTextureBlendMode(vTex.reinterpret(), SDL_BLENDMODE_BLEND)
|
||||
return SdlImageBitmap(
|
||||
fRenderer, vW, vH,
|
||||
fRenderer, vSdlSurface.pointed.w, vSdlSurface.pointed.h,
|
||||
config = ImageBitmapConfig.Argb8888,
|
||||
hasAlpha = true,
|
||||
colorSpace = androidx.compose.ui.graphics.colorspace.ColorSpaces.Srgb,
|
||||
inDecodedTexture = vTex,
|
||||
fDecodedSurface = vSdlSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import kotlin.experimental.ExperimentalNativeApi
|
||||
|
||||
internal expect fun makeMetalBridge(backend: SDL3Backend): SkiaBridge?
|
||||
|
||||
fun rendererPreferredGpuMode(): GpuMode {
|
||||
actual fun rendererPreferredGpuMode(): GpuMode {
|
||||
@OptIn(ExperimentalNativeApi::class)
|
||||
return when (Platform.osFamily) {
|
||||
OsFamily.MACOSX -> GpuMode.Skia.Metal
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import com.compose.sdl.renderer.skia.SkiaRenderBackend
|
||||
SDL3 module's createRenderBackend / rendererPreferredGpuMode in the same
|
||||
package; exactly one renderer module is included per target. Rejects
|
||||
Sdl3.* since this module has no SDL3_ttf / SDL3_image. */
|
||||
fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
||||
actual fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
||||
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||
if (vResolved is GpuMode.Sdl3) {
|
||||
error("Sdl3.* modes aren't available in a Skia build — rerun with -Prenderer=sdl3")
|
||||
|
||||
+20
-16
@@ -58,22 +58,6 @@ internal class SkiaImageCache {
|
||||
}
|
||||
}
|
||||
|
||||
/* SVGDOM → offscreen raster → Image. Falls back to a 100×100 canvas when
|
||||
the document declares no explicit width/height (the Android-vector path
|
||||
always supplies them). */
|
||||
private fun rasterizeSvg(inBytes: ByteArray): Image? = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(inBytes))
|
||||
val vRoot = vDom.root
|
||||
var vW = vRoot?.width?.value ?: 0f
|
||||
var vH = vRoot?.height?.value ?: 0f
|
||||
if (vW <= 0f) vW = 100f
|
||||
if (vH <= 0f) vH = 100f
|
||||
vDom.setContainerSize(vW, vH)
|
||||
val vSurface = Surface.makeRasterN32Premul(vW.toInt().coerceAtLeast(1), vH.toInt().coerceAtLeast(1))
|
||||
vDom.render(vSurface.canvas)
|
||||
vSurface.makeImageSnapshot()
|
||||
}.getOrNull()
|
||||
|
||||
// ==================
|
||||
// MARK: Draw
|
||||
// ==================
|
||||
@@ -133,3 +117,23 @@ internal class SkiaImageCache {
|
||||
fCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: SVG rasterisation (shared with SkiaEncodedImageDecoder)
|
||||
// ==================
|
||||
|
||||
/* SVGDOM → offscreen raster → Image. Falls back to a 100×100 canvas when
|
||||
the document declares no explicit width/height (the Android-vector path
|
||||
always supplies them). */
|
||||
internal fun rasterizeSvg(inBytes: ByteArray): Image? = runCatching {
|
||||
val vDom = SVGDOM(Data.makeFromBytes(inBytes))
|
||||
val vRoot = vDom.root
|
||||
var vW = vRoot?.width?.value ?: 0f
|
||||
var vH = vRoot?.height?.value ?: 0f
|
||||
if (vW <= 0f) vW = 100f
|
||||
if (vH <= 0f) vH = 100f
|
||||
vDom.setContainerSize(vW, vH)
|
||||
val vSurface = Surface.makeRasterN32Premul(vW.toInt().coerceAtLeast(1), vH.toInt().coerceAtLeast(1))
|
||||
vDom.render(vSurface.canvas)
|
||||
vSurface.makeImageSnapshot()
|
||||
}.getOrNull()
|
||||
|
||||
+31
@@ -5,6 +5,8 @@ import androidx.compose.ui.graphics.Canvas
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.ImageBitmapConfig
|
||||
import androidx.compose.ui.graphics.colorspace.ColorSpace
|
||||
import androidx.compose.ui.graphics.colorspace.ColorSpaces
|
||||
import com.compose.sdl.graphics.EncodedImageDecoder
|
||||
import com.compose.sdl.graphics.OffscreenRenderer
|
||||
import org.jetbrains.skia.Image
|
||||
import org.jetbrains.skia.Surface
|
||||
@@ -86,3 +88,32 @@ internal class SkiaOffscreenRenderer(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================
|
||||
// MARK: SkiaEncodedImageDecoder
|
||||
// ==================
|
||||
|
||||
/* Encoded-image decode hook impl for the Skia renderer — the counterpart of
|
||||
Sdl3EncodedImageDecoder, consumed by :components-resources' actuals
|
||||
(painterResource / SVG). Raster formats go through Image.makeFromEncoded;
|
||||
when that fails the bytes are retried as SVG through the same SVGDOM
|
||||
rasterisation SkiaImageCache uses. Pure CPU raster (no GrContext), so it is
|
||||
safe on the resources pipeline's Dispatchers.Default workers. */
|
||||
internal class SkiaEncodedImageDecoder : EncodedImageDecoder {
|
||||
|
||||
override fun decode(inBytes: ByteArray): ImageBitmap? {
|
||||
if (inBytes.isEmpty()) return null
|
||||
val vImg = runCatching { Image.makeFromEncoded(inBytes) }.getOrNull()
|
||||
?: rasterizeSvg(inBytes)
|
||||
?: return null
|
||||
val vBmp = SkiaImageBitmap(
|
||||
vImg.width, vImg.height,
|
||||
config = ImageBitmapConfig.Argb8888,
|
||||
hasAlpha = true,
|
||||
colorSpace = ColorSpaces.Srgb,
|
||||
)
|
||||
vBmp.surface.canvas.drawImage(vImg, 0f, 0f)
|
||||
vImg.close()
|
||||
return vBmp
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -33,6 +33,18 @@ internal class SkiaRenderBackend(
|
||||
private val fSkiaImageCache = SkiaImageCache()
|
||||
private var fCurrentCanvas: Canvas? = null
|
||||
|
||||
init {
|
||||
// Encoded-image decode (painterResource / SVG in :components-resources).
|
||||
// Registered at CONSTRUCTION, not first frame: the official resources
|
||||
// pipeline decodes during COMPOSITION, which runs before beginFrame —
|
||||
// and on Dispatchers.Default workers, which the CPU-raster Skia decoder
|
||||
// tolerates. Without this the first painterResource on a Skia build
|
||||
// dies with "Image decode failed" (issue #1).
|
||||
if (com.compose.sdl.graphics.encodedImageDecoder == null) {
|
||||
com.compose.sdl.graphics.encodedImageDecoder = SkiaEncodedImageDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
override val textMeasurer: TextMeasurer
|
||||
get() = fSkiaTextRenderer.textMeasurer
|
||||
|
||||
|
||||
+40
-16
@@ -100,32 +100,56 @@ kotlin {
|
||||
// Common API on both stacks (its native/jvm actuals pick the
|
||||
// right rendering pipeline) — usable from shared screens.
|
||||
implementation(project(":material-symbols"))
|
||||
|
||||
// Official Maven coords for everything the shared screens
|
||||
// touch, so commonMain metadata (and the IDE's common
|
||||
// analysis) resolve. Metadata + jvm resolve them from Maven;
|
||||
// the NATIVE configurations substitute each for its port
|
||||
// module (root build's FULL-COMMONIZATION BRIDGE). Every
|
||||
// artifact must be declared DIRECTLY: transitives of a
|
||||
// substituted module are invisible to the granular-metadata
|
||||
// visibility check. The runtime is the official klib
|
||||
// everywhere, never substituted (jvm configs force the +dev
|
||||
// build above).
|
||||
implementation("org.jetbrains.compose.runtime:runtime:1.11.1")
|
||||
implementation("org.jetbrains.compose.ui:ui:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-graphics:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-text:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-unit:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-geometry:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui-util:$vComposeJvmVersion")
|
||||
// The common @Preview (androidx.compose.ui.tooling.preview) —
|
||||
// IDE-only metadata; previews render through the jvm target.
|
||||
implementation("org.jetbrains.compose.ui:ui-tooling-preview:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.foundation:foundation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.foundation:foundation-layout:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.animation:animation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.animation:animation-core:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
||||
|
||||
// nav3 + lifecycle: real KMP Maven artifacts on every target
|
||||
// (see "Known Compatible" in CLAUDE.md) — except navigation3-ui,
|
||||
// which the bridge substitutes for the vendored :navigation3-ui
|
||||
// on native.
|
||||
implementation("androidx.navigation3:navigation3-runtime:1.1.4")
|
||||
implementation("org.jetbrains.androidx.navigation3:navigation3-ui:1.2.0-alpha02")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0")
|
||||
}
|
||||
}
|
||||
nativeMain {
|
||||
dependencies {
|
||||
implementation(project(":window"))
|
||||
implementation(project(":material3"))
|
||||
implementation(project(":navigation3-ui"))
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0")
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
// The +dev build matching scripts/compose-fork's pinned
|
||||
// COMPOSE_CORE_REF — byte-exact parity with the vendored
|
||||
// sources, and (unlike the published beta01) its desktop
|
||||
// loadTypeface applies Font.variationSettings, so the
|
||||
// Material Symbols variable-font axes work on JVM.
|
||||
implementation("org.jetbrains.compose.runtime:runtime:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.foundation:foundation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.animation:animation:$vComposeJvmVersion")
|
||||
implementation("org.jetbrains.compose.material3:material3:$vComposeM3JvmVersion")
|
||||
implementation("org.jetbrains.compose.ui:ui:$vComposeJvmVersion")
|
||||
|
||||
// The +dev builds matching scripts/compose-fork's pinned
|
||||
// COMPOSE_CORE_REF come from the version forcing above —
|
||||
// byte-exact parity with the vendored sources, and (unlike
|
||||
// the published beta01) desktop loadTypeface applies
|
||||
// Font.variationSettings, so the Material Symbols
|
||||
// variable-font axes work on JVM.
|
||||
implementation("androidx.navigation3:navigation3-runtime:1.2.0-alpha05")
|
||||
implementation("org.jetbrains.androidx.navigation3:navigation3-ui:1.2.0-alpha02")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.11.0")
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@@ -5,18 +5,17 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@@ -4,22 +4,14 @@ package screens
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicSecureTextField
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -20,12 +20,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
|
||||
// Foundation — the undecorated text primitive below material3's Text, plus the
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.style.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
internal fun ButtonsScreen() {
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -18,8 +11,8 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -3,19 +3,8 @@ package screens
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.ElevatedAssistChip
|
||||
import androidx.compose.material3.ElevatedFilterChip
|
||||
import androidx.compose.material3.ElevatedSuggestionChip
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.SuggestionChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@@ -5,15 +5,12 @@ package screens
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package screens
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// ==================
|
||||
// MARK: Colors screen — Material palette swatches
|
||||
// ==================
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun ColorsScreen() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package screens
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.style.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
internal fun CounterScreen() {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -6,26 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -4,23 +4,13 @@ import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.draganddrop.dragAndDropTarget
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
|
||||
@@ -3,16 +3,7 @@ package screens
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.ElevatedButton
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
import androidx.compose.material3.SmallFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -3,15 +3,7 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowColumn
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
||||
@@ -3,25 +3,16 @@ package screens
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@@ -3,23 +3,12 @@ package screens
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -2,23 +2,13 @@ package screens
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyHorizontalStaggeredGrid
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package screens
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.*
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import demo.generated.resources.Res
|
||||
import demo.generated.resources.compose_logo
|
||||
import demo.generated.resources.heart
|
||||
import demo.generated.resources.photo
|
||||
import demo.generated.resources.star
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import demo.generated.resources.*
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
// ==================
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
// ==================
|
||||
// MARK: Interaction (hover / press / focus) screen
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.HoverInteraction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package screens
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
internal fun LayoutScreen() {
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.style.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// ==================
|
||||
// MARK: LazyColumn screen
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
||||
@@ -4,13 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@@ -2,33 +2,9 @@
|
||||
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.AppBarColumn
|
||||
import androidx.compose.material3.AppBarOverflowIndicator
|
||||
import androidx.compose.material3.AppBarRow
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FlexibleBottomAppBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MediumFlexibleTopAppBar
|
||||
import androidx.compose.material3.MediumTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TwoRowsTopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import demo.shim.DemoIcon
|
||||
|
||||
@@ -6,29 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.ButtonGroup
|
||||
import androidx.compose.material3.ElevatedToggleButton
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledIconToggleButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||
import androidx.compose.material3.IconToggleButton
|
||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.OutlinedIconToggleButton
|
||||
import androidx.compose.material3.OutlinedToggleButton
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SplitButtonDefaults
|
||||
import androidx.compose.material3.SplitButtonLayout
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ToggleButton
|
||||
import androidx.compose.material3.TonalToggleButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
||||
@@ -2,34 +2,9 @@
|
||||
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DismissibleDrawerSheet
|
||||
import androidx.compose.material3.DismissibleNavigationDrawer
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.PermanentDrawerSheet
|
||||
import androidx.compose.material3.PermanentNavigationDrawer
|
||||
import androidx.compose.material3.Scrim
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -2,31 +2,9 @@
|
||||
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FloatingActionButtonMenu
|
||||
import androidx.compose.material3.FloatingActionButtonMenuItem
|
||||
import androidx.compose.material3.HorizontalFloatingToolbar
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeExtendedFloatingActionButton
|
||||
import androidx.compose.material3.LargeFloatingActionButton
|
||||
import androidx.compose.material3.MediumExtendedFloatingActionButton
|
||||
import androidx.compose.material3.MediumFloatingActionButton
|
||||
import androidx.compose.material3.SmallExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ToggleFloatingActionButton
|
||||
import androidx.compose.material3.VerticalFloatingToolbar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -2,54 +2,10 @@
|
||||
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenuGroup
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.DropdownMenuPopup
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Label
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuDefaults
|
||||
import androidx.compose.material3.OutlinedSecureTextField
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.RangeSlider
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScrollField
|
||||
import androidx.compose.material3.ScrollFieldDefaults
|
||||
import androidx.compose.material3.SecureTextField
|
||||
import androidx.compose.material3.SegmentedListItem
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.VerticalSlider
|
||||
import androidx.compose.material3.rememberScrollFieldState
|
||||
import androidx.compose.material3.rememberSliderState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -6,24 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DateRangePicker
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TimeInput
|
||||
import androidx.compose.material3.TimePicker
|
||||
import androidx.compose.material3.TimePickerDialog
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
import androidx.compose.material3.rememberDateRangePickerState
|
||||
import androidx.compose.material3.rememberTimePickerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@@ -2,29 +2,9 @@
|
||||
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ModalWideNavigationRail
|
||||
import androidx.compose.material3.ShortNavigationBar
|
||||
import androidx.compose.material3.ShortNavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.WideNavigationRail
|
||||
import androidx.compose.material3.WideNavigationRailItem
|
||||
import androidx.compose.material3.WideNavigationRailValue
|
||||
import androidx.compose.material3.rememberWideNavigationRailState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user