Compare commits

...
21 Commits
Author SHA1 Message Date
Bitsy f5fae6d32e docs(bridge): document settings-wide vs per-module application
Both worked since the plugin is Plugin<Any> branching on Settings vs
Project, but the module-level path was undocumented and untested —
verified now with the example applied compose-plugin style. Also spells
out that a pluginManagement plugins{} entry only pins the version while
the settings-level plugins{} block is what applies build-wide.
2026-07-14 13:08:39 +02:00
Bitsy b94f1f7bbb feat(bridge): compose.desktop.native { entryPoint } — application{} for native
The native counterpart of compose.desktop { application { mainClass } }:
the bridge grafts a 'native' extension onto the Compose plugin's desktop
extension (extensions are ExtensionAware — no compile-time dependency)
and, in afterEvaluate, declares binaries.executable(entryPoint) on every
Kotlin/Native DESKTOP target (mingw/linux/osx families). Targets that
already declare an executable are skipped, so manual configuration wins.

KGP access is reflective for the same reason as the resources packaging:
settings-applied plugins live in a parent classloader that cannot see
project-plugin types.

Replaces the last piece of consumer boilerplate — bubble-wrap's build
shrinks to targets + dependencies + the two entry points, verified
end-to-end (executable declared by the DSL links, bundles data.kres,
boots).
2026-07-14 13:04:38 +02:00
Bitsy ffbb87eddd perf(sdl): containment-gate text draws under pending clips
Text runs no longer force mask realization unconditionally: like other
draws they pass a containment test against the pending rounded clips
(with a 2px margin for glyph overhang / AA bleed). Small centred labels
inside circle-clipped nodes — a popped bubble's tiny text — previously
cost a full offscreen mask pass each, per frame.

Bubble-wrap probe (84 bubbles, half popped, continuous animation):
23 FPS before the lazy-clip series, 39 after it.
2026-07-14 12:47:18 +02:00
Bitsy 63fad4083f fix(sdl): lazily-realized clip masks belong to the clip's save-frame
The press artifact ('square on click, hover fine'): a press draws TWO
overhanging circles — the state layer, then the ripple animation — each
inside its own inner clipRect{} save-frame. The first one realized the
pending mask INSIDE the ripple's inner frame, whose saved clipLayers
count predated the realization, so that frame's restore() composited
and closed the mask early; the second circle then drew with no pending
and no mask — a faint square filling the clip's rect region. Hover only
draws one circle, hence always looked correct.

A lazily-realized mask is now attributed to the save-frame that PUSHED
the clip: PendingRoundClip records fStack.size at push, and realization
bumps the clipLayers threshold of every save-state entered since, so
only the owning frame's restore composites the layer — matching where
the eager implementation would have opened it.

Reproduced and verified with an automated press-hold rig (locate bubble
by colour, hold, capture mid-hold): square before, round after; eager-
forced realization used as the discriminating experiment.
2026-07-14 12:44:15 +02:00
Bitsy 872d740ce7 perf(sdl): lazy rounded clips — record, prove-safe, or realize on demand
Opening an offscreen mask per rounded clip is the most expensive thing
the SDL canvas does (two render-target switches + clear + corner cut +
feather + composite) — and Compose's dominant idiom
clip(shape).background(color) never needs one. clipRoundRect now only
RECORDS the clip (the rect part applies immediately); each draw either
proves itself safe or realizes the mask on demand:

- full-cover solid rect over the innermost pending clip (the background
  idiom) -> emitted directly as an antialiased rounded-rect fill,
- content whose device aabb passes a 4-corner ellipse containment test
  against every pending outline -> drawn as-is under the rect clip,
- anything genuinely crossing an arc (overhanging text, expanding
  ripples, difference clips, shadows) -> realizes the offscreen masks
  (outermost first) exactly as before.

save/restore tracks pending depth; never-realized pendings pop for
free. Measured on the bubble-wrap sheet (84 circle-clipped bubbles +
highlight dots, continuous animation): 23 -> 37 FPS.

Known open issue under investigation: a press on a bubble can show a
square state-layer/ripple artifact (hover is correct) — suspected
custom-blend fallback in the mask feather pass; follow-up commit.
2026-07-14 12:27:58 +02:00
Bitsy 4c2d46429d fix(publish): Windows owns the root metadata modules — 0.1.15 roots had no mingwX64 variants
The KotlinMultiplatform (root) publications ran on the macOS job, and a
macOS host never declares the mingwX64 target (vHostSupportsMingw) — so
every published root .module carried variants for linux/macos/jvm but
NOT mingw_x64. Windows consumers of the bridge resolved the root fine
and then found 'No matching variant' for every coordinate: v0.1.15 (and
every earlier release) is unusable from the published root modules on
Windows. Local mavenLocal flows never caught it because this repo's dev
box IS Windows, whose roots include all targets.

Two changes:
- createRenderBackend / rendererPreferredGpuMode become a real
  expect/actual seam (:ui nativeMain expect, actuals in each renderer
  source set). They were duplicate no-expect declarations — fine
  per-target, but shared nativeMain METADATA couldn't see them when a
  host's targets span both renderers, which is exactly what blocked
  :window's metadata compilation on Windows hosts.
- The publish workflow moves the KotlinMultiplatform + Jvm + bridge
  plugin publications to the WINDOWS job (the only host declaring every
  target) and updates the delete-then-retry ownership map to match.

Verified on Windows: repo-wide common+native metadata sweep green for
all library modules, full publishKotlinMultiplatformPublicationToMavenLocal
(root .module variants now cover linux_x64/linux_arm64/macos_arm64/
mingw_x64/jvm), demo links + boots, and the bubble-wrap consumer example
resolves and links mingwX64 through the bridge against the local roots.
2026-07-14 10:34:50 +02:00
Bitsy 120abbad93 fix(sdl): glyphs scale with the layer + antialiased, overlap-free clip masks
Text: drawNativeText mapped the pen position through the canvas affine
but passed LOCAL font size and centering boxes to the glyph renderer —
inside graphicsLayer(scale) text rendered full-size and off-centre
(JVM/Skia scales glyphs with the canvas). Font size and boxes now
switch to device units at the drawText boundary (affine column norms);
wrapping stays in local units so line breaks keep matching layout.

Clip masks: the round-rect corner cutter now clamps device radii to the
int-truncated bbox half-size (opposite cuts overlapped by a fraction of
a pixel, flattening N/E/S/W into an octagon look), adapts segment count
to on-screen arc length (~4px/vertex, Sdl3DrawScope density), and adds
a ~1.2px feathered ring (custom dst*=srcAlpha blend) — SDL_RenderGeometry
has no AA, so mask edges were visibly rougher than drawn shapes.

Parity-verified against upstream JVM with OS screenshots of the same
pre-popped bubble-wrap sheet.
2026-07-14 01:30:53 +02:00
Bitsy a39002f1b8 feat(sdl3.def): carry the Linux Skia-stack linker flags in the klib
-L multi-arch dirs + fontconfig/GL/X11 moved from the apps' build files
into sdl3.def's linux linkerOpts — cinterop endorses them into the klib,
so consumers (bridge apps included) need no linkerOpts at all on Linux.
Harmless under -Prenderer=sdl3 (unused -l) and when a -L dir is absent
(LLD skips it). The apps keep their copies until the next publish makes
the klib-carried flags the ones consumers resolve; duplicates are inert.

Windows flags stay app-side ON PURPOSE: --subsystem,windows / -Wl,-s are
policy (no console window, stripped binary), not requirements — a .def
would force them on every consumer binary including debug builds and
test executables.
2026-07-14 01:10:37 +02:00
Bitsy fb470ecf0c fix(sdl): scale round-rect clip radii into device space — scaled circles became lozenges
Sdl3Canvas.clipRoundRect cuts the corner mask in DEVICE space (the bbox
goes through the canvas affine) but used the LOCAL user-space corner
radii. Under a graphicsLayer scale the oversized cuts consumed nearly
the whole shape — a circle clipped at scale 0.55 rendered as a diamond
(bubble-wrap's popped bubbles on the SDL renderer vs perfect circles on
JVM). Scale the radii by the affine's column norms alongside the bbox;
rotation already degrades to AABBs on this path.

Repro: two OS-level screenshots of the same pre-popped sheet, native vs
jvm, before/after.
2026-07-14 01:10:10 +02:00
Bitsy 2b6ac65a1f feat(bridge): auto-package composeResources into data.kres for consumers
With the official org.jetbrains.compose plugin applied, the bridge now
registers a package<Variant>ComposeResources<Target> Zip per native
executable link task: the Compose plugin's PREPARED resources (values
xml already converted to .cvr — zipping the raw source dir would break
stringResource) land in data.kres next to the binary, STORED, under the
composeResources/<res-package>/ prefix the generated accessors carry.
Consumers get the official composeResources UX on the port's targets
with zero build-script code.

Convention-based on purpose: applied from settings, the plugin's
classloader is a PARENT of the project's buildscript loader, so KGP /
Compose-plugin types are structurally invisible (typed compileOnly
access dies with NoClassDefFound at class-init). Link-task names,
binary output dirs, prepare-task names and the prepared-resources dir
are stable conventions; packageOfResClass is read reflectively with the
documented {group}.{module}.generated.resources fallback.

Verified end-to-end with the bubble-wrap example: drawable + strings.xml
resolve through Res.* on mingwX64 (data.kres auto-bundled, app boots,
.cvr conversion intact) while jvm and android keep the official
pipeline.
2026-07-14 00:58:29 +02:00
Bitsy 1b2e83a155 ci(publish): delete-then-retry so a mid-publish network blip can't poison a release
GitHub Packages forbids overwriting files that already exist in a
version, and maven-publish uploads each publication's files sequentially
from the first one. When the v0.1.13 macOS run lost DNS mid-publish, the
retries hit 409 Conflict on the first already-uploaded file of each
publication and aborted before re-attempting the missing tail — the
version could never complete (a 409 is only benign when the publication
had already fully landed, which the 409 alone can't tell you).

Between attempts each host now deletes ITS OWN package versions for the
current release via the API (GITHUB_TOKEN, packages:write) and starts
the upload from a clean slate. Hosts own disjoint package names (roots /
-macosarm64 / -jvm / bridge on macOS, -mingwx64 on Windows, -linuxx64 /
-linuxarm64 on Linux) so parallel jobs can't clobber each other; a 403
on delete degrades to the old blind retry. Last-version deletes (a
package new in this release) fall back to deleting the whole package.
2026-07-13 23:36:12 +02:00
Bitsy 8a6cb868d9 fix(sdl): defer decoded-image texture creation to the first main-thread draw
Sdl3EncodedImageDecoder ran SDL_CreateTextureFromSurface on the
resources pipeline's Dispatchers.Default workers — SDL renderer calls
are not thread-safe, so every painterResource decode on the SDL
renderer was a sporadic-crash risk (same family as #1, SDL flavour).

decode() now does the pure IMG_Load_IO surface decode only (renderer-
free, worker-safe) and hands the surface to SdlImageBitmap, which
creates the texture lazily on first access — createCanvas / the draw
paths, main thread only. Blend modes preserved: straight-alpha BLEND
for decoded surfaces, premultiplied for the vector render-target path.

Verified on Windows: demo --screen=Images renders PNG/JPG/SVG/Android-
vector through the new worker-decode → lazy-texture path.
2026-07-13 22:31:04 +02:00
Bitsy 4a7080b313 fix(window): periodic GC nudge so Cleaner-managed native memory is released
Renderer resources (Skia surfaces / images / typefaces, SDL textures)
free their NATIVE memory through 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 (50MB → 190MB navigating the demo, never
reclaimed). Not a retention leak: layers are plain value objects, the
text/image caches are capped, and the draw paths already close() their
Skia objects eagerly.

The main loop now calls GC.collect() every 10s, only after rendering
activity, scheduled between frames — ~ms pause at these heap sizes,
keeps RSS tracking real usage on both renderers.

Fixes #2
2026-07-13 22:26:35 +02:00
Bitsy 1afb13aae0 fix(skia): register an encoded-image decoder so painterResource works on Skia builds
:components-resources' skikoRenderer variant reuses the SDL actuals,
which decode through :ui's encodedImageDecoder hook — but only the SDL
backend ever registered it. On every macOS/Linux Skia build the first
painterResource() (the demo's Images screen) hit a null decoder and
crashed with 'Image decode failed'. Deterministic, not a race.

Add SkiaEncodedImageDecoder (Image.makeFromEncoded + SVGDOM fallback via
rasterizeSvg, hoisted to file scope from SkiaImageCache) drawn into a
SkiaImageBitmap, registered at SkiaRenderBackend construction — decode
runs on the resources pipeline's Dispatchers.Default workers, which the
CPU-raster path tolerates. Mark the hook @Volatile (main-thread write,
worker-thread reads) and de-SDL the error message.

Fixes #1
2026-07-13 22:26:25 +02:00
Bitsy 3d519bcfcd docs: ui-tooling-preview, apidemo JVM parity target, bridge plugin
README: module tree entries for :ui-tooling-preview and the bridge
plugin, apidemo jvm run command + native-only mTLS note, and a new
"Using it from your own build" section for the bridge.

CLAUDE.md: same module-tree additions, apidemo multiplatform layout
(compat/Compat.kt seams), :apidemo:run parity command, bridge coverage
wording, and a new pitfall: substituted Maven modules hide their
transitives from KGP's granular-metadata visibility check — declare
every artifact commonMain touches directly + a bridge rule each.
2026-07-13 22:06:45 +02:00
Bitsy 72287fc995 feat(bridge): consumer-side Gradle plugin — official CMP coords on every platform
Dependency-substitution rules can't ship inside Maven artifacts, so the
FULL-COMMONIZATION BRIDGE is published as a Gradle plugin instead:
:compose-desktop-native-bridge (id com.bitsycore.compose-desktop-native
.bridge). Consumers apply it once in settings (gradle.lifecycle
.beforeProject, Gradle 8.8+) or per module, declare the OFFICIAL
org.jetbrains.compose coordinates in commonMain, and every native
desktop configuration substitutes the published com.bitsycore.compose
.sdl klibs — android/jvm/iOS/wasm keep resolving the official artifacts.

The substituted version defaults to the plugin's own (stamped into a
resource at build time; plugin and klibs ship from the same tag) and can
be overridden with the composeDesktopNative.version property. The
runtime is deliberately absent from the table — official klibs serve
every target. Published by the macOS publish job (host-independent jar
+ plugin marker).

Verified end-to-end offline: klibs + plugin published to mavenLocal, a
scratch third-party build applying only the plugin compiled a Compose
composable for mingwX64 against official coords — dependencyInsight
shows org.jetbrains.compose.ui:ui -> com.bitsycore.compose.sdl:ui
(selected by rule).
2026-07-13 22:06:45 +02:00
Bitsy 0b4a170910 feat(apidemo): port to JVM — shared commonMain UI + per-stack seams
Same commonization as :demo: the whole UI/logic (App, Sidebar,
ViewerPanel, panels, Model, Http, Persist, SyntaxHighlight, ...) moves
to commonMain compiling against the official Maven compose coords
(native configs substitute the port modules via the root bridge); a
jvm() target runs it on upstream Compose Desktop.

Seams (compat/Compat.kt expects; native actuals are pure delegation to
com.compose.sdl, jvm actuals use AWT + upstream desktop APIs):
file dialogs, appDataDir, revealInFileManager, clipboard text entry,
in-memory response images (Skia decode / loadSvgPainter on jvm),
wrappedRowCount (upstream TextMeasurer at density 1), editorTabWidth,
createApiHttpClient (Curl native / CIO jvm), InstallWindowHooks
(close-intercept + app-wide shortcuts -> Window onCloseRequest/
onPreviewKeyEvent), systemFileSystem (okio SYSTEM isn't in common
metadata), expect/actual mono font. Scrollbar/SplitPane/LayoutExt are
copies of the port's pure-Compose project code (ScrollState adapter
dropped — reads foundation internals; only a LazyColumn is scrollbared).

mTLS (CurlMtls + per-OS ClientCert*) stays native: ChainCert/TlsChain +
expects move to TlsCommon.kt, jvm actuals report the feature as
native-only. kotlinx-coroutines-swing provides Dispatchers.Main on
desktop jvm (App launches on it); jvmProcessResources stages
NotoSansMono + the used Material Symbols fonts.

Verified: common metadata + mingwX64 + jvm compile, native exe links
and boots, jvm app runs (fonts staged, clean exit through the
persist-then-close hook).
2026-07-13 21:40:24 +02:00
Bitsy 72e6f145b3 feat(ui-tooling-preview): common @Preview via vendored module + bridge
The canonical common @Preview is androidx.compose.ui.tooling.preview
.Preview from org.jetbrains.compose.ui:ui-tooling-preview (the umbrella
components-ui-tooling-preview annotation is deprecated). The Maven
artifact ships no mingwX64/linux klibs, so vendor the upstream module
verbatim as :ui-tooling-preview (compose-fork manifest at
COMPOSE_CORE_REF, commonMain + nonJvm actual) and substitute the Maven
coord for it on native configurations via the root bridge.

:demo declares the coord in commonMain; previews render through the jvm
parity target (IntelliJ/AS with the CMP plugin). Example on ColorsScreen.
2026-07-13 21:40:07 +02:00
Bitsy 367ed94c01 refactor(screens): streamline import statements across multiple screen files 2026-07-13 20:46:09 +02:00
Bitsy fa9bd93ada fix(demo): commonize dependencies so commonMain metadata + IDE analysis resolve
demo's commonMain had NO Compose dependencies — shared screens only
compiled because each target folds them in with jvmMain/nativeMain deps,
leaving compileCommonMainKotlinMetadata (and the IDE's common analysis)
broken between the JetBrains artifacts and the local fork.

Apply the :material-symbols pattern: commonMain declares the official
Maven coords for everything the shared screens touch (runtime, ui,
ui-graphics/-text/-unit/-geometry/-util, foundation(+layout),
animation(+core), material3, nav3-runtime, nav3-ui,
lifecycle-viewmodel-navigation3); metadata + jvm resolve them from
Maven, and the root FULL-COMMONIZATION BRIDGE substitutes each for its
port module on native configurations (new rules: ui-text/ui-graphics ->
:ui, ui-unit/-geometry/-util, foundation-layout, animation(+core),
navigation3-ui). Each artifact is declared DIRECTLY because transitives
of a substituted module are invisible to KGP's granular-metadata
visibility check.

nativeMain keeps only :window (the rest now flows from commonMain);
jvmMain keeps only its version bumps (nav3-runtime 1.2.0-alpha05) and
compose.desktop.currentOs.

Verified: :demo compileCommonMainKotlinMetadata + mingwX64 + jvm compile,
mingwX64 link, and a --screenshot runtime smoke test all pass.
2026-07-13 20:43:14 +02:00
Bitsy d1551bc9c1 fix(publish): resolve ui-graphics/ui-unit in material-symbols common metadata
The macOS publish job (the only one running the KotlinMultiplatform
metadata publication) failed in :material-symbols:
compileCommonMainKotlinMetadata — Color/isSpecified/Dp/dp unresolved.
They live in ui-graphics/ui-unit, TRANSITIVES of the Maven ui artifact:
the native substitution (ui -> project(":ui")) hides those transitives
from KGP's granular-metadata visibility check, dropping their metadata
klibs from the commonMain classpath.

Declare both explicitly in commonMain and teach the bridge their
project equivalents (ui-graphics is merged into :ui; ui-unit has its
own module).
2026-07-13 20:34:31 +02:00
122 changed files with 4098 additions and 2197 deletions
+72 -15
View File
@@ -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
+51 -11
View File
@@ -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
@@ -481,6 +509,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
+40 -2
View File
@@ -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
+101 -13
View File
@@ -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
+20
View File
@@ -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
}
@@ -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 })
}
}
}
@@ -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)
@@ -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()
@@ -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)
}
+162
View File
@@ -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 }
+23
View File
@@ -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
}
+22
View File
@@ -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()
}
}
+26
View File
@@ -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) }
}
}
+6 -17
View File
@@ -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) }
+3 -13
View File
@@ -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
View File
@@ -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"))
}
}
}
@@ -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?")
)
@@ -146,6 +146,8 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
// ============
// Main loop
var vGcLastTicks = SDL_GetTicks()
var vRenderedSinceGc = false
while (!runtime.exitRequested) {
Snapshot.sendApplyNotifications()
@@ -224,6 +226,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()
}
}
// ============
@@ -243,6 +262,11 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
SDL_Quit()
}
/* 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
@@ -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
@@ -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. " +
@@ -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,122 @@ 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). */
private fun realizePendingClips() {
if (fPendingClips.isEmpty()) return
val vRenderer = fRenderer.reinterpret<cnames.structs.SDL_Renderer>()
while (fPendingClips.isNotEmpty()) {
val vPending = fPendingClips.removeFirst()
fScope.flush()
// A clipRect after the push may have narrowed the visible area.
val vRegion = intersect(fClip, 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)
}
}
}
}
// 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 +281,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 +385,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 +449,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 +470,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 +492,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 +566,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 +611,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 +688,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 +823,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 +831,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 +907,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 +942,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 +962,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 +1019,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 +1055,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 +1074,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 +1097,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).
@@ -31,29 +31,47 @@ internal var currentMainCanvas: Sdl3Canvas? = null
// blits it back — with the Icon tint applied through SDL_SetTextureColorMod.
@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).
private var 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(),
private var fTexture: COpaquePointer? = 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) }
// 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() {
fDecodedSurface?.let { vSurface ->
fTexture = SDL_CreateTextureFromSurface(fRenderer.reinterpret(), vSurface)
?.also { SDL_SetTextureBlendMode(it.reinterpret(), SDL_BLENDMODE_BLEND) }
SDL_DestroySurface(vSurface)
fDecodedSurface = null
}
return fTexture
}
override fun readPixels(
buffer: IntArray,
startX: Int,
@@ -116,24 +134,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
@@ -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")
@@ -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()
@@ -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
}
}
@@ -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
View File
@@ -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
@@ -6,28 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.AppBarWithSearch
import androidx.compose.material3.Button
import androidx.compose.material3.DockedSearchBar
import androidx.compose.material3.ExpandedDockedSearchBar
import androidx.compose.material3.ExpandedDockedSearchBarWithGap
import androidx.compose.material3.ExpandedFullScreenContainedSearchBar
import androidx.compose.material3.ExpandedFullScreenSearchBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopSearchBar
import androidx.compose.material3.rememberSearchBarState
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.Modifier
import androidx.compose.ui.unit.dp
import demo.shim.DemoIcon
@@ -3,36 +3,9 @@
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomSheet
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Button
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDragHandle
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.rememberSwipeToDismissBoxState
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
@@ -4,19 +4,8 @@ package screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LeadingIconTab
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
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.unit.dp
import demo.shim.DemoIcon

Some files were not shown because too many files have changed in this diff Show More